@fozy-labs/rx-toolkit 0.5.4 → 0.6.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.
Files changed (302) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +22 -13
  3. package/dist/common/devtools/reduxDevtools.js +17 -2
  4. package/dist/common/react/index.d.ts +1 -0
  5. package/dist/common/react/index.js +1 -0
  6. package/dist/common/react/useIsomorphicLayoutEffect.d.ts +17 -0
  7. package/dist/common/react/useIsomorphicLayoutEffect.js +17 -0
  8. package/dist/index.d.ts +1 -2
  9. package/dist/index.js +1 -2
  10. package/dist/query/api/createApi.d.ts +4 -0
  11. package/dist/query/api/createApi.js +9 -0
  12. package/dist/query/api/index.d.ts +1 -0
  13. package/dist/query/api/index.js +1 -0
  14. package/dist/query/constants.d.ts +12 -0
  15. package/dist/query/constants.js +15 -0
  16. package/dist/query/core/api/Api.d.ts +20 -0
  17. package/dist/query/core/api/Api.js +129 -0
  18. package/dist/query/core/api/constants.d.ts +2 -0
  19. package/dist/query/core/api/constants.js +3 -0
  20. package/dist/query/core/api/index.d.ts +4 -0
  21. package/dist/query/core/api/index.js +4 -0
  22. package/dist/query/core/api/mergeHooks.d.ts +2 -0
  23. package/dist/query/core/api/mergeHooks.js +26 -0
  24. package/dist/query/core/api/normalizeLinks.d.ts +2 -0
  25. package/dist/query/core/api/normalizeLinks.js +11 -0
  26. package/dist/query/core/cache/CacheEntry.d.ts +21 -0
  27. package/dist/query/core/cache/CacheEntry.js +54 -0
  28. package/dist/query/core/cache/CacheMap.d.ts +19 -0
  29. package/dist/query/core/cache/CacheMap.js +32 -0
  30. package/dist/query/core/cache/QueryCacheEntry.d.ts +21 -0
  31. package/dist/query/core/cache/QueryCacheEntry.js +136 -0
  32. package/dist/query/core/cache/index.d.ts +3 -0
  33. package/dist/query/core/cache/index.js +3 -0
  34. package/dist/query/core/command/Command.d.ts +67 -0
  35. package/dist/query/core/command/Command.js +253 -0
  36. package/dist/query/core/command/CommandAgent.d.ts +17 -0
  37. package/dist/query/core/command/CommandAgent.js +67 -0
  38. package/dist/query/core/command/LinkManager.d.ts +24 -0
  39. package/dist/query/core/command/LinkManager.js +71 -0
  40. package/dist/query/core/command/index.d.ts +2 -0
  41. package/dist/query/core/command/index.js +2 -0
  42. package/dist/query/core/errors/CacheEntryRemovedError.d.ts +7 -0
  43. package/dist/query/core/errors/CacheEntryRemovedError.js +9 -0
  44. package/dist/query/core/errors/MachineStateError.d.ts +8 -0
  45. package/dist/query/core/errors/MachineStateError.js +10 -0
  46. package/dist/query/core/errors/MachineTransitionError.d.ts +7 -0
  47. package/dist/query/core/errors/MachineTransitionError.js +9 -0
  48. package/dist/query/core/errors/index.d.ts +3 -0
  49. package/dist/query/core/errors/index.js +3 -0
  50. package/dist/query/core/index.d.ts +8 -0
  51. package/dist/query/core/index.js +8 -0
  52. package/dist/query/core/machine/Machine.d.ts +21 -0
  53. package/dist/query/core/machine/Machine.js +42 -0
  54. package/dist/query/core/machine/MachineBase.d.ts +31 -0
  55. package/dist/query/core/machine/MachineBase.js +176 -0
  56. package/dist/query/core/machine/MachineError.d.ts +10 -0
  57. package/dist/query/core/machine/MachineError.js +19 -0
  58. package/dist/query/core/machine/MachinePending.d.ts +13 -0
  59. package/dist/query/core/machine/MachinePending.js +32 -0
  60. package/dist/query/core/machine/MachineRefreshError.d.ts +12 -0
  61. package/dist/query/core/machine/MachineRefreshError.js +23 -0
  62. package/dist/query/core/machine/MachineRefreshing.d.ts +15 -0
  63. package/dist/query/core/machine/MachineRefreshing.js +43 -0
  64. package/dist/query/core/machine/MachineSuccess.d.ts +12 -0
  65. package/dist/query/core/machine/MachineSuccess.js +23 -0
  66. package/dist/query/core/machine/MachineWithData.d.ts +25 -0
  67. package/dist/query/core/machine/MachineWithData.js +73 -0
  68. package/dist/query/core/machine/index.d.ts +9 -0
  69. package/dist/query/core/machine/index.js +9 -0
  70. package/dist/query/core/machine/machine-helpers.d.ts +9 -0
  71. package/dist/query/core/machine/machine-helpers.js +80 -0
  72. package/dist/query/core/patcher/Patcher.d.ts +30 -0
  73. package/dist/query/core/patcher/Patcher.js +122 -0
  74. package/dist/query/core/patcher/index.d.ts +1 -0
  75. package/dist/query/core/patcher/index.js +1 -0
  76. package/dist/query/core/resource/Resource.d.ts +105 -0
  77. package/dist/query/core/resource/Resource.js +340 -0
  78. package/dist/query/core/resource/ResourceAgent.d.ts +37 -0
  79. package/dist/query/core/resource/ResourceAgent.js +179 -0
  80. package/dist/query/core/resource/index.d.ts +2 -0
  81. package/dist/query/core/resource/index.js +2 -0
  82. package/dist/query/core/snapshoter/Snapshoter.d.ts +22 -0
  83. package/dist/query/core/snapshoter/Snapshoter.js +78 -0
  84. package/dist/query/core/snapshoter/index.d.ts +2 -0
  85. package/dist/query/core/snapshoter/index.js +1 -0
  86. package/dist/query/core/syncer/Syncer.d.ts +32 -0
  87. package/dist/query/core/syncer/Syncer.js +85 -0
  88. package/dist/query/core/syncer/index.d.ts +2 -0
  89. package/dist/query/core/syncer/index.js +1 -0
  90. package/dist/query/index.d.ts +5 -10
  91. package/dist/query/index.js +5 -13
  92. package/dist/query/lib/broadcastSyncDriver.d.ts +5 -0
  93. package/dist/query/lib/broadcastSyncDriver.js +46 -0
  94. package/dist/query/lib/index.d.ts +3 -0
  95. package/dist/query/lib/index.js +3 -0
  96. package/dist/{query-v2 → query}/lib/stableStringify.js +5 -3
  97. package/dist/query/lib/toKeyed.d.ts +11 -0
  98. package/dist/query/lib/toKeyed.js +18 -0
  99. package/dist/query/react/ReactHooksPlugin.d.ts +31 -0
  100. package/dist/query/react/ReactHooksPlugin.js +21 -0
  101. package/dist/query/react/index.d.ts +3 -0
  102. package/dist/query/react/index.js +3 -0
  103. package/dist/query/react/useCommand.d.ts +2 -0
  104. package/dist/query/react/useCommand.js +14 -0
  105. package/dist/query/react/useResource.d.ts +2 -0
  106. package/dist/query/react/useResource.js +16 -0
  107. package/dist/query/types/api.d.ts +39 -0
  108. package/dist/query/types/cache.d.ts +51 -0
  109. package/dist/query/types/command.d.ts +52 -0
  110. package/dist/query/types/common.d.ts +65 -0
  111. package/dist/query/types/index.d.ts +8 -4
  112. package/dist/query/types/index.js +8 -5
  113. package/dist/query/types/plugin-hkt.d.ts +64 -0
  114. package/dist/query/types/resource.d.ts +49 -0
  115. package/dist/query/types/snapshot.d.ts +27 -0
  116. package/dist/query/types/snapshot.js +2 -0
  117. package/dist/query/types/state.d.ts +24 -0
  118. package/dist/signals/base/ComputeCache.js +1 -1
  119. package/dist/signals/base/Devtools.js +2 -6
  120. package/dist/signals/signals/Computed.d.ts +1 -0
  121. package/dist/signals/signals/Computed.js +5 -1
  122. package/dist/signals/signals/Effect.d.ts +0 -4
  123. package/dist/signals/signals/Effect.js +0 -6
  124. package/dist/signals/signals/LocalState.d.ts +1 -10
  125. package/dist/signals/signals/LocalState.js +0 -12
  126. package/dist/signals/signals/Signal.d.ts +2 -8
  127. package/dist/signals/signals/Signal.js +1 -10
  128. package/dist/signals/signals/State.d.ts +2 -1
  129. package/dist/signals/signals/State.js +9 -0
  130. package/dist/signals/types/SignalOptions.d.ts +0 -2
  131. package/dist/signals/types/normalizeSignalOptions.js +0 -3
  132. package/dist/signals/types/signals.types.d.ts +3 -1
  133. package/docs/CHANGELOG.md +48 -2
  134. package/docs/migrations/0.6.0.md +224 -0
  135. package/docs/query/README.md +52 -562
  136. package/docs/query/api/README.md +59 -0
  137. package/docs/query/api/_CacheEntry.md +39 -0
  138. package/docs/query/api/_CacheMap.md +30 -0
  139. package/docs/query/api/_QueryCacheEntry.md +81 -0
  140. package/docs/query/api/command-agent.md +60 -0
  141. package/docs/query/api/command.md +76 -0
  142. package/docs/query/api/resource-agent.md +68 -0
  143. package/docs/query/api/resource.md +77 -0
  144. package/docs/query/concepts/agent.md +70 -0
  145. package/docs/query/concepts/architecture.md +139 -0
  146. package/docs/query/concepts/cache.md +81 -0
  147. package/docs/query/concepts/dataflows.md +473 -0
  148. package/docs/query/concepts/keyed.md +42 -0
  149. package/docs/query/concepts/machine.md +106 -0
  150. package/docs/query/concepts/patching.md +85 -0
  151. package/docs/query/usage/broadcast.md +188 -0
  152. package/docs/query/usage/command.md +203 -0
  153. package/docs/query/usage/lifecycle.md +114 -0
  154. package/docs/query/usage/links.md +125 -0
  155. package/docs/query/usage/plugins.md +96 -0
  156. package/docs/query/usage/resource.md +206 -0
  157. package/docs/query/usage/snapshot.md +80 -0
  158. package/docs/usage/react/README.md +45 -91
  159. package/package.json +5 -7
  160. package/dist/query/SKIP_TOKEN.d.ts +0 -1
  161. package/dist/query/SKIP_TOKEN.js +0 -1
  162. package/dist/query/api/createCommand.d.ts +0 -21
  163. package/dist/query/api/createCommand.js +0 -20
  164. package/dist/query/api/createOperation.d.ts +0 -5
  165. package/dist/query/api/createOperation.js +0 -6
  166. package/dist/query/api/createResource.d.ts +0 -3
  167. package/dist/query/api/createResource.js +0 -2
  168. package/dist/query/api/createResourceDuplicator.d.ts +0 -4
  169. package/dist/query/api/createResourceDuplicator.js +0 -2
  170. package/dist/query/api/resetAllQueriesCache.d.ts +0 -1
  171. package/dist/query/api/resetAllQueriesCache.js +0 -4
  172. package/dist/query/core/Command/Command.d.ts +0 -35
  173. package/dist/query/core/Command/Command.js +0 -210
  174. package/dist/query/core/Command/CommandAgent.d.ts +0 -19
  175. package/dist/query/core/Command/CommandAgent.js +0 -54
  176. package/dist/query/core/Command/index.d.ts +0 -2
  177. package/dist/query/core/Command/index.js +0 -2
  178. package/dist/query/core/Operation/Operation.d.ts +0 -8
  179. package/dist/query/core/Operation/Operation.js +0 -4
  180. package/dist/query/core/Operation/OperationAgent.d.ts +0 -4
  181. package/dist/query/core/Operation/OperationAgent.js +0 -4
  182. package/dist/query/core/QueriesCache.d.ts +0 -9
  183. package/dist/query/core/QueriesCache.js +0 -28
  184. package/dist/query/core/QueriesLifetimeHooks.d.ts +0 -22
  185. package/dist/query/core/QueriesLifetimeHooks.js +0 -86
  186. package/dist/query/core/ResetAllQueriesSignal.d.ts +0 -6
  187. package/dist/query/core/ResetAllQueriesSignal.js +0 -11
  188. package/dist/query/core/Resource/Resource.d.ts +0 -51
  189. package/dist/query/core/Resource/Resource.js +0 -232
  190. package/dist/query/core/Resource/ResourceAgent.d.ts +0 -35
  191. package/dist/query/core/Resource/ResourceAgent.js +0 -110
  192. package/dist/query/core/Resource/ResourceDuplicator.d.ts +0 -73
  193. package/dist/query/core/Resource/ResourceDuplicator.js +0 -227
  194. package/dist/query/core/Resource/ResourceDuplicatorAgent.d.ts +0 -35
  195. package/dist/query/core/Resource/ResourceDuplicatorAgent.js +0 -110
  196. package/dist/query/core/Resource/ResourceRef.d.ts +0 -16
  197. package/dist/query/core/Resource/ResourceRef.js +0 -136
  198. package/dist/query/lib/IndirectMap.d.ts +0 -19
  199. package/dist/query/lib/IndirectMap.js +0 -88
  200. package/dist/query/lib/ReactiveCache.d.ts +0 -62
  201. package/dist/query/lib/ReactiveCache.js +0 -80
  202. package/dist/query/react/useCommandAgent.d.ts +0 -24
  203. package/dist/query/react/useCommandAgent.js +0 -39
  204. package/dist/query/react/useOperationAgent.d.ts +0 -6
  205. package/dist/query/react/useOperationAgent.js +0 -6
  206. package/dist/query/react/useResourceAgent.d.ts +0 -6
  207. package/dist/query/react/useResourceAgent.js +0 -31
  208. package/dist/query/react/useResourceRef.d.ts +0 -5
  209. package/dist/query/react/useResourceRef.js +0 -13
  210. package/dist/query/types/Command.types.d.ts +0 -154
  211. package/dist/query/types/Command.types.js +0 -1
  212. package/dist/query/types/Operation.types.d.ts +0 -13
  213. package/dist/query/types/Operation.types.js +0 -1
  214. package/dist/query/types/Resource.types.d.ts +0 -129
  215. package/dist/query/types/Resource.types.js +0 -1
  216. package/dist/query/types/shared.types.d.ts +0 -26
  217. package/dist/query/types/shared.types.js +0 -1
  218. package/dist/query-v2/api/createApi.d.ts +0 -10
  219. package/dist/query-v2/api/createApi.js +0 -83
  220. package/dist/query-v2/core/common/CacheEntry.d.ts +0 -29
  221. package/dist/query-v2/core/common/CacheEntry.js +0 -71
  222. package/dist/query-v2/core/common/CacheMap.d.ts +0 -38
  223. package/dist/query-v2/core/common/CacheMap.js +0 -127
  224. package/dist/query-v2/core/common/LifecycleHooks.d.ts +0 -22
  225. package/dist/query-v2/core/common/LifecycleHooks.js +0 -104
  226. package/dist/query-v2/core/common/index.d.ts +0 -3
  227. package/dist/query-v2/core/common/index.js +0 -3
  228. package/dist/query-v2/core/index.d.ts +0 -3
  229. package/dist/query-v2/core/index.js +0 -3
  230. package/dist/query-v2/core/machines/Machine.d.ts +0 -14
  231. package/dist/query-v2/core/machines/Machine.js +0 -33
  232. package/dist/query-v2/core/machines/MachineError.d.ts +0 -11
  233. package/dist/query-v2/core/machines/MachineError.js +0 -26
  234. package/dist/query-v2/core/machines/MachineIdle.d.ts +0 -8
  235. package/dist/query-v2/core/machines/MachineIdle.js +0 -19
  236. package/dist/query-v2/core/machines/MachinePending.d.ts +0 -12
  237. package/dist/query-v2/core/machines/MachinePending.js +0 -29
  238. package/dist/query-v2/core/machines/MachineRefreshing.d.ts +0 -14
  239. package/dist/query-v2/core/machines/MachineRefreshing.js +0 -46
  240. package/dist/query-v2/core/machines/MachineSuccess.d.ts +0 -16
  241. package/dist/query-v2/core/machines/MachineSuccess.js +0 -42
  242. package/dist/query-v2/core/machines/MachineWithData.d.ts +0 -18
  243. package/dist/query-v2/core/machines/MachineWithData.js +0 -40
  244. package/dist/query-v2/core/machines/Patcher.d.ts +0 -20
  245. package/dist/query-v2/core/machines/Patcher.js +0 -104
  246. package/dist/query-v2/core/machines/index.d.ts +0 -8
  247. package/dist/query-v2/core/machines/index.js +0 -8
  248. package/dist/query-v2/core/resource/ResourceV2.d.ts +0 -120
  249. package/dist/query-v2/core/resource/ResourceV2.js +0 -464
  250. package/dist/query-v2/core/resource/ResourceV2Agent.d.ts +0 -26
  251. package/dist/query-v2/core/resource/ResourceV2Agent.js +0 -132
  252. package/dist/query-v2/core/resource/index.d.ts +0 -2
  253. package/dist/query-v2/core/resource/index.js +0 -2
  254. package/dist/query-v2/index.d.ts +0 -11
  255. package/dist/query-v2/index.js +0 -17
  256. package/dist/query-v2/lib/NO_VALUE.d.ts +0 -2
  257. package/dist/query-v2/lib/NO_VALUE.js +0 -1
  258. package/dist/query-v2/lib/SKIP_TOKEN.d.ts +0 -2
  259. package/dist/query-v2/lib/SKIP_TOKEN.js +0 -1
  260. package/dist/query-v2/lib/index.d.ts +0 -4
  261. package/dist/query-v2/lib/index.js +0 -3
  262. package/dist/query-v2/plugins/ReactHooksPlugin.d.ts +0 -25
  263. package/dist/query-v2/plugins/ReactHooksPlugin.js +0 -19
  264. package/dist/query-v2/plugins/types.d.ts +0 -1
  265. package/dist/query-v2/react/__tests__/helpers.d.ts +0 -12
  266. package/dist/query-v2/react/__tests__/helpers.js +0 -33
  267. package/dist/query-v2/react/index.d.ts +0 -2
  268. package/dist/query-v2/react/index.js +0 -2
  269. package/dist/query-v2/react/useResourceV2Agent.d.ts +0 -12
  270. package/dist/query-v2/react/useResourceV2Agent.js +0 -36
  271. package/dist/query-v2/react/useResourceV2Ref.d.ts +0 -12
  272. package/dist/query-v2/react/useResourceV2Ref.js +0 -57
  273. package/dist/query-v2/snapshot/Snapshot.d.ts +0 -13
  274. package/dist/query-v2/snapshot/Snapshot.js +0 -76
  275. package/dist/query-v2/types/agent.types.d.ts +0 -54
  276. package/dist/query-v2/types/api.types.d.ts +0 -22
  277. package/dist/query-v2/types/cache.types.d.ts +0 -37
  278. package/dist/query-v2/types/index.d.ts +0 -9
  279. package/dist/query-v2/types/index.js +0 -9
  280. package/dist/query-v2/types/lifecycle.types.d.ts +0 -25
  281. package/dist/query-v2/types/machine.types.d.ts +0 -67
  282. package/dist/query-v2/types/plugin.types.d.ts +0 -38
  283. package/dist/query-v2/types/resource.types.d.ts +0 -35
  284. package/dist/query-v2/types/resource.types.js +0 -1
  285. package/dist/query-v2/types/shared.types.d.ts +0 -20
  286. package/dist/query-v2/types/shared.types.js +0 -1
  287. package/dist/query-v2/types/snapshot.types.d.ts +0 -21
  288. package/dist/query-v2/types/snapshot.types.js +0 -1
  289. package/docs/contributing/query-v2/README.md +0 -379
  290. package/docs/migrations/query-v2.md +0 -171
  291. package/docs/query-v2/README.md +0 -280
  292. package/docs/query-v2/api-reference.md +0 -235
  293. package/docs/query-v2/optimistic-updates.md +0 -148
  294. package/docs/query-v2/ssr.md +0 -130
  295. /package/dist/{query-v2 → query}/lib/stableStringify.d.ts +0 -0
  296. /package/dist/{query-v2/plugins/types.js → query/types/api.js} +0 -0
  297. /package/dist/{query-v2/types/agent.types.js → query/types/cache.js} +0 -0
  298. /package/dist/{query-v2/types/api.types.js → query/types/command.js} +0 -0
  299. /package/dist/{query-v2/types/cache.types.js → query/types/common.js} +0 -0
  300. /package/dist/{query-v2/types/lifecycle.types.js → query/types/plugin-hkt.js} +0 -0
  301. /package/dist/{query-v2/types/machine.types.js → query/types/resource.js} +0 -0
  302. /package/dist/{query-v2/types/plugin.types.js → query/types/state.js} +0 -0
@@ -0,0 +1,340 @@
1
+ import { Signal } from "../../../signals";
2
+ import { toKeyed as toKeyedUtil } from "../../lib/toKeyed";
3
+ import { CacheMap } from "../cache/CacheMap";
4
+ import { QueryCacheEntry } from "../cache/QueryCacheEntry";
5
+ import { CacheEntryRemovedError } from "../errors";
6
+ import { Machine } from "../machine/Machine";
7
+ import { ResourceAgent } from "./ResourceAgent";
8
+ // ==================== Resource ====================
9
+ /**
10
+ * Data-fetching abstraction with caching and SWR.
11
+ *
12
+ * Each unique set of serialized arguments maps to a single {@link QueryCacheEntry}.
13
+ * Entries are retained for `retentionTime` ms after the last subscriber unsubscribes.
14
+ *
15
+ * @template TArgs - Query argument type.
16
+ * @template TData - Query return data type.
17
+ */
18
+ export class Resource {
19
+ _cache = new CacheMap();
20
+ /**
21
+ * Хранит последний добавленный кэш-энтер, для возможности реактивной подписки (с помощью getEntry$)
22
+ */
23
+ _lastEntry$ = Signal.state(null, { isDisabled: true });
24
+ /**
25
+ * Определяет общий статус ресурса
26
+ * - "idle": ресурса не активен, записей нет, getEntry$ возвращает null
27
+ * - "running": ресурс активен, есть хотя бы одна запись, getEntry$ может возвращать записи
28
+ */
29
+ _status$ = Signal.state("idle", { isDisabled: true });
30
+ _queryFn;
31
+ _key;
32
+ _retentionTime;
33
+ _serializeArgs;
34
+ _onCacheEntryAdded;
35
+ _onQueryStarted;
36
+ _beforeQuery;
37
+ constructor(config) {
38
+ this._queryFn = config.queryFn;
39
+ this._key = config.key;
40
+ this._retentionTime = config.retentionTime;
41
+ this._serializeArgs = config.serializeArgs;
42
+ this._onCacheEntryAdded = config.onCacheEntryAdded;
43
+ this._onQueryStarted = config.onQueryStarted;
44
+ this._beforeQuery = config.beforeQuery;
45
+ if (config.snapshot) {
46
+ for (const [key, snap] of Object.entries(config.snapshot.entries)) {
47
+ this._hydrateEntry(key, {
48
+ args: snap.args,
49
+ data: snap.data,
50
+ updatedAt: snap.updatedAt,
51
+ isStale: snap.isStale ?? false,
52
+ });
53
+ }
54
+ }
55
+ }
56
+ // ==================== Public API ====================
57
+ /**
58
+ * Execute a query with the given arguments.
59
+ *
60
+ * @param args - Query arguments.
61
+ * @param doForce - When `true`, forces a refresh even if data is cached.
62
+ */
63
+ trigger(args, doForce = false) {
64
+ this._getOrCreate(args, doForce);
65
+ }
66
+ /**
67
+ * Mark the entry as stale and trigger a background SWR refresh.
68
+ *
69
+ * @param args - Query arguments identifying the cache entry.
70
+ */
71
+ refresh(args) {
72
+ const keyed = this.toKeyed(args);
73
+ const entry = this._cache.get(keyed.key);
74
+ if (entry) {
75
+ entry.refresh();
76
+ }
77
+ }
78
+ /**
79
+ * Synchronously return the cache entry for the given arguments.
80
+ *
81
+ * @param args - Query arguments (or `void` when `TArgs` is `void`).
82
+ * @param doInitiate - When `true`, creates and starts the entry if absent.
83
+ * @returns The cache entry, or `null` if not found and `doInitiate` is `false`.
84
+ */
85
+ getEntry(args, doInitiate = false) {
86
+ const keyed = this.toKeyed(args);
87
+ const entry = this._cache.get(keyed.key);
88
+ if (entry) {
89
+ return entry;
90
+ }
91
+ if (doInitiate) {
92
+ return this._getOrCreate(keyed);
93
+ }
94
+ return null;
95
+ }
96
+ getEntry$(args, doInitiate = false) {
97
+ return Signal.compute(() => {
98
+ const keyed = this.toKeyed(args);
99
+ const status = this._status$();
100
+ if (status === "idle" && !doInitiate) {
101
+ return null;
102
+ }
103
+ const lastEntry = this._lastEntry$();
104
+ if (lastEntry?.keyedArgs.key === keyed.key) {
105
+ return lastEntry;
106
+ }
107
+ return this._cache.get(keyed.key) ?? null;
108
+ }, { isDisabled: true });
109
+ }
110
+ /**
111
+ * Create a reactive {@link ResourceAgent} that observes this resource
112
+ * and provides SWR-aware state transitions.
113
+ */
114
+ createAgent() {
115
+ return new ResourceAgent(this);
116
+ }
117
+ /**
118
+ * Serialize arguments into a cache key string.
119
+ *
120
+ * @param args - Query arguments.
121
+ * @returns The serialized key used for cache lookup.
122
+ */
123
+ serialize(args) {
124
+ return this.toKeyed(args).key;
125
+ }
126
+ /**
127
+ * Wrap arguments into a `{ value, key }` pair, avoiding repeated serialization.
128
+ *
129
+ * @param args - Query arguments.
130
+ * @returns A {@link Keyed} wrapper containing the original args and their cache key.
131
+ */
132
+ toKeyed(args) {
133
+ return toKeyedUtil(args, this._serializeArgs);
134
+ }
135
+ /** Iterate over all cache entries. */
136
+ getEntries() {
137
+ return this._cache.values();
138
+ }
139
+ /** Clear all cache entries. */
140
+ reset() {
141
+ for (const entry of this._cache.values()) {
142
+ entry.complete();
143
+ }
144
+ this._cache.clear();
145
+ this._status$.set("idle");
146
+ this._lastEntry$.set(null);
147
+ }
148
+ // ==================== Private ====================
149
+ /**
150
+ * Get an existing cache entry or create a new one.
151
+ *
152
+ * @internal Used by {@link ResourceAgent} and Command links.
153
+ * @param args - Query arguments.
154
+ * @param doForce - When `true`, forces a refresh on an existing entry.
155
+ */
156
+ _getOrCreate(args, doForce = false) {
157
+ const keyed = this.toKeyed(args);
158
+ const existing = this._cache.get(keyed.key);
159
+ if (existing) {
160
+ if (doForce)
161
+ existing.refresh();
162
+ return existing;
163
+ }
164
+ return this._createEntry(keyed);
165
+ }
166
+ _createEntry(keyed, initialMachine) {
167
+ // ── beforeQuery sync intercept ──
168
+ // If beforeQuery is set AND there's no snapshot (initialMachine), intercept
169
+ // to ask other tabs for data before executing queryFn.
170
+ if (!initialMachine && this._beforeQuery && this._key) {
171
+ return this._createEntryWithBeforeQuery(keyed);
172
+ }
173
+ return this._createEntryDirect(keyed, initialMachine);
174
+ }
175
+ /** Standard entry creation: queryFn auto-executes in constructor. */
176
+ _createEntryDirect(keyed, initialMachine) {
177
+ // Capture initial query promise for onQueryStarted lifecycle hook.
178
+ // During the QueryCacheEntry constructor, _execute() fires synchronously,
179
+ // calling wrappedQueryFn before `entry` is assigned. We save the promise
180
+ // in the `else` branch and fire onQueryStarted after construction.
181
+ // eslint-disable-next-line prefer-const -- assigned after constructor; closure reads it
182
+ let entry;
183
+ let initialQueryPromise = null;
184
+ const wrappedQueryFn = (keyedArgs, signal) => {
185
+ const promise = this._queryFn(keyedArgs.value, signal);
186
+ if (entry) {
187
+ // Subsequent calls (refresh / retry) — entry is already assigned
188
+ this._fireOnQueryStarted(entry, keyedArgs.value, promise);
189
+ }
190
+ else {
191
+ // Initial call during constructor — defer
192
+ initialQueryPromise = promise;
193
+ }
194
+ return promise;
195
+ };
196
+ entry = new QueryCacheEntry({
197
+ queryFn: wrappedQueryFn,
198
+ retentionTime: this._retentionTime,
199
+ keyedArgs: keyed,
200
+ resourceKey: this._key,
201
+ initialMachine,
202
+ beforeDevtoolsPush: undefined,
203
+ });
204
+ // Register in cache
205
+ this._cache.set(keyed.key, entry);
206
+ this._status$.set("running");
207
+ this._lastEntry$.set(entry);
208
+ // Cleanup: remove entry from cache when it completes (retention expired)
209
+ entry.completed$.subscribe(() => {
210
+ this._cache.delete(keyed.key);
211
+ if (this._cache.size === 0)
212
+ this._status$.set("idle");
213
+ if (this._lastEntry$() === entry) {
214
+ this._lastEntry$.set(null);
215
+ }
216
+ });
217
+ // Fire onCacheEntryAdded lifecycle hook
218
+ this._fireOnCacheEntryAdded(entry, keyed);
219
+ // Fire onQueryStarted for the initial query (deferred from constructor)
220
+ if (initialQueryPromise) {
221
+ this._fireOnQueryStarted(entry, keyed.value, initialQueryPromise);
222
+ }
223
+ return entry;
224
+ }
225
+ /** Entry creation with beforeQuery intercept: starts in pending, asks other tabs first. */
226
+ _createEntryWithBeforeQuery(keyed) {
227
+ const wrappedQueryFn = (keyedArgs, signal) => {
228
+ const promise = this._queryFn(keyedArgs.value, signal);
229
+ this._fireOnQueryStarted(entry, keyedArgs.value, promise);
230
+ return promise;
231
+ };
232
+ // Create entry with an explicit pending Machine to PREVENT auto-execute
233
+ const entry = new QueryCacheEntry({
234
+ queryFn: wrappedQueryFn,
235
+ retentionTime: this._retentionTime,
236
+ keyedArgs: keyed,
237
+ resourceKey: this._key,
238
+ initialMachine: Machine.pending(keyed.value),
239
+ beforeDevtoolsPush: undefined,
240
+ });
241
+ // Register in cache immediately (UI sees pending state)
242
+ this._cache.set(keyed.key, entry);
243
+ this._status$.set("running");
244
+ this._lastEntry$.set(entry);
245
+ entry.completed$.subscribe(() => {
246
+ this._cache.delete(keyed.key);
247
+ if (this._cache.size === 0)
248
+ this._status$.set("idle");
249
+ if (this._lastEntry$() === entry) {
250
+ this._lastEntry$.set(null);
251
+ }
252
+ });
253
+ this._fireOnCacheEntryAdded(entry, keyed);
254
+ // Ask other tabs for data, fall back to queryFn
255
+ this._beforeQuery(this._key, keyed.key)
256
+ .then((result) => {
257
+ if (result) {
258
+ const machine = entry.machine$.peek();
259
+ if (machine.status === "pending") {
260
+ entry.set(machine.success(result.data));
261
+ }
262
+ }
263
+ else {
264
+ entry._execute();
265
+ }
266
+ })
267
+ .catch(() => {
268
+ entry._execute();
269
+ });
270
+ return entry;
271
+ }
272
+ _hydrateEntry(key, meta) {
273
+ const machine = Machine.fromSnapshot(meta, meta.isStale);
274
+ const keyed = toKeyedUtil(meta.args, this._serializeArgs);
275
+ // Verify key matches
276
+ if (keyed.key !== key) {
277
+ console.warn(`[rx-toolkit] Snapshot hydration skipped: expected key "${key}" but serialized args produced key "${keyed.key}".`);
278
+ return;
279
+ }
280
+ this._createEntry(keyed, machine);
281
+ }
282
+ _fireOnCacheEntryAdded(entry, keyed) {
283
+ if (!this._onCacheEntryAdded)
284
+ return;
285
+ // $cacheDataLoaded: resolves with data on first success, rejects if entry removed first
286
+ // $cacheEntryRemoved: resolves when entry is removed from cache
287
+ let resolveRemoved;
288
+ const $cacheEntryRemoved = new Promise((resolve) => {
289
+ resolveRemoved = resolve;
290
+ });
291
+ const $cacheDataLoaded = new Promise((resolve, reject) => {
292
+ // Watch the entry's state for the first success
293
+ const sub = entry.state$.obs.subscribe((machine) => {
294
+ if (machine.state.status === "success" || machine.state.status === "refreshing") {
295
+ resolve(machine.state.data);
296
+ sub.unsubscribe();
297
+ }
298
+ });
299
+ // If the entry is cleaned up before success, reject
300
+ entry.completed$.subscribe(() => {
301
+ sub.unsubscribe();
302
+ reject(new CacheEntryRemovedError("data loaded"));
303
+ });
304
+ });
305
+ // When entry is cleaned up, resolve $cacheEntryRemoved
306
+ entry.completed$.subscribe(() => {
307
+ resolveRemoved();
308
+ });
309
+ const ctx = {
310
+ entry,
311
+ $cacheDataLoaded,
312
+ $cacheEntryRemoved,
313
+ };
314
+ try {
315
+ const result = this._onCacheEntryAdded(keyed.value, ctx);
316
+ // Hook may be async — suppress unhandled rejection
317
+ void Promise.resolve(result).catch(() => { });
318
+ }
319
+ catch {
320
+ // Lifecycle errors are suppressed (per docs)
321
+ }
322
+ }
323
+ _fireOnQueryStarted(entry, args, queryPromise) {
324
+ if (!this._onQueryStarted)
325
+ return;
326
+ const $queryFulfilled = queryPromise.then((data) => ({ data }));
327
+ const ctx = {
328
+ entry,
329
+ $queryFulfilled,
330
+ };
331
+ try {
332
+ const result = this._onQueryStarted(args, ctx);
333
+ // Hook may be async — suppress unhandled rejection
334
+ void Promise.resolve(result).catch(() => { });
335
+ }
336
+ catch {
337
+ // Lifecycle errors are suppressed (per docs)
338
+ }
339
+ }
340
+ }
@@ -0,0 +1,37 @@
1
+ import type { ArgsOrVoidOrSkip, IResourceAgent, TResourceAgentState } from "../../../query/types";
2
+ import type { Resource } from "./Resource";
3
+ /**
4
+ * Reactive observer for a {@link Resource} with SWR behaviour.
5
+ *
6
+ * The agent tracks a single cache entry at a time, deriving a flat
7
+ * {@link TResourceAgentState} signal. When arguments change via {@link start},
8
+ * the previous entry's data is preserved as stale fallback (SWR).
9
+ *
10
+ * @template TArgs - Query argument type.
11
+ * @template TData - Query return data type.
12
+ */
13
+ export declare class ResourceAgent<TArgs, TData> implements IResourceAgent<TArgs, TData> {
14
+ private readonly _resource;
15
+ private readonly _tracking$;
16
+ readonly state$: import("../../../signals").ComputeFn<TResourceAgentState<TArgs, TData>>;
17
+ private _previous$;
18
+ private _isStarted;
19
+ private _isMarked;
20
+ constructor(resource: Resource<TArgs, TData>);
21
+ get args(): TArgs | null;
22
+ /**
23
+ */
24
+ start(): void;
25
+ /**
26
+ */
27
+ set(args: ArgsOrVoidOrSkip<TArgs>, mark: boolean): void;
28
+ /** Retry the last failed query. Only meaningful after an error state. */
29
+ retry: () => void;
30
+ /** Force a background refresh of the current entry (SWR). */
31
+ refresh: () => void;
32
+ private _deriveState;
33
+ private _promoteToPrevious;
34
+ private _deriveNotIdleState;
35
+ private _createPendingState;
36
+ private _idleState;
37
+ }
@@ -0,0 +1,179 @@
1
+ import { Batcher, Signal } from "../../../signals";
2
+ import { SKIP } from "../../constants";
3
+ /**
4
+ * Reactive observer for a {@link Resource} with SWR behaviour.
5
+ *
6
+ * The agent tracks a single cache entry at a time, deriving a flat
7
+ * {@link TResourceAgentState} signal. When arguments change via {@link start},
8
+ * the previous entry's data is preserved as stale fallback (SWR).
9
+ *
10
+ * @template TArgs - Query argument type.
11
+ * @template TData - Query return data type.
12
+ */
13
+ export class ResourceAgent {
14
+ _resource;
15
+ _tracking$ = Signal.state(null, { isDisabled: true });
16
+ state$ = Signal.compute(() => this._deriveState(), {
17
+ isDisabled: true,
18
+ });
19
+ _previous$ = null;
20
+ _isStarted = false;
21
+ _isMarked = false;
22
+ constructor(resource) {
23
+ this._resource = resource;
24
+ }
25
+ get args() {
26
+ return this._tracking$.peek()?.keyed.value ?? null;
27
+ }
28
+ // ==================== Public API (IResourceAgent) ====================
29
+ /**
30
+ */
31
+ start() {
32
+ this._isStarted = true;
33
+ const tracking = this._tracking$.peek();
34
+ if (!tracking) {
35
+ return;
36
+ }
37
+ this._resource.trigger(tracking.keyed);
38
+ }
39
+ /**
40
+ */
41
+ set(args, mark) {
42
+ this._isMarked = mark ?? false;
43
+ const tracking = this._tracking$.peek();
44
+ if (args === SKIP) {
45
+ if (!tracking)
46
+ return;
47
+ this._previous$ = null;
48
+ this._tracking$.set(null);
49
+ return;
50
+ }
51
+ const keyed = this._resource.toKeyed(args);
52
+ // Early return if same args
53
+ if (tracking && tracking.keyed.key === keyed.key) {
54
+ return;
55
+ }
56
+ if (tracking) {
57
+ this._promoteToPrevious(tracking);
58
+ }
59
+ const newEntry = this._resource.getEntry$(keyed);
60
+ Batcher.run(() => {
61
+ if (this._isStarted) {
62
+ this._resource.trigger(keyed);
63
+ }
64
+ this._tracking$.set({
65
+ keyed,
66
+ current$: newEntry,
67
+ });
68
+ });
69
+ }
70
+ /** Retry the last failed query. Only meaningful after an error state. */
71
+ retry = () => {
72
+ this._tracking$.peek()?.current$.peek()?.retry();
73
+ };
74
+ /** Force a background refresh of the current entry (SWR). */
75
+ refresh = () => {
76
+ this._tracking$.peek()?.current$.peek()?.refresh();
77
+ };
78
+ // ==================== Private ====================
79
+ _deriveState() {
80
+ const tracking = this._tracking$();
81
+ if (!tracking)
82
+ return this._idleState;
83
+ const entry = tracking.current$();
84
+ if (!entry) {
85
+ if (this._isStarted) {
86
+ queueMicrotask(() => this._resource.trigger(tracking.keyed));
87
+ return this._createPendingState(tracking.keyed.value);
88
+ }
89
+ if (this._isMarked) {
90
+ return this._createPendingState(tracking.keyed.value);
91
+ }
92
+ return this._idleState;
93
+ }
94
+ const machine = entry.machine$();
95
+ return this._deriveNotIdleState(machine.state);
96
+ }
97
+ _promoteToPrevious(tracking) {
98
+ if (!tracking)
99
+ return;
100
+ const current$ = tracking.current$;
101
+ if (current$) {
102
+ const status = current$.peek()?.machine$.peek().state.status;
103
+ if (status === "success" || status === "refreshing" || status === "refresh-error") {
104
+ this._previous$ = current$;
105
+ }
106
+ else {
107
+ this._previous$ = null;
108
+ }
109
+ }
110
+ }
111
+ _deriveNotIdleState(machineState) {
112
+ let agentStatus = machineState.status;
113
+ let data = machineState.data;
114
+ const previousEntry = this._previous$?.();
115
+ // SWR: pending + previous data → refreshing
116
+ if (machineState.status === "pending" && previousEntry) {
117
+ const prevMachine = previousEntry.machine$();
118
+ if (prevMachine.state.data != null) {
119
+ agentStatus = "refreshing";
120
+ data = prevMachine.state.data;
121
+ }
122
+ }
123
+ // SWR: error + previous data → keep stale data
124
+ if (machineState.status === "error" && previousEntry) {
125
+ const prevMachine = previousEntry.machine$();
126
+ if (prevMachine.state.data != null) {
127
+ data = prevMachine.state.data;
128
+ }
129
+ }
130
+ // Clear previous once success
131
+ if (machineState.status === "success") {
132
+ this._previous$ = null;
133
+ }
134
+ return {
135
+ status: agentStatus,
136
+ data,
137
+ error: machineState.error,
138
+ args: machineState.args,
139
+ isLoading: agentStatus === "pending" || agentStatus === "refreshing",
140
+ isInitialLoading: agentStatus === "pending",
141
+ isRefreshing: agentStatus === "refreshing",
142
+ isRefreshError: agentStatus === "refresh-error",
143
+ isSuccess: agentStatus === "success",
144
+ isError: agentStatus === "error" || agentStatus === "refresh-error",
145
+ retry: this.retry,
146
+ refresh: this.refresh,
147
+ };
148
+ }
149
+ _createPendingState(args) {
150
+ return {
151
+ status: "pending",
152
+ data: null,
153
+ error: null,
154
+ args,
155
+ isLoading: true,
156
+ isInitialLoading: true,
157
+ isRefreshing: false,
158
+ isRefreshError: false,
159
+ isSuccess: false,
160
+ isError: false,
161
+ retry: this.retry,
162
+ refresh: this.refresh,
163
+ };
164
+ }
165
+ _idleState = {
166
+ status: "idle",
167
+ data: null,
168
+ error: null,
169
+ args: null,
170
+ isLoading: false,
171
+ isInitialLoading: false,
172
+ isRefreshing: false,
173
+ isRefreshError: false,
174
+ isSuccess: false,
175
+ isError: false,
176
+ retry: this.retry,
177
+ refresh: this.refresh,
178
+ };
179
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./Resource";
2
+ export * from "./ResourceAgent";
@@ -0,0 +1,2 @@
1
+ export * from "./Resource";
2
+ export * from "./ResourceAgent";
@@ -0,0 +1,22 @@
1
+ import type { TApiSnapshot, TResourceSnapshot } from "../../../query/types";
2
+ import type { Resource } from "../resource/Resource";
3
+ export interface TSnapshoterOptions {
4
+ initialSnapshot: TApiSnapshot | null;
5
+ snapshotValidTime: number | false;
6
+ keyPrefix: string | null;
7
+ }
8
+ export declare class Snapshoter {
9
+ private readonly _initialSnapshot;
10
+ private readonly _snapshotValidTime;
11
+ private readonly _keyPrefix;
12
+ constructor(options: TSnapshoterOptions);
13
+ /**
14
+ * Build hydration entries for a resource from the initial snapshot.
15
+ * Returns `undefined` when no matching snapshot data exists.
16
+ */
17
+ hydrateResource(snapshotKey: string | undefined, resourceSnapshotValidTime?: number | false): TResourceSnapshot | undefined;
18
+ /**
19
+ * Collects a serializable snapshot of all registered resources.
20
+ */
21
+ getSnapshot(resources: Resource<any, any>[]): TApiSnapshot;
22
+ }
@@ -0,0 +1,78 @@
1
+ import { CURRENT_SNAPSHOT_VERSION } from "../../constants";
2
+ export class Snapshoter {
3
+ _initialSnapshot;
4
+ _snapshotValidTime;
5
+ _keyPrefix;
6
+ constructor(options) {
7
+ this._initialSnapshot = options.initialSnapshot;
8
+ this._snapshotValidTime = options.snapshotValidTime;
9
+ this._keyPrefix = options.keyPrefix;
10
+ }
11
+ /**
12
+ * Build hydration entries for a resource from the initial snapshot.
13
+ * Returns `undefined` when no matching snapshot data exists.
14
+ */
15
+ hydrateResource(snapshotKey, resourceSnapshotValidTime) {
16
+ if (!this._initialSnapshot || !snapshotKey || !this._initialSnapshot.resources[snapshotKey]) {
17
+ return undefined;
18
+ }
19
+ const resSnapshot = this._initialSnapshot.resources[snapshotKey];
20
+ const entries = {};
21
+ const now = Date.now();
22
+ const effectiveSnapshotValidTime = resourceSnapshotValidTime !== undefined ? resourceSnapshotValidTime : this._snapshotValidTime;
23
+ for (const [entryKey, snapEntry] of Object.entries(resSnapshot.entries)) {
24
+ if (snapEntry.status !== "success")
25
+ continue;
26
+ let isStale = false;
27
+ if (effectiveSnapshotValidTime !== false && typeof snapEntry.updatedAt === "number") {
28
+ isStale = snapEntry.updatedAt + effectiveSnapshotValidTime < now;
29
+ }
30
+ entries[entryKey] = {
31
+ status: snapEntry.status,
32
+ args: snapEntry.args,
33
+ data: snapEntry.data,
34
+ updatedAt: snapEntry.updatedAt,
35
+ isStale,
36
+ };
37
+ }
38
+ return Object.keys(entries).length > 0 ? { entries } : undefined;
39
+ }
40
+ /**
41
+ * Collects a serializable snapshot of all registered resources.
42
+ */
43
+ getSnapshot(resources) {
44
+ const resourcesMap = {};
45
+ for (const resource of resources) {
46
+ const resourceKey = resource._key;
47
+ if (!resourceKey)
48
+ continue;
49
+ const entries = {};
50
+ let hasEntries = false;
51
+ for (const entry of resource.getEntries()) {
52
+ const { state } = entry.peek();
53
+ if (state.status !== "success" && state.status !== "refresh-error")
54
+ continue;
55
+ entries[entry.keyedArgs.key] = {
56
+ status: state.status,
57
+ args: state.args,
58
+ data: state.data,
59
+ updatedAt: state.updatedAt,
60
+ };
61
+ hasEntries = true;
62
+ }
63
+ let snapshotResourceKey = resourceKey;
64
+ if (this._keyPrefix != null && snapshotResourceKey.startsWith(`${this._keyPrefix}/`)) {
65
+ snapshotResourceKey = snapshotResourceKey.slice(this._keyPrefix.length + 1);
66
+ }
67
+ if (hasEntries) {
68
+ resourcesMap[snapshotResourceKey] = { entries };
69
+ }
70
+ }
71
+ return {
72
+ version: CURRENT_SNAPSHOT_VERSION,
73
+ keyPrefix: this._keyPrefix,
74
+ timestamp: Date.now(),
75
+ resources: resourcesMap,
76
+ };
77
+ }
78
+ }
@@ -0,0 +1,2 @@
1
+ export { Snapshoter } from "./Snapshoter";
2
+ export type { TSnapshoterOptions } from "./Snapshoter";
@@ -0,0 +1 @@
1
+ export { Snapshoter } from "./Snapshoter";