@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.
- package/LICENSE +21 -21
- package/README.md +143 -137
- package/dist/common/devtools/combineDevtools.js +3 -3
- package/dist/common/devtools/index.d.ts +3 -3
- package/dist/common/devtools/index.js +3 -3
- package/dist/common/devtools/reduxDevtools.d.ts +1 -1
- package/dist/common/devtools/reduxDevtools.js +17 -17
- package/dist/common/devtools/types.d.ts +0 -6
- package/dist/common/options/DefaultOptions.d.ts +1 -1
- package/dist/common/options/SharedOptions.d.ts +3 -2
- package/dist/common/options/SharedOptions.js +6 -0
- package/dist/common/options/index.d.ts +1 -1
- package/dist/common/options/index.js +1 -1
- package/dist/common/react/index.d.ts +2 -2
- package/dist/common/react/index.js +2 -2
- package/dist/common/react/useConstant.js +1 -1
- package/dist/common/utils/deepEqual.js +1 -1
- package/dist/common/utils/index.d.ts +3 -3
- package/dist/common/utils/index.js +3 -3
- package/dist/common/utils/shallowEqual.js +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.js +8 -7
- package/dist/query/SKIP_TOKEN.js +1 -1
- package/dist/query/api/createCommand.d.ts +21 -0
- package/dist/query/api/createCommand.js +20 -0
- package/dist/query/api/createOperation.d.ts +5 -3
- package/dist/query/api/createOperation.js +6 -2
- package/dist/query/api/createResource.d.ts +2 -2
- package/dist/query/api/createResourceDuplicator.d.ts +2 -2
- package/dist/query/core/Command/Command.d.ts +35 -0
- package/dist/query/core/{Opertation/Operation.js → Command/Command.js} +13 -14
- package/dist/query/core/Command/CommandAgent.d.ts +19 -0
- package/dist/query/core/{Opertation/OperationAgent.js → Command/CommandAgent.js} +13 -13
- package/dist/query/core/Command/index.d.ts +2 -0
- package/dist/query/core/Command/index.js +2 -0
- package/dist/query/core/Operation/Operation.d.ts +8 -0
- package/dist/query/core/Operation/Operation.js +4 -0
- package/dist/query/core/Operation/OperationAgent.d.ts +4 -0
- package/dist/query/core/Operation/OperationAgent.js +4 -0
- package/dist/query/core/QueriesCache.d.ts +2 -2
- package/dist/query/core/QueriesCache.js +1 -1
- package/dist/query/core/QueriesLifetimeHooks.d.ts +1 -1
- package/dist/query/core/QueriesLifetimeHooks.js +7 -7
- package/dist/query/core/Resource/Resource.d.ts +16 -16
- package/dist/query/core/Resource/Resource.js +7 -7
- package/dist/query/core/Resource/ResourceAgent.d.ts +2 -2
- package/dist/query/core/Resource/ResourceAgent.js +3 -3
- package/dist/query/core/Resource/ResourceDuplicator.d.ts +17 -17
- package/dist/query/core/Resource/ResourceDuplicator.js +18 -20
- package/dist/query/core/Resource/ResourceDuplicatorAgent.d.ts +6 -6
- package/dist/query/core/Resource/ResourceDuplicatorAgent.js +3 -3
- package/dist/query/core/Resource/ResourceRef.d.ts +2 -2
- package/dist/query/core/Resource/ResourceRef.js +12 -12
- package/dist/query/index.d.ts +11 -8
- package/dist/query/index.js +14 -8
- package/dist/query/lib/IndirectMap.js +4 -4
- package/dist/query/lib/ReactiveCache.d.ts +1 -1
- package/dist/query/react/useCommandAgent.d.ts +24 -0
- package/dist/query/react/useCommandAgent.js +39 -0
- package/dist/query/react/useOperationAgent.d.ts +6 -8
- package/dist/query/react/useOperationAgent.js +6 -23
- package/dist/query/react/useResourceAgent.d.ts +4 -4
- package/dist/query/react/useResourceAgent.js +1 -1
- package/dist/query/react/useResourceRef.d.ts +3 -3
- package/dist/query/react/useResourceRef.js +7 -2
- package/dist/query/types/Command.types.d.ts +154 -0
- package/dist/query/types/Command.types.js +1 -0
- package/dist/query/types/Operation.types.d.ts +13 -154
- package/dist/query/types/Resource.types.d.ts +7 -5
- package/dist/query/types/index.d.ts +4 -3
- package/dist/query/types/index.js +5 -3
- package/dist/query-v2/api/createApi.d.ts +10 -0
- package/dist/query-v2/api/createApi.js +83 -0
- package/dist/query-v2/core/common/CacheEntry.d.ts +29 -0
- package/dist/query-v2/core/common/CacheEntry.js +71 -0
- package/dist/query-v2/core/common/CacheMap.d.ts +38 -0
- package/dist/query-v2/core/common/CacheMap.js +127 -0
- package/dist/query-v2/core/common/LifecycleHooks.d.ts +22 -0
- package/dist/query-v2/core/common/LifecycleHooks.js +104 -0
- package/dist/query-v2/core/common/index.d.ts +3 -0
- package/dist/query-v2/core/common/index.js +3 -0
- package/dist/query-v2/core/index.d.ts +3 -0
- package/dist/query-v2/core/index.js +3 -0
- package/dist/query-v2/core/machines/Machine.d.ts +14 -0
- package/dist/query-v2/core/machines/Machine.js +33 -0
- package/dist/query-v2/core/machines/MachineError.d.ts +11 -0
- package/dist/query-v2/core/machines/MachineError.js +26 -0
- package/dist/query-v2/core/machines/MachineIdle.d.ts +8 -0
- package/dist/query-v2/core/machines/MachineIdle.js +19 -0
- package/dist/query-v2/core/machines/MachinePending.d.ts +12 -0
- package/dist/query-v2/core/machines/MachinePending.js +29 -0
- package/dist/query-v2/core/machines/MachineRefreshing.d.ts +14 -0
- package/dist/query-v2/core/machines/MachineRefreshing.js +46 -0
- package/dist/query-v2/core/machines/MachineSuccess.d.ts +16 -0
- package/dist/query-v2/core/machines/MachineSuccess.js +42 -0
- package/dist/query-v2/core/machines/MachineWithData.d.ts +18 -0
- package/dist/query-v2/core/machines/MachineWithData.js +40 -0
- package/dist/query-v2/core/machines/Patcher.d.ts +20 -0
- package/dist/query-v2/core/machines/Patcher.js +104 -0
- package/dist/query-v2/core/machines/index.d.ts +8 -0
- package/dist/query-v2/core/machines/index.js +8 -0
- package/dist/query-v2/core/resource/ResourceV2.d.ts +120 -0
- package/dist/query-v2/core/resource/ResourceV2.js +464 -0
- package/dist/query-v2/core/resource/ResourceV2Agent.d.ts +26 -0
- package/dist/query-v2/core/resource/ResourceV2Agent.js +132 -0
- package/dist/query-v2/core/resource/index.d.ts +2 -0
- package/dist/query-v2/core/resource/index.js +2 -0
- package/dist/query-v2/index.d.ts +11 -0
- package/dist/query-v2/index.js +17 -0
- package/dist/query-v2/lib/NO_VALUE.d.ts +2 -0
- package/dist/query-v2/lib/NO_VALUE.js +1 -0
- package/dist/query-v2/lib/SKIP_TOKEN.d.ts +2 -0
- package/dist/query-v2/lib/SKIP_TOKEN.js +1 -0
- package/dist/query-v2/lib/index.d.ts +4 -0
- package/dist/query-v2/lib/index.js +3 -0
- package/dist/query-v2/lib/stableStringify.d.ts +8 -0
- package/dist/query-v2/lib/stableStringify.js +23 -0
- package/dist/query-v2/plugins/ReactHooksPlugin.d.ts +25 -0
- package/dist/query-v2/plugins/ReactHooksPlugin.js +19 -0
- package/dist/query-v2/plugins/types.d.ts +1 -0
- package/dist/query-v2/plugins/types.js +1 -0
- package/dist/query-v2/react/__tests__/helpers.d.ts +12 -0
- package/dist/query-v2/react/__tests__/helpers.js +33 -0
- package/dist/query-v2/react/index.d.ts +2 -0
- package/dist/query-v2/react/index.js +2 -0
- package/dist/query-v2/react/useResourceV2Agent.d.ts +12 -0
- package/dist/query-v2/react/useResourceV2Agent.js +36 -0
- package/dist/query-v2/react/useResourceV2Ref.d.ts +12 -0
- package/dist/query-v2/react/useResourceV2Ref.js +57 -0
- package/dist/query-v2/snapshot/Snapshot.d.ts +13 -0
- package/dist/query-v2/snapshot/Snapshot.js +76 -0
- package/dist/query-v2/types/agent.types.d.ts +54 -0
- package/dist/query-v2/types/agent.types.js +1 -0
- package/dist/query-v2/types/api.types.d.ts +22 -0
- package/dist/query-v2/types/api.types.js +1 -0
- package/dist/query-v2/types/cache.types.d.ts +37 -0
- package/dist/query-v2/types/cache.types.js +1 -0
- package/dist/query-v2/types/index.d.ts +9 -0
- package/dist/query-v2/types/index.js +9 -0
- package/dist/query-v2/types/lifecycle.types.d.ts +25 -0
- package/dist/query-v2/types/lifecycle.types.js +1 -0
- package/dist/query-v2/types/machine.types.d.ts +67 -0
- package/dist/query-v2/types/machine.types.js +1 -0
- package/dist/query-v2/types/plugin.types.d.ts +38 -0
- package/dist/query-v2/types/plugin.types.js +1 -0
- package/dist/query-v2/types/resource.types.d.ts +35 -0
- package/dist/query-v2/types/resource.types.js +1 -0
- package/dist/query-v2/types/shared.types.d.ts +20 -0
- package/dist/query-v2/types/shared.types.js +1 -0
- package/dist/query-v2/types/snapshot.types.d.ts +21 -0
- package/dist/query-v2/types/snapshot.types.js +1 -0
- package/dist/signals/base/Batcher.js +9 -5
- package/dist/signals/base/ComputeCache.js +3 -3
- package/dist/signals/base/DependencyTracker.js +1 -1
- package/dist/signals/base/Devtools.d.ts +3 -2
- package/dist/signals/base/Devtools.js +54 -27
- package/dist/signals/base/Indexer.js +1 -1
- package/dist/signals/base/ReadonlySignal.js +1 -1
- package/dist/signals/base/SyncObservable.d.ts +1 -2
- package/dist/signals/base/SyncObservable.js +2 -5
- package/dist/signals/base/index.d.ts +6 -6
- package/dist/signals/base/index.js +6 -6
- package/dist/signals/index.d.ts +5 -4
- package/dist/signals/index.js +5 -4
- package/dist/signals/operators/index.d.ts +1 -1
- package/dist/signals/operators/index.js +1 -1
- package/dist/signals/operators/signalize.d.ts +1 -1
- package/dist/signals/react/index.d.ts +1 -1
- package/dist/signals/react/index.js +1 -1
- package/dist/signals/signals/Computed.d.ts +3 -4
- package/dist/signals/signals/Computed.js +18 -10
- package/dist/signals/signals/Effect.js +2 -1
- package/dist/signals/signals/LocalState.d.ts +44 -0
- package/dist/signals/signals/{LocalSignal.js → LocalState.js} +62 -28
- package/dist/signals/signals/Signal.d.ts +8 -7
- package/dist/signals/signals/Signal.js +4 -1
- package/dist/signals/signals/State.d.ts +4 -5
- package/dist/signals/signals/State.js +23 -9
- package/dist/signals/signals/index.d.ts +5 -5
- package/dist/signals/signals/index.js +5 -6
- package/dist/signals/types/SignalOptions.d.ts +16 -0
- package/dist/signals/types/SignalOptions.js +1 -0
- package/dist/signals/types/index.d.ts +3 -1
- package/dist/signals/types/index.js +3 -1
- package/dist/signals/types/normalizeSignalOptions.d.ts +2 -0
- package/dist/signals/types/normalizeSignalOptions.js +10 -0
- package/dist/signals/types/signals.types.d.ts +6 -2
- package/docs/CHANGELOG.md +111 -32
- package/docs/CONTRIBUTING.md +230 -0
- package/docs/contributing/ai-assisted-development.md +47 -0
- package/docs/contributing/query-v2/README.md +379 -0
- package/docs/{release → contributing/release}/README.md +59 -59
- package/docs/devtools/README.md +228 -228
- package/docs/migrations/0.5.0.md +58 -58
- package/docs/migrations/query-v2.md +171 -0
- package/docs/options/README.md +92 -90
- package/docs/query/README.md +575 -571
- package/docs/query-v2/README.md +280 -0
- package/docs/query-v2/api-reference.md +235 -0
- package/docs/query-v2/optimistic-updates.md +148 -0
- package/docs/query-v2/ssr.md +130 -0
- package/docs/signals/README.md +300 -295
- package/docs/usage/react/README.md +309 -307
- package/package.json +86 -63
- package/dist/query/core/Opertation/Operation.d.ts +0 -35
- package/dist/query/core/Opertation/OperationAgent.d.ts +0 -19
- package/dist/signals/signals/LocalSignal.d.ts +0 -32
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { IResourceV2Agent } from "../../../query-v2/types/agent.types";
|
|
2
|
+
import type { ICacheEntry } from "../../../query-v2/types/cache.types";
|
|
3
|
+
import type { TOnCacheEntryAdded, TOnQueryStarted } from "../../../query-v2/types/lifecycle.types";
|
|
4
|
+
import type { TMachine, TPatchFn } from "../../../query-v2/types/machine.types";
|
|
5
|
+
import type { IResourceV2 } from "../../../query-v2/types/resource.types";
|
|
6
|
+
import type { TBeforeDevtoolsPushFn, TCompareArgsFn, TQueryFn, TSerializeArgsFn } from "../../../query-v2/types/shared.types";
|
|
7
|
+
import { CacheEntry } from "../common/CacheEntry";
|
|
8
|
+
import type { TMachineInstance } from "../machines/Machine";
|
|
9
|
+
export interface ResourceV2Config<TArgs, TData, TError = Error> {
|
|
10
|
+
key?: string;
|
|
11
|
+
keyPrefix?: string;
|
|
12
|
+
keyStrategy?: "serialize" | "compare";
|
|
13
|
+
queryFn: TQueryFn<TArgs, TData>;
|
|
14
|
+
onCacheEntryAdded?: TOnCacheEntryAdded<TArgs, TData>;
|
|
15
|
+
onQueryStarted?: TOnQueryStarted<TArgs, TData>;
|
|
16
|
+
serializeArgs?: TSerializeArgsFn;
|
|
17
|
+
compareArg?: TCompareArgsFn;
|
|
18
|
+
cacheLifetime?: number;
|
|
19
|
+
beforeDevtoolsPush?: TBeforeDevtoolsPushFn<TMachine<TData, TError>>;
|
|
20
|
+
maxSnapshotDataAge?: number;
|
|
21
|
+
doCacheArgs?: boolean;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Cache-backed resource manager.
|
|
25
|
+
* Orchestrates queries, caching, lifecycle hooks, GC, and optimistic patches for a single resource type.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ResourceV2<TArgs, TData, TError = Error> implements IResourceV2<TArgs, TData, TError> {
|
|
28
|
+
private readonly _cache;
|
|
29
|
+
private readonly _queryFn;
|
|
30
|
+
private readonly _lifecycleHooks;
|
|
31
|
+
private readonly _serializeArgs;
|
|
32
|
+
private readonly _compareArg;
|
|
33
|
+
private readonly _keyStrategy;
|
|
34
|
+
private readonly _cacheLifetime;
|
|
35
|
+
private readonly _beforeDevtoolsPush?;
|
|
36
|
+
private readonly _key?;
|
|
37
|
+
private readonly _keyPrefix?;
|
|
38
|
+
/** In-flight queries keyed by serialized args — for dedup + abort */
|
|
39
|
+
private readonly _inFlight;
|
|
40
|
+
/** Cache lifetime timers keyed by serialized args */
|
|
41
|
+
private readonly _gcTimers;
|
|
42
|
+
/** Refresh error listeners (used by Agent for refreshError tracking) */
|
|
43
|
+
private readonly _refreshErrorListeners;
|
|
44
|
+
constructor(config: ResourceV2Config<TArgs, TData, TError>);
|
|
45
|
+
/**
|
|
46
|
+
* Create an agent that tracks a single cache entry with reactive state (SWR).
|
|
47
|
+
*
|
|
48
|
+
* @returns A new agent bound to this resource.
|
|
49
|
+
*/
|
|
50
|
+
createAgent(): IResourceV2Agent<TArgs, TData, TError>;
|
|
51
|
+
/**
|
|
52
|
+
* Execute a query for the given args, returning the cache entry.
|
|
53
|
+
* Deduplicates concurrent calls for the same args unless `doForce` is set.
|
|
54
|
+
*
|
|
55
|
+
* @param args - Query arguments (used as cache key).
|
|
56
|
+
* @param doForce - If true, bypass dedup and re-execute the query.
|
|
57
|
+
* @returns The cache entry after query initiation.
|
|
58
|
+
*/
|
|
59
|
+
query(args: TArgs, doForce?: boolean): Promise<ICacheEntry<TData, TError>>;
|
|
60
|
+
/**
|
|
61
|
+
* Reactive query — read machine state as a signal dependency.
|
|
62
|
+
* Initiates the query if not already cached.
|
|
63
|
+
*
|
|
64
|
+
* @param args - Query arguments.
|
|
65
|
+
* @returns Current machine state (registers a reactive subscription).
|
|
66
|
+
*/
|
|
67
|
+
query$(args: TArgs, doForce?: boolean): TMachine<TData, TError>;
|
|
68
|
+
/**
|
|
69
|
+
* Get the cache entry for the given args without initiating a query (unless `doInitiate` is set).
|
|
70
|
+
*
|
|
71
|
+
* @param args - Query arguments.
|
|
72
|
+
* @returns The cache entry, or `null` if not cached.
|
|
73
|
+
*/
|
|
74
|
+
entry(args: TArgs, doInitiate?: boolean): ICacheEntry<TData, TError> | null;
|
|
75
|
+
entry$(args: TArgs, doInitiate?: boolean): TMachine<TData, TError>;
|
|
76
|
+
invalidate(args: TArgs): void;
|
|
77
|
+
compareArgs(a: TArgs, b: TArgs): boolean;
|
|
78
|
+
/** Public key getter for API registry */
|
|
79
|
+
get key(): string | undefined;
|
|
80
|
+
/** Public keyStrategy getter for snapshot validation */
|
|
81
|
+
get keyStrategy(): "serialize" | "compare";
|
|
82
|
+
/** Serialize args to string key */
|
|
83
|
+
getSerializedKey(args: TArgs): string;
|
|
84
|
+
/** Iterate cache entries — for snapshot */
|
|
85
|
+
cacheEntries(): Iterable<[TArgs | string, CacheEntry<TData, TError>]>;
|
|
86
|
+
/** Hydrate a cache entry from snapshot data */
|
|
87
|
+
hydrateEntry(args: TArgs, machine: TMachineInstance<TData, TError>): void;
|
|
88
|
+
/** Check if cache entry exists for given args */
|
|
89
|
+
hasEntry(args: TArgs): boolean;
|
|
90
|
+
/** Pre-populate cache with data */
|
|
91
|
+
populateEntry(args: TArgs, data: TData): void;
|
|
92
|
+
/** Create an optimistic patch on a cache entry */
|
|
93
|
+
createEntryPatch(args: TArgs, patchFn: TPatchFn<TData>): {
|
|
94
|
+
commit: () => void;
|
|
95
|
+
abort: () => void;
|
|
96
|
+
} | null;
|
|
97
|
+
/** Lock a cache entry — prevent GC eviction. Returns unlock function. */
|
|
98
|
+
lockEntry(args: TArgs): {
|
|
99
|
+
unlock: () => void;
|
|
100
|
+
};
|
|
101
|
+
/** Subscribe to refresh error events (used by Agent) */
|
|
102
|
+
onRefreshError(listener: (args: TArgs, error: TError) => void): () => void;
|
|
103
|
+
/** Reset the entire cache — aborts in-flight requests, clears GC timers, completes all entries. */
|
|
104
|
+
resetCache(): void;
|
|
105
|
+
/**
|
|
106
|
+
* Schedule GC for a cache entry after cacheLifetime.
|
|
107
|
+
* Called when subscriber count drops to 0.
|
|
108
|
+
*/
|
|
109
|
+
scheduleGc(args: TArgs): void;
|
|
110
|
+
/**
|
|
111
|
+
* Cancel pending GC for a cache entry.
|
|
112
|
+
* Called when a new subscriber appears.
|
|
113
|
+
*/
|
|
114
|
+
cancelGc(args: TArgs): void;
|
|
115
|
+
private _cancelGcTimer;
|
|
116
|
+
private _evictEntry;
|
|
117
|
+
private _executeQuery;
|
|
118
|
+
private _executeRefresh;
|
|
119
|
+
private _buildCacheEntryOptions;
|
|
120
|
+
}
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { shallowEqual } from "../../../common/utils/shallowEqual";
|
|
2
|
+
import { SKIP } from "../../../query-v2/lib/SKIP_TOKEN";
|
|
3
|
+
import { stableStringify } from "../../../query-v2/lib/stableStringify";
|
|
4
|
+
import { Batcher } from "../../../signals";
|
|
5
|
+
import { CacheEntry } from "../common/CacheEntry";
|
|
6
|
+
import { CacheMap } from "../common/CacheMap";
|
|
7
|
+
import { LifecycleHooks } from "../common/LifecycleHooks";
|
|
8
|
+
import { MachineIdle } from "../machines/MachineIdle";
|
|
9
|
+
import { MachineRefreshing } from "../machines/MachineRefreshing";
|
|
10
|
+
import { MachineSuccess } from "../machines/MachineSuccess";
|
|
11
|
+
import { MachineWithData } from "../machines/MachineWithData";
|
|
12
|
+
import { ResourceV2Agent } from "./ResourceV2Agent";
|
|
13
|
+
/**
|
|
14
|
+
* Cache-backed resource manager.
|
|
15
|
+
* Orchestrates queries, caching, lifecycle hooks, GC, and optimistic patches for a single resource type.
|
|
16
|
+
*/
|
|
17
|
+
export class ResourceV2 {
|
|
18
|
+
_cache;
|
|
19
|
+
_queryFn;
|
|
20
|
+
_lifecycleHooks;
|
|
21
|
+
_serializeArgs;
|
|
22
|
+
_compareArg;
|
|
23
|
+
_keyStrategy;
|
|
24
|
+
_cacheLifetime;
|
|
25
|
+
_beforeDevtoolsPush;
|
|
26
|
+
_key;
|
|
27
|
+
_keyPrefix;
|
|
28
|
+
/** In-flight queries keyed by serialized args — for dedup + abort */
|
|
29
|
+
_inFlight = new Map();
|
|
30
|
+
/** Cache lifetime timers keyed by serialized args */
|
|
31
|
+
_gcTimers = new Map();
|
|
32
|
+
/** Refresh error listeners (used by Agent for refreshError tracking) */
|
|
33
|
+
_refreshErrorListeners = new Set();
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this._queryFn = config.queryFn;
|
|
36
|
+
this._serializeArgs = config.serializeArgs ?? stableStringify;
|
|
37
|
+
this._compareArg = config.compareArg ?? shallowEqual;
|
|
38
|
+
this._keyStrategy = config.keyStrategy ?? "serialize";
|
|
39
|
+
this._cacheLifetime = config.cacheLifetime ?? 60_000;
|
|
40
|
+
this._beforeDevtoolsPush = config.beforeDevtoolsPush;
|
|
41
|
+
this._key = config.key;
|
|
42
|
+
this._keyPrefix = config.keyPrefix;
|
|
43
|
+
this._cache = CacheMap.create({
|
|
44
|
+
keyStrategy: this._keyStrategy,
|
|
45
|
+
serializeArgs: this._serializeArgs,
|
|
46
|
+
compareArg: this._compareArg,
|
|
47
|
+
doCacheArgs: config.doCacheArgs ?? false,
|
|
48
|
+
});
|
|
49
|
+
this._lifecycleHooks = new LifecycleHooks({
|
|
50
|
+
onCacheEntryAdded: config.onCacheEntryAdded,
|
|
51
|
+
onQueryStarted: config.onQueryStarted,
|
|
52
|
+
serializeArgs: this._serializeArgs,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create an agent that tracks a single cache entry with reactive state (SWR).
|
|
57
|
+
*
|
|
58
|
+
* @returns A new agent bound to this resource.
|
|
59
|
+
*/
|
|
60
|
+
createAgent() {
|
|
61
|
+
return new ResourceV2Agent(this);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Execute a query for the given args, returning the cache entry.
|
|
65
|
+
* Deduplicates concurrent calls for the same args unless `doForce` is set.
|
|
66
|
+
*
|
|
67
|
+
* @param args - Query arguments (used as cache key).
|
|
68
|
+
* @param doForce - If true, bypass dedup and re-execute the query.
|
|
69
|
+
* @returns The cache entry after query initiation.
|
|
70
|
+
*/
|
|
71
|
+
async query(args, doForce) {
|
|
72
|
+
// SKIP check
|
|
73
|
+
if (args === SKIP) {
|
|
74
|
+
throw new Error("SKIP_TOKEN is not valid for direct query()");
|
|
75
|
+
}
|
|
76
|
+
const key = this._serializeArgs(args);
|
|
77
|
+
// Cache hit — return existing entry (unless force)
|
|
78
|
+
const existing = this._cache.get(args);
|
|
79
|
+
if (existing && !doForce) {
|
|
80
|
+
const machine = existing.peek();
|
|
81
|
+
// If already success or pending or refreshing, return the entry
|
|
82
|
+
if (machine.state.status !== "idle") {
|
|
83
|
+
return existing;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Query dedup — if already in-flight for same args, return same promise
|
|
87
|
+
if (!doForce) {
|
|
88
|
+
const flight = this._inFlight.get(key);
|
|
89
|
+
if (flight) {
|
|
90
|
+
return flight.promise;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Cancel existing in-flight for same args (ADR-4 Layer 1)
|
|
94
|
+
const existingFlight = this._inFlight.get(key);
|
|
95
|
+
if (existingFlight) {
|
|
96
|
+
existingFlight.abortController.abort();
|
|
97
|
+
this._inFlight.delete(key);
|
|
98
|
+
}
|
|
99
|
+
// Cancel any pending GC timer for these args
|
|
100
|
+
this._cancelGcTimer(key);
|
|
101
|
+
const abortController = new AbortController();
|
|
102
|
+
const isNewEntry = !existing;
|
|
103
|
+
// Create or reuse entry inside Batcher for atomic updates
|
|
104
|
+
let cacheEntry;
|
|
105
|
+
// Set up entry and pending state
|
|
106
|
+
Batcher.run(() => {
|
|
107
|
+
if (isNewEntry || doForce) {
|
|
108
|
+
if (existing && doForce) {
|
|
109
|
+
cacheEntry = existing;
|
|
110
|
+
// Transition to refreshing or pending based on current state
|
|
111
|
+
const current = cacheEntry.peek();
|
|
112
|
+
if (current instanceof MachineSuccess) {
|
|
113
|
+
const refreshing = current.invalidate();
|
|
114
|
+
cacheEntry.set(refreshing);
|
|
115
|
+
}
|
|
116
|
+
else if (current instanceof MachineRefreshing) {
|
|
117
|
+
// Already refreshing — it'll be re-queried
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const idle = MachineIdle.create();
|
|
121
|
+
const pending = idle.start(args);
|
|
122
|
+
cacheEntry.set(pending);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// New entry
|
|
127
|
+
const idle = MachineIdle.create();
|
|
128
|
+
const pending = idle.start(args);
|
|
129
|
+
cacheEntry = new CacheEntry(pending, this._buildCacheEntryOptions(args));
|
|
130
|
+
this._cache.set(args, cacheEntry);
|
|
131
|
+
// Fire onCacheEntryAdded for new entries
|
|
132
|
+
this._lifecycleHooks.fireCacheEntryAdded(args, () => cacheEntry.peek());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
cacheEntry = existing;
|
|
137
|
+
const current = cacheEntry.peek();
|
|
138
|
+
const pending = current.start(args);
|
|
139
|
+
cacheEntry.set(pending);
|
|
140
|
+
}
|
|
141
|
+
// Fire onQueryStarted
|
|
142
|
+
this._lifecycleHooks.fireQueryStarted(args, () => cacheEntry);
|
|
143
|
+
});
|
|
144
|
+
// Execute queryFn
|
|
145
|
+
const promise = this._executeQuery(args, key, cacheEntry, abortController);
|
|
146
|
+
this._inFlight.set(key, { promise: promise, abortController });
|
|
147
|
+
return promise;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Reactive query — read machine state as a signal dependency.
|
|
151
|
+
* Initiates the query if not already cached.
|
|
152
|
+
*
|
|
153
|
+
* @param args - Query arguments.
|
|
154
|
+
* @returns Current machine state (registers a reactive subscription).
|
|
155
|
+
*/
|
|
156
|
+
query$(args, doForce) {
|
|
157
|
+
if (args === SKIP) {
|
|
158
|
+
return MachineIdle.create().state;
|
|
159
|
+
}
|
|
160
|
+
// Trigger query (fire-and-forget) and read entry signal reactively
|
|
161
|
+
const entryResult = this._cache.get(args);
|
|
162
|
+
if (!entryResult) {
|
|
163
|
+
// Initiate query
|
|
164
|
+
this.query(args, doForce);
|
|
165
|
+
// Return idle while it's being initiated
|
|
166
|
+
const newEntry = this._cache.get(args);
|
|
167
|
+
if (newEntry) {
|
|
168
|
+
return newEntry.machine$().state;
|
|
169
|
+
}
|
|
170
|
+
return MachineIdle.create().state;
|
|
171
|
+
}
|
|
172
|
+
if (doForce) {
|
|
173
|
+
this.query(args, true);
|
|
174
|
+
}
|
|
175
|
+
// Reactive read — registers signal dependency
|
|
176
|
+
return entryResult.machine$().state;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get the cache entry for the given args without initiating a query (unless `doInitiate` is set).
|
|
180
|
+
*
|
|
181
|
+
* @param args - Query arguments.
|
|
182
|
+
* @returns The cache entry, or `null` if not cached.
|
|
183
|
+
*/
|
|
184
|
+
entry(args, doInitiate) {
|
|
185
|
+
const existing = this._cache.get(args);
|
|
186
|
+
if (existing) {
|
|
187
|
+
return existing;
|
|
188
|
+
}
|
|
189
|
+
if (doInitiate) {
|
|
190
|
+
// Fire-and-forget query initiation
|
|
191
|
+
this.query(args);
|
|
192
|
+
return this._cache.get(args) ?? null;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
entry$(args, doInitiate) {
|
|
197
|
+
if (args === SKIP) {
|
|
198
|
+
return MachineIdle.create().state;
|
|
199
|
+
}
|
|
200
|
+
const existing = this._cache.get(args);
|
|
201
|
+
if (!existing && doInitiate) {
|
|
202
|
+
this.query(args);
|
|
203
|
+
const created = this._cache.get(args);
|
|
204
|
+
if (created) {
|
|
205
|
+
return created.machine$().state;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (existing) {
|
|
209
|
+
return existing.machine$().state;
|
|
210
|
+
}
|
|
211
|
+
return MachineIdle.create().state;
|
|
212
|
+
}
|
|
213
|
+
invalidate(args) {
|
|
214
|
+
const existing = this._cache.get(args);
|
|
215
|
+
if (!existing)
|
|
216
|
+
return;
|
|
217
|
+
const machine = existing.peek();
|
|
218
|
+
// Only invalidate from success state
|
|
219
|
+
if (!(machine instanceof MachineSuccess))
|
|
220
|
+
return;
|
|
221
|
+
const key = this._serializeArgs(args);
|
|
222
|
+
// Abort any existing in-flight for these args (ADR-4 Layer 1)
|
|
223
|
+
const flight = this._inFlight.get(key);
|
|
224
|
+
if (flight) {
|
|
225
|
+
flight.abortController.abort();
|
|
226
|
+
this._inFlight.delete(key);
|
|
227
|
+
}
|
|
228
|
+
const abortController = new AbortController();
|
|
229
|
+
Batcher.run(() => {
|
|
230
|
+
const refreshing = machine.invalidate();
|
|
231
|
+
existing.set(refreshing);
|
|
232
|
+
this._lifecycleHooks.fireQueryStarted(args, () => existing);
|
|
233
|
+
});
|
|
234
|
+
const promise = this._executeRefresh(args, key, existing, abortController);
|
|
235
|
+
this._inFlight.set(key, { promise, abortController });
|
|
236
|
+
}
|
|
237
|
+
compareArgs(a, b) {
|
|
238
|
+
if (this._keyStrategy === "compare") {
|
|
239
|
+
return this._compareArg(a, b);
|
|
240
|
+
}
|
|
241
|
+
return this._serializeArgs(a) === this._serializeArgs(b);
|
|
242
|
+
}
|
|
243
|
+
/** Public key getter for API registry */
|
|
244
|
+
get key() {
|
|
245
|
+
return this._key;
|
|
246
|
+
}
|
|
247
|
+
/** Public keyStrategy getter for snapshot validation */
|
|
248
|
+
get keyStrategy() {
|
|
249
|
+
return this._keyStrategy;
|
|
250
|
+
}
|
|
251
|
+
/** Serialize args to string key */
|
|
252
|
+
getSerializedKey(args) {
|
|
253
|
+
return this._serializeArgs(args);
|
|
254
|
+
}
|
|
255
|
+
/** Iterate cache entries — for snapshot */
|
|
256
|
+
cacheEntries() {
|
|
257
|
+
return this._cache.entries();
|
|
258
|
+
}
|
|
259
|
+
/** Hydrate a cache entry from snapshot data */
|
|
260
|
+
hydrateEntry(args, machine) {
|
|
261
|
+
const existing = this._cache.get(args);
|
|
262
|
+
if (existing)
|
|
263
|
+
return; // Don't overwrite existing entries
|
|
264
|
+
const entry = new CacheEntry(machine, this._buildCacheEntryOptions(args));
|
|
265
|
+
this._cache.set(args, entry);
|
|
266
|
+
// Fire lifecycle for hydrated entries
|
|
267
|
+
this._lifecycleHooks.fireCacheEntryAdded(args, () => entry.peek());
|
|
268
|
+
if (machine.state.status === "success") {
|
|
269
|
+
this._lifecycleHooks.resolveCacheDataLoaded(args, machine.state.data);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/** Check if cache entry exists for given args */
|
|
273
|
+
hasEntry(args) {
|
|
274
|
+
return this._cache.has(args);
|
|
275
|
+
}
|
|
276
|
+
/** Pre-populate cache with data */
|
|
277
|
+
populateEntry(args, data) {
|
|
278
|
+
const existing = this._cache.get(args);
|
|
279
|
+
if (existing) {
|
|
280
|
+
const success = MachineSuccess.create(data, args);
|
|
281
|
+
existing.set(success);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const success = MachineSuccess.create(data, args);
|
|
285
|
+
const entry = new CacheEntry(success, this._buildCacheEntryOptions(args));
|
|
286
|
+
this._cache.set(args, entry);
|
|
287
|
+
}
|
|
288
|
+
/** Create an optimistic patch on a cache entry */
|
|
289
|
+
createEntryPatch(args, patchFn) {
|
|
290
|
+
const entry = this._cache.get(args);
|
|
291
|
+
if (!entry)
|
|
292
|
+
return null;
|
|
293
|
+
const machine = entry.peek();
|
|
294
|
+
if (!(machine instanceof MachineWithData))
|
|
295
|
+
return null;
|
|
296
|
+
const { machine: patchedMachine, patch } = machine.createPatch(patchFn);
|
|
297
|
+
entry.set(patchedMachine);
|
|
298
|
+
return {
|
|
299
|
+
commit: () => {
|
|
300
|
+
const current = entry.peek();
|
|
301
|
+
if (current instanceof MachineWithData) {
|
|
302
|
+
const finished = current.finishPatch("commit", patch);
|
|
303
|
+
entry.set(finished);
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
abort: () => {
|
|
307
|
+
const current = entry.peek();
|
|
308
|
+
if (current instanceof MachineWithData) {
|
|
309
|
+
const finished = current.finishPatch("abort", patch);
|
|
310
|
+
entry.set(finished);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** Lock a cache entry — prevent GC eviction. Returns unlock function. */
|
|
316
|
+
lockEntry(args) {
|
|
317
|
+
this.cancelGc(args);
|
|
318
|
+
return {
|
|
319
|
+
unlock: () => {
|
|
320
|
+
// Re-schedule GC (will be cancelled again if still subscribed)
|
|
321
|
+
this.scheduleGc(args);
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
/** Subscribe to refresh error events (used by Agent) */
|
|
326
|
+
onRefreshError(listener) {
|
|
327
|
+
this._refreshErrorListeners.add(listener);
|
|
328
|
+
return () => {
|
|
329
|
+
this._refreshErrorListeners.delete(listener);
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/** Reset the entire cache — aborts in-flight requests, clears GC timers, completes all entries. */
|
|
333
|
+
resetCache() {
|
|
334
|
+
// Abort all in-flight requests
|
|
335
|
+
for (const [, flight] of this._inFlight) {
|
|
336
|
+
flight.abortController.abort();
|
|
337
|
+
}
|
|
338
|
+
this._inFlight.clear();
|
|
339
|
+
// Clear all GC timers
|
|
340
|
+
for (const [, timer] of this._gcTimers) {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
}
|
|
343
|
+
this._gcTimers.clear();
|
|
344
|
+
// Complete all cache entries
|
|
345
|
+
for (const entry of this._cache.values()) {
|
|
346
|
+
entry.complete();
|
|
347
|
+
}
|
|
348
|
+
// Clear lifecycle hooks
|
|
349
|
+
this._lifecycleHooks.clearAll();
|
|
350
|
+
// Clear the cache map
|
|
351
|
+
this._cache.clear();
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Schedule GC for a cache entry after cacheLifetime.
|
|
355
|
+
* Called when subscriber count drops to 0.
|
|
356
|
+
*/
|
|
357
|
+
scheduleGc(args) {
|
|
358
|
+
const key = this._serializeArgs(args);
|
|
359
|
+
this._cancelGcTimer(key);
|
|
360
|
+
this._gcTimers.set(key, setTimeout(() => {
|
|
361
|
+
this._gcTimers.delete(key);
|
|
362
|
+
this._evictEntry(args, key);
|
|
363
|
+
}, this._cacheLifetime));
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Cancel pending GC for a cache entry.
|
|
367
|
+
* Called when a new subscriber appears.
|
|
368
|
+
*/
|
|
369
|
+
cancelGc(args) {
|
|
370
|
+
const key = this._serializeArgs(args);
|
|
371
|
+
this._cancelGcTimer(key);
|
|
372
|
+
}
|
|
373
|
+
// --- Private helpers ---
|
|
374
|
+
_cancelGcTimer(key) {
|
|
375
|
+
const timer = this._gcTimers.get(key);
|
|
376
|
+
if (timer != null) {
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
this._gcTimers.delete(key);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
_evictEntry(args, key) {
|
|
382
|
+
const entry = this._cache.get(args);
|
|
383
|
+
if (!entry)
|
|
384
|
+
return;
|
|
385
|
+
// Abort in-flight if any
|
|
386
|
+
const flight = this._inFlight.get(key);
|
|
387
|
+
if (flight) {
|
|
388
|
+
flight.abortController.abort();
|
|
389
|
+
this._inFlight.delete(key);
|
|
390
|
+
}
|
|
391
|
+
// Fire lifecycle removal
|
|
392
|
+
this._lifecycleHooks.fireCacheEntryRemoved(args);
|
|
393
|
+
// Complete the entry (ADR-4 Layer 3)
|
|
394
|
+
entry.complete();
|
|
395
|
+
// Remove from cache
|
|
396
|
+
this._cache.delete(args);
|
|
397
|
+
}
|
|
398
|
+
async _executeQuery(args, key, cacheEntry, abortController) {
|
|
399
|
+
try {
|
|
400
|
+
const data = await this._queryFn(args, {
|
|
401
|
+
abortSignal: abortController.signal,
|
|
402
|
+
});
|
|
403
|
+
// If aborted after await, don't process result
|
|
404
|
+
if (abortController.signal.aborted) {
|
|
405
|
+
return cacheEntry;
|
|
406
|
+
}
|
|
407
|
+
Batcher.run(() => {
|
|
408
|
+
const current = cacheEntry.peek();
|
|
409
|
+
if (current.state.status === "pending") {
|
|
410
|
+
const success = current.successHappened(data);
|
|
411
|
+
cacheEntry.set(success);
|
|
412
|
+
}
|
|
413
|
+
else if (current.state.status === "refreshing") {
|
|
414
|
+
const success = current.successHappened(data);
|
|
415
|
+
cacheEntry.set(success);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
this._lifecycleHooks.resolveCacheDataLoaded(args, data);
|
|
419
|
+
this._lifecycleHooks.resolveQueryFulfilled(data);
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
if (abortController.signal.aborted) {
|
|
423
|
+
this._lifecycleHooks.rejectQueryFulfilled(error);
|
|
424
|
+
return cacheEntry;
|
|
425
|
+
}
|
|
426
|
+
Batcher.run(() => {
|
|
427
|
+
const current = cacheEntry.peek();
|
|
428
|
+
if (current.state.status === "pending") {
|
|
429
|
+
const errorMachine = current.errorHappened(error);
|
|
430
|
+
cacheEntry.set(errorMachine);
|
|
431
|
+
}
|
|
432
|
+
else if (current.state.status === "refreshing") {
|
|
433
|
+
// ADR-2: Preserve stale data
|
|
434
|
+
const success = current.errorHappened(error);
|
|
435
|
+
cacheEntry.set(success);
|
|
436
|
+
// Notify refresh error listeners (used by Agent)
|
|
437
|
+
for (const listener of this._refreshErrorListeners) {
|
|
438
|
+
listener(args, error);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
this._lifecycleHooks.rejectQueryFulfilled(error);
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
this._inFlight.delete(key);
|
|
446
|
+
}
|
|
447
|
+
return cacheEntry;
|
|
448
|
+
}
|
|
449
|
+
async _executeRefresh(args, key, cacheEntry, abortController) {
|
|
450
|
+
return this._executeQuery(args, key, cacheEntry, abortController);
|
|
451
|
+
}
|
|
452
|
+
_buildCacheEntryOptions(args) {
|
|
453
|
+
const parts = [];
|
|
454
|
+
if (this._keyPrefix)
|
|
455
|
+
parts.push(this._keyPrefix);
|
|
456
|
+
if (this._key)
|
|
457
|
+
parts.push(this._key);
|
|
458
|
+
parts.push(this._serializeArgs(args));
|
|
459
|
+
return {
|
|
460
|
+
keyParts: parts.length > 0 ? parts : undefined,
|
|
461
|
+
beforeDevtoolsPush: this._beforeDevtoolsPush,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type SKIP_TOKEN } from "../../../query-v2/lib/SKIP_TOKEN";
|
|
2
|
+
import type { IResourceV2Agent, IResourceV2AgentState } from "../../../query-v2/types/agent.types";
|
|
3
|
+
import type { ComputeFn } from "../../../signals";
|
|
4
|
+
import type { ResourceV2 } from "./ResourceV2";
|
|
5
|
+
/**
|
|
6
|
+
* Agent that tracks a single cache entry with reactive state, designed for React hook consumption.
|
|
7
|
+
* Provides SWR (stale-while-revalidate) semantics by keeping previous data while new data loads.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ResourceV2Agent<TArgs, TData, TError = Error> implements IResourceV2Agent<TArgs, TData, TError> {
|
|
10
|
+
private readonly _resource;
|
|
11
|
+
private readonly _tracking$;
|
|
12
|
+
private readonly _refreshError$;
|
|
13
|
+
private readonly _state$;
|
|
14
|
+
private readonly _unsubRefreshError;
|
|
15
|
+
private _currentArgs;
|
|
16
|
+
constructor(resource: ResourceV2<TArgs, TData, TError>);
|
|
17
|
+
/** Computed reactive state signal — projects CacheEntry machine state into a flat agent state object. */
|
|
18
|
+
get state$(): ComputeFn<IResourceV2AgentState<TArgs, TData, TError>>;
|
|
19
|
+
/**
|
|
20
|
+
* Start (or re-start) the agent with new args. Skips if args are unchanged.
|
|
21
|
+
*
|
|
22
|
+
* @param args - Query arguments, or `SKIP_TOKEN` to do nothing.
|
|
23
|
+
*/
|
|
24
|
+
start(args: TArgs | SKIP_TOKEN): Promise<void>;
|
|
25
|
+
compareArgs(a: TArgs, b: TArgs): boolean;
|
|
26
|
+
}
|