@fozy-labs/rx-toolkit 0.5.4 → 0.6.2
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.
- package/LICENSE +1 -1
- package/README.md +22 -13
- package/dist/common/devtools/reduxDevtools.js +17 -2
- package/dist/common/react/index.d.ts +1 -0
- package/dist/common/react/index.js +1 -0
- package/dist/common/react/useIsomorphicLayoutEffect.d.ts +17 -0
- package/dist/common/react/useIsomorphicLayoutEffect.js +17 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/query/api/createApi.d.ts +4 -0
- package/dist/query/api/createApi.js +9 -0
- package/dist/query/api/index.d.ts +1 -0
- package/dist/query/api/index.js +1 -0
- package/dist/query/constants.d.ts +12 -0
- package/dist/query/constants.js +15 -0
- package/dist/query/core/api/Api.d.ts +20 -0
- package/dist/query/core/api/Api.js +129 -0
- package/dist/query/core/api/constants.d.ts +2 -0
- package/dist/query/core/api/constants.js +3 -0
- package/dist/query/core/api/index.d.ts +3 -0
- package/dist/query/core/api/index.js +3 -0
- package/dist/query/core/api/mergeHooks.d.ts +2 -0
- package/dist/query/core/api/mergeHooks.js +26 -0
- package/dist/query/core/api/normalizeLinks.d.ts +2 -0
- package/dist/query/core/api/normalizeLinks.js +11 -0
- package/dist/query/core/cache/CacheEntry.d.ts +21 -0
- package/dist/query/core/cache/CacheEntry.js +54 -0
- package/dist/query/core/cache/CacheMap.d.ts +19 -0
- package/dist/query/core/cache/CacheMap.js +32 -0
- package/dist/query/core/cache/QueryCacheEntry.d.ts +21 -0
- package/dist/query/core/cache/QueryCacheEntry.js +136 -0
- package/dist/query/core/cache/index.d.ts +3 -0
- package/dist/query/core/cache/index.js +3 -0
- package/dist/query/core/command/Command.d.ts +67 -0
- package/dist/query/core/command/Command.js +253 -0
- package/dist/query/core/command/CommandAgent.d.ts +17 -0
- package/dist/query/core/command/CommandAgent.js +67 -0
- package/dist/query/core/command/LinkManager.d.ts +24 -0
- package/dist/query/core/command/LinkManager.js +71 -0
- package/dist/query/core/command/index.d.ts +2 -0
- package/dist/query/core/command/index.js +2 -0
- package/dist/query/core/errors/CacheEntryRemovedError.d.ts +7 -0
- package/dist/query/core/errors/CacheEntryRemovedError.js +9 -0
- package/dist/query/core/errors/MachineStateError.d.ts +8 -0
- package/dist/query/core/errors/MachineStateError.js +10 -0
- package/dist/query/core/errors/MachineTransitionError.d.ts +7 -0
- package/dist/query/core/errors/MachineTransitionError.js +9 -0
- package/dist/query/core/errors/index.d.ts +3 -0
- package/dist/query/core/errors/index.js +3 -0
- package/dist/query/core/index.d.ts +8 -0
- package/dist/query/core/index.js +8 -0
- package/dist/query/core/machine/Machine.d.ts +21 -0
- package/dist/query/core/machine/Machine.js +42 -0
- package/dist/query/core/machine/MachineBase.d.ts +31 -0
- package/dist/query/core/machine/MachineBase.js +176 -0
- package/dist/query/core/machine/MachineError.d.ts +10 -0
- package/dist/query/core/machine/MachineError.js +19 -0
- package/dist/query/core/machine/MachinePending.d.ts +13 -0
- package/dist/query/core/machine/MachinePending.js +32 -0
- package/dist/query/core/machine/MachineRefreshError.d.ts +12 -0
- package/dist/query/core/machine/MachineRefreshError.js +23 -0
- package/dist/query/core/machine/MachineRefreshing.d.ts +15 -0
- package/dist/query/core/machine/MachineRefreshing.js +43 -0
- package/dist/query/core/machine/MachineSuccess.d.ts +12 -0
- package/dist/query/core/machine/MachineSuccess.js +23 -0
- package/dist/query/core/machine/MachineWithData.d.ts +25 -0
- package/dist/query/core/machine/MachineWithData.js +73 -0
- package/dist/query/core/machine/index.d.ts +9 -0
- package/dist/query/core/machine/index.js +9 -0
- package/dist/query/core/machine/machine-helpers.d.ts +9 -0
- package/dist/query/core/machine/machine-helpers.js +80 -0
- package/dist/query/core/patcher/Patcher.d.ts +30 -0
- package/dist/query/core/patcher/Patcher.js +122 -0
- package/dist/query/core/patcher/index.d.ts +1 -0
- package/dist/query/core/patcher/index.js +1 -0
- package/dist/query/core/resource/Resource.d.ts +105 -0
- package/dist/query/core/resource/Resource.js +340 -0
- package/dist/query/core/resource/ResourceAgent.d.ts +37 -0
- package/dist/query/core/resource/ResourceAgent.js +179 -0
- package/dist/query/core/resource/index.d.ts +2 -0
- package/dist/query/core/resource/index.js +2 -0
- package/dist/query/core/snapshoter/Snapshoter.d.ts +22 -0
- package/dist/query/core/snapshoter/Snapshoter.js +78 -0
- package/dist/query/core/snapshoter/index.d.ts +2 -0
- package/dist/query/core/snapshoter/index.js +1 -0
- package/dist/query/core/syncer/Syncer.d.ts +32 -0
- package/dist/query/core/syncer/Syncer.js +85 -0
- package/dist/query/core/syncer/index.d.ts +2 -0
- package/dist/query/core/syncer/index.js +1 -0
- package/dist/query/index.d.ts +5 -10
- package/dist/query/index.js +5 -13
- package/dist/query/lib/broadcastSyncDriver.d.ts +5 -0
- package/dist/query/lib/broadcastSyncDriver.js +46 -0
- package/dist/query/lib/index.d.ts +3 -0
- package/dist/query/lib/index.js +3 -0
- package/dist/{query-v2 → query}/lib/stableStringify.js +5 -3
- package/dist/query/lib/toKeyed.d.ts +11 -0
- package/dist/query/lib/toKeyed.js +18 -0
- package/dist/query/react/ReactHooksPlugin.d.ts +31 -0
- package/dist/query/react/ReactHooksPlugin.js +21 -0
- package/dist/query/react/index.d.ts +3 -0
- package/dist/query/react/index.js +3 -0
- package/dist/query/react/useCommand.d.ts +2 -0
- package/dist/query/react/useCommand.js +14 -0
- package/dist/query/react/useResource.d.ts +2 -0
- package/dist/query/react/useResource.js +16 -0
- package/dist/query/types/api.d.ts +39 -0
- package/dist/query/types/cache.d.ts +51 -0
- package/dist/query/types/command.d.ts +52 -0
- package/dist/query/types/common.d.ts +65 -0
- package/dist/query/types/index.d.ts +8 -4
- package/dist/query/types/index.js +8 -5
- package/dist/query/types/plugin-hkt.d.ts +64 -0
- package/dist/query/types/resource.d.ts +49 -0
- package/dist/query/types/snapshot.d.ts +27 -0
- package/dist/query/types/snapshot.js +2 -0
- package/dist/query/types/state.d.ts +24 -0
- package/dist/signals/base/ComputeCache.js +1 -1
- package/dist/signals/base/Devtools.js +2 -6
- package/dist/signals/signals/Computed.d.ts +1 -0
- package/dist/signals/signals/Computed.js +5 -1
- package/dist/signals/signals/Effect.d.ts +0 -4
- package/dist/signals/signals/Effect.js +0 -6
- package/dist/signals/signals/LocalState.d.ts +1 -10
- package/dist/signals/signals/LocalState.js +0 -12
- package/dist/signals/signals/Signal.d.ts +2 -8
- package/dist/signals/signals/Signal.js +1 -10
- package/dist/signals/signals/State.d.ts +2 -1
- package/dist/signals/signals/State.js +9 -0
- package/dist/signals/types/SignalOptions.d.ts +0 -2
- package/dist/signals/types/normalizeSignalOptions.js +0 -3
- package/dist/signals/types/signals.types.d.ts +3 -1
- package/docs/CHANGELOG.md +54 -2
- package/docs/contributing/release/README.md +2 -8
- package/docs/migrations/0.6.0.md +224 -0
- package/docs/query/README.md +52 -562
- package/docs/query/api/README.md +59 -0
- package/docs/query/api/_CacheEntry.md +39 -0
- package/docs/query/api/_CacheMap.md +30 -0
- package/docs/query/api/_QueryCacheEntry.md +81 -0
- package/docs/query/api/command-agent.md +60 -0
- package/docs/query/api/command.md +76 -0
- package/docs/query/api/resource-agent.md +68 -0
- package/docs/query/api/resource.md +77 -0
- package/docs/query/concepts/agent.md +70 -0
- package/docs/query/concepts/architecture.md +139 -0
- package/docs/query/concepts/cache.md +81 -0
- package/docs/query/concepts/dataflows.md +473 -0
- package/docs/query/concepts/keyed.md +42 -0
- package/docs/query/concepts/machine.md +106 -0
- package/docs/query/concepts/patching.md +85 -0
- package/docs/query/usage/broadcast.md +188 -0
- package/docs/query/usage/command.md +203 -0
- package/docs/query/usage/lifecycle.md +114 -0
- package/docs/query/usage/links.md +125 -0
- package/docs/query/usage/plugins.md +96 -0
- package/docs/query/usage/resource.md +206 -0
- package/docs/query/usage/snapshot.md +80 -0
- package/docs/usage/react/README.md +45 -91
- package/package.json +5 -7
- package/dist/query/SKIP_TOKEN.d.ts +0 -1
- package/dist/query/SKIP_TOKEN.js +0 -1
- package/dist/query/api/createCommand.d.ts +0 -21
- package/dist/query/api/createCommand.js +0 -20
- package/dist/query/api/createOperation.d.ts +0 -5
- package/dist/query/api/createOperation.js +0 -6
- package/dist/query/api/createResource.d.ts +0 -3
- package/dist/query/api/createResource.js +0 -2
- package/dist/query/api/createResourceDuplicator.d.ts +0 -4
- package/dist/query/api/createResourceDuplicator.js +0 -2
- package/dist/query/api/resetAllQueriesCache.d.ts +0 -1
- package/dist/query/api/resetAllQueriesCache.js +0 -4
- package/dist/query/core/Command/Command.d.ts +0 -35
- package/dist/query/core/Command/Command.js +0 -210
- package/dist/query/core/Command/CommandAgent.d.ts +0 -19
- package/dist/query/core/Command/CommandAgent.js +0 -54
- package/dist/query/core/Command/index.d.ts +0 -2
- package/dist/query/core/Command/index.js +0 -2
- package/dist/query/core/Operation/Operation.d.ts +0 -8
- package/dist/query/core/Operation/Operation.js +0 -4
- package/dist/query/core/Operation/OperationAgent.d.ts +0 -4
- package/dist/query/core/Operation/OperationAgent.js +0 -4
- package/dist/query/core/QueriesCache.d.ts +0 -9
- package/dist/query/core/QueriesCache.js +0 -28
- package/dist/query/core/QueriesLifetimeHooks.d.ts +0 -22
- package/dist/query/core/QueriesLifetimeHooks.js +0 -86
- package/dist/query/core/ResetAllQueriesSignal.d.ts +0 -6
- package/dist/query/core/ResetAllQueriesSignal.js +0 -11
- package/dist/query/core/Resource/Resource.d.ts +0 -51
- package/dist/query/core/Resource/Resource.js +0 -232
- package/dist/query/core/Resource/ResourceAgent.d.ts +0 -35
- package/dist/query/core/Resource/ResourceAgent.js +0 -110
- package/dist/query/core/Resource/ResourceDuplicator.d.ts +0 -73
- package/dist/query/core/Resource/ResourceDuplicator.js +0 -227
- package/dist/query/core/Resource/ResourceDuplicatorAgent.d.ts +0 -35
- package/dist/query/core/Resource/ResourceDuplicatorAgent.js +0 -110
- package/dist/query/core/Resource/ResourceRef.d.ts +0 -16
- package/dist/query/core/Resource/ResourceRef.js +0 -136
- package/dist/query/lib/IndirectMap.d.ts +0 -19
- package/dist/query/lib/IndirectMap.js +0 -88
- package/dist/query/lib/ReactiveCache.d.ts +0 -62
- package/dist/query/lib/ReactiveCache.js +0 -80
- package/dist/query/react/useCommandAgent.d.ts +0 -24
- package/dist/query/react/useCommandAgent.js +0 -39
- package/dist/query/react/useOperationAgent.d.ts +0 -6
- package/dist/query/react/useOperationAgent.js +0 -6
- package/dist/query/react/useResourceAgent.d.ts +0 -6
- package/dist/query/react/useResourceAgent.js +0 -31
- package/dist/query/react/useResourceRef.d.ts +0 -5
- package/dist/query/react/useResourceRef.js +0 -13
- package/dist/query/types/Command.types.d.ts +0 -154
- package/dist/query/types/Command.types.js +0 -1
- package/dist/query/types/Operation.types.d.ts +0 -13
- package/dist/query/types/Operation.types.js +0 -1
- package/dist/query/types/Resource.types.d.ts +0 -129
- package/dist/query/types/Resource.types.js +0 -1
- package/dist/query/types/shared.types.d.ts +0 -26
- package/dist/query/types/shared.types.js +0 -1
- package/dist/query-v2/api/createApi.d.ts +0 -10
- package/dist/query-v2/api/createApi.js +0 -83
- package/dist/query-v2/core/common/CacheEntry.d.ts +0 -29
- package/dist/query-v2/core/common/CacheEntry.js +0 -71
- package/dist/query-v2/core/common/CacheMap.d.ts +0 -38
- package/dist/query-v2/core/common/CacheMap.js +0 -127
- package/dist/query-v2/core/common/LifecycleHooks.d.ts +0 -22
- package/dist/query-v2/core/common/LifecycleHooks.js +0 -104
- package/dist/query-v2/core/common/index.d.ts +0 -3
- package/dist/query-v2/core/common/index.js +0 -3
- package/dist/query-v2/core/index.d.ts +0 -3
- package/dist/query-v2/core/index.js +0 -3
- package/dist/query-v2/core/machines/Machine.d.ts +0 -14
- package/dist/query-v2/core/machines/Machine.js +0 -33
- package/dist/query-v2/core/machines/MachineError.d.ts +0 -11
- package/dist/query-v2/core/machines/MachineError.js +0 -26
- package/dist/query-v2/core/machines/MachineIdle.d.ts +0 -8
- package/dist/query-v2/core/machines/MachineIdle.js +0 -19
- package/dist/query-v2/core/machines/MachinePending.d.ts +0 -12
- package/dist/query-v2/core/machines/MachinePending.js +0 -29
- package/dist/query-v2/core/machines/MachineRefreshing.d.ts +0 -14
- package/dist/query-v2/core/machines/MachineRefreshing.js +0 -46
- package/dist/query-v2/core/machines/MachineSuccess.d.ts +0 -16
- package/dist/query-v2/core/machines/MachineSuccess.js +0 -42
- package/dist/query-v2/core/machines/MachineWithData.d.ts +0 -18
- package/dist/query-v2/core/machines/MachineWithData.js +0 -40
- package/dist/query-v2/core/machines/Patcher.d.ts +0 -20
- package/dist/query-v2/core/machines/Patcher.js +0 -104
- package/dist/query-v2/core/machines/index.d.ts +0 -8
- package/dist/query-v2/core/machines/index.js +0 -8
- package/dist/query-v2/core/resource/ResourceV2.d.ts +0 -120
- package/dist/query-v2/core/resource/ResourceV2.js +0 -464
- package/dist/query-v2/core/resource/ResourceV2Agent.d.ts +0 -26
- package/dist/query-v2/core/resource/ResourceV2Agent.js +0 -132
- package/dist/query-v2/core/resource/index.d.ts +0 -2
- package/dist/query-v2/core/resource/index.js +0 -2
- package/dist/query-v2/index.d.ts +0 -11
- package/dist/query-v2/index.js +0 -17
- package/dist/query-v2/lib/NO_VALUE.d.ts +0 -2
- package/dist/query-v2/lib/NO_VALUE.js +0 -1
- package/dist/query-v2/lib/SKIP_TOKEN.d.ts +0 -2
- package/dist/query-v2/lib/SKIP_TOKEN.js +0 -1
- package/dist/query-v2/lib/index.d.ts +0 -4
- package/dist/query-v2/lib/index.js +0 -3
- package/dist/query-v2/plugins/ReactHooksPlugin.d.ts +0 -25
- package/dist/query-v2/plugins/ReactHooksPlugin.js +0 -19
- package/dist/query-v2/plugins/types.d.ts +0 -1
- package/dist/query-v2/react/__tests__/helpers.d.ts +0 -12
- package/dist/query-v2/react/__tests__/helpers.js +0 -33
- package/dist/query-v2/react/index.d.ts +0 -2
- package/dist/query-v2/react/index.js +0 -2
- package/dist/query-v2/react/useResourceV2Agent.d.ts +0 -12
- package/dist/query-v2/react/useResourceV2Agent.js +0 -36
- package/dist/query-v2/react/useResourceV2Ref.d.ts +0 -12
- package/dist/query-v2/react/useResourceV2Ref.js +0 -57
- package/dist/query-v2/snapshot/Snapshot.d.ts +0 -13
- package/dist/query-v2/snapshot/Snapshot.js +0 -76
- package/dist/query-v2/types/agent.types.d.ts +0 -54
- package/dist/query-v2/types/api.types.d.ts +0 -22
- package/dist/query-v2/types/cache.types.d.ts +0 -37
- package/dist/query-v2/types/index.d.ts +0 -9
- package/dist/query-v2/types/index.js +0 -9
- package/dist/query-v2/types/lifecycle.types.d.ts +0 -25
- package/dist/query-v2/types/machine.types.d.ts +0 -67
- package/dist/query-v2/types/plugin.types.d.ts +0 -38
- package/dist/query-v2/types/resource.types.d.ts +0 -35
- package/dist/query-v2/types/resource.types.js +0 -1
- package/dist/query-v2/types/shared.types.d.ts +0 -20
- package/dist/query-v2/types/shared.types.js +0 -1
- package/dist/query-v2/types/snapshot.types.d.ts +0 -21
- package/dist/query-v2/types/snapshot.types.js +0 -1
- package/docs/contributing/query-v2/README.md +0 -379
- package/docs/migrations/query-v2.md +0 -171
- package/docs/query-v2/README.md +0 -280
- package/docs/query-v2/api-reference.md +0 -235
- package/docs/query-v2/optimistic-updates.md +0 -148
- package/docs/query-v2/ssr.md +0 -130
- /package/dist/{query-v2 → query}/lib/stableStringify.d.ts +0 -0
- /package/dist/{query-v2/plugins/types.js → query/types/api.js} +0 -0
- /package/dist/{query-v2/types/agent.types.js → query/types/cache.js} +0 -0
- /package/dist/{query-v2/types/api.types.js → query/types/command.js} +0 -0
- /package/dist/{query-v2/types/cache.types.js → query/types/common.js} +0 -0
- /package/dist/{query-v2/types/lifecycle.types.js → query/types/plugin-hkt.js} +0 -0
- /package/dist/{query-v2/types/machine.types.js → query/types/resource.js} +0 -0
- /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,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 @@
|
|
|
1
|
+
export { Snapshoter } from "./Snapshoter";
|