qortex-core 0.1.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.
- package/index.d.ts +152 -0
- package/index.js +296 -0
- package/index.mjs +267 -0
- package/package.json +45 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query key can be a string or readonly array of strings/numbers
|
|
3
|
+
* Using readonly to prevent accidental mutations
|
|
4
|
+
*/
|
|
5
|
+
type QueryKey = string | readonly (string | number)[];
|
|
6
|
+
/** Valid query key values - only strings and numbers are allowed */
|
|
7
|
+
type QueryKeyValue = string | number;
|
|
8
|
+
/** Function that fetches data, can be async or sync */
|
|
9
|
+
type Fetcher<T = any> = () => Promise<T> | T;
|
|
10
|
+
/** Function that compares two values for equality */
|
|
11
|
+
type EqualityFn<T = any> = (a: T | undefined, b: T | undefined) => boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Infers the resolved return type of a fetcher function
|
|
14
|
+
* Falls back to any for user-friendly experience
|
|
15
|
+
*/
|
|
16
|
+
type InferFetcherResult<F> = F extends (...args: any[]) => Promise<infer R> ? R : F extends (...args: any[]) => infer R ? R : any;
|
|
17
|
+
/**
|
|
18
|
+
* Infers the return type of a fetcher, handling both sync and async cases
|
|
19
|
+
* Falls back to any for user-friendly experience
|
|
20
|
+
*/
|
|
21
|
+
type InferFetcherReturnType<T> = T extends Fetcher<infer R> ? R : any;
|
|
22
|
+
/**
|
|
23
|
+
* Query status types for better type safety
|
|
24
|
+
*/
|
|
25
|
+
type QueryStatus = "idle" | "fetching" | "success" | "error";
|
|
26
|
+
/**
|
|
27
|
+
* Comprehensive options for all query operations
|
|
28
|
+
* Improved with better type constraints
|
|
29
|
+
*/
|
|
30
|
+
type QueryOptions<T = any> = {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
refetchOnSubscribe?: "always" | "stale" | false;
|
|
33
|
+
fetcher?: Fetcher<T>;
|
|
34
|
+
equalityFn?: EqualityFn<T>;
|
|
35
|
+
staleTime?: number;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
placeholderData?: T;
|
|
38
|
+
usePreviousDataOnError?: boolean;
|
|
39
|
+
usePlaceholderOnError?: boolean;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Default configuration options that can be set globally
|
|
43
|
+
* Includes throttleTime which is not part of regular QueryOptions
|
|
44
|
+
*/
|
|
45
|
+
type DefaultConfig = {
|
|
46
|
+
enabled?: boolean;
|
|
47
|
+
refetchOnSubscribe?: "always" | "stale" | false;
|
|
48
|
+
staleTime?: number;
|
|
49
|
+
usePreviousDataOnError?: boolean;
|
|
50
|
+
usePlaceholderOnError?: boolean;
|
|
51
|
+
equalityFn?: EqualityFn<any>;
|
|
52
|
+
throttleTime?: number;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Public query state returned by getQueryState
|
|
56
|
+
* Improved with stricter error typing and better generic constraints
|
|
57
|
+
*/
|
|
58
|
+
type QueryState<T = any, E = Error> = {
|
|
59
|
+
data?: T;
|
|
60
|
+
error?: E;
|
|
61
|
+
status: QueryStatus;
|
|
62
|
+
updatedAt?: number;
|
|
63
|
+
isStale: boolean;
|
|
64
|
+
isPlaceholderData: boolean;
|
|
65
|
+
isLoading: boolean;
|
|
66
|
+
isFetching: boolean;
|
|
67
|
+
isError: boolean;
|
|
68
|
+
isSuccess: boolean;
|
|
69
|
+
refetch: () => Promise<T>;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Core query manager that handles caching, fetching, and state management
|
|
74
|
+
* Implements robust throttling and race condition prevention
|
|
75
|
+
*/
|
|
76
|
+
declare class QueryManager {
|
|
77
|
+
private cache;
|
|
78
|
+
private subs;
|
|
79
|
+
private lastReturnedState;
|
|
80
|
+
private defaultConfig;
|
|
81
|
+
private throttleTime;
|
|
82
|
+
/**
|
|
83
|
+
* Set default configuration for all queries
|
|
84
|
+
*/
|
|
85
|
+
setDefaultConfig({ throttleTime, ...config }: DefaultConfig): void;
|
|
86
|
+
/**
|
|
87
|
+
* Ensures a query state exists in cache, creating it if necessary
|
|
88
|
+
* User-friendly with any fallback for better developer experience
|
|
89
|
+
*/
|
|
90
|
+
private ensureState;
|
|
91
|
+
/**
|
|
92
|
+
* Notifies all subscribers of a query state change
|
|
93
|
+
*/
|
|
94
|
+
private emit;
|
|
95
|
+
/**
|
|
96
|
+
* Registers a fetcher function for a query key
|
|
97
|
+
* Automatically fetches if enabled is not false
|
|
98
|
+
* Enhanced with automatic type inference from fetcher
|
|
99
|
+
*/
|
|
100
|
+
registerFetcher<T = any>(key: QueryKey, opts: QueryOptions<T>): void;
|
|
101
|
+
registerFetcher<F extends Fetcher>(key: QueryKey, opts: QueryOptions<InferFetcherResult<F>> & {
|
|
102
|
+
fetcher: F;
|
|
103
|
+
}): void;
|
|
104
|
+
/**
|
|
105
|
+
* Executes a fetch operation with proper error handling and state management
|
|
106
|
+
* Prevents duplicate fetches
|
|
107
|
+
* Enhanced with automatic type inference from fetcher
|
|
108
|
+
*/
|
|
109
|
+
fetchQuery<T = any>(key: QueryKey, opts?: QueryOptions<T>): Promise<T>;
|
|
110
|
+
fetchQuery<F extends Fetcher>(key: QueryKey, opts: QueryOptions<InferFetcherResult<F>> & {
|
|
111
|
+
fetcher: F;
|
|
112
|
+
}): Promise<InferFetcherResult<F>>;
|
|
113
|
+
/**
|
|
114
|
+
* Manually sets query data without triggering a fetch
|
|
115
|
+
* Marks query as successful
|
|
116
|
+
*/
|
|
117
|
+
setQueryData<T = any>(key: QueryKey, data: T): void;
|
|
118
|
+
/**
|
|
119
|
+
* Gets query data
|
|
120
|
+
* Handles mount logic to potentially start fetching
|
|
121
|
+
*/
|
|
122
|
+
getQueryData<T = any>(key: QueryKey, opts?: QueryOptions<T>): T | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Gets comprehensive query state including computed flags
|
|
125
|
+
* Handles placeholder data and error states appropriately
|
|
126
|
+
* Handles mount logic to potentially start fetching
|
|
127
|
+
*/
|
|
128
|
+
getQueryState<T = unknown>(key: QueryKey, opts?: QueryOptions<T>): QueryState<T>;
|
|
129
|
+
/**
|
|
130
|
+
* Marks a query as invalidated, triggering refetch
|
|
131
|
+
*/
|
|
132
|
+
invalidateQuery(key: QueryKey): void;
|
|
133
|
+
/**
|
|
134
|
+
* Subscribes to query state changes with automatic subscription management
|
|
135
|
+
* Handles mount logic to potentially start fetching
|
|
136
|
+
*/
|
|
137
|
+
subscribeQuery<T = any>(key: QueryKey, cb: () => void, opts?: QueryOptions<T>): () => void;
|
|
138
|
+
/**
|
|
139
|
+
* Core mount logic that determines when to fetch
|
|
140
|
+
* Implements robust throttling and race condition prevention
|
|
141
|
+
*/
|
|
142
|
+
private handleMountLogic;
|
|
143
|
+
}
|
|
144
|
+
declare const queryManager: QueryManager;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Normalizes query keys to a consistent string format for internal storage
|
|
148
|
+
* Arrays are joined with commas, primitives are converted to strings
|
|
149
|
+
*/
|
|
150
|
+
declare function serializeKey(key: QueryKey): string;
|
|
151
|
+
|
|
152
|
+
export { DefaultConfig, EqualityFn, Fetcher, InferFetcherResult, InferFetcherReturnType, QueryKey, QueryKeyValue, QueryManager, QueryOptions, QueryState, QueryStatus, queryManager, serializeKey };
|
package/index.js
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
QueryManager: () => QueryManager,
|
|
24
|
+
queryManager: () => queryManager,
|
|
25
|
+
serializeKey: () => serializeKey
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(src_exports);
|
|
28
|
+
|
|
29
|
+
// src/constants.ts
|
|
30
|
+
var DEFAULT_STALE_TIME = 0;
|
|
31
|
+
var THROTTLE_TIME = 50;
|
|
32
|
+
|
|
33
|
+
// src/utils.ts
|
|
34
|
+
function serializeKey(key) {
|
|
35
|
+
return Array.isArray(key) ? key.join(",") : String(key);
|
|
36
|
+
}
|
|
37
|
+
function shallowEqual(a, b) {
|
|
38
|
+
if (a === b)
|
|
39
|
+
return true;
|
|
40
|
+
if (a == null || b == null)
|
|
41
|
+
return a === b;
|
|
42
|
+
if (typeof a !== "object" || typeof b !== "object")
|
|
43
|
+
return false;
|
|
44
|
+
try {
|
|
45
|
+
const aAny = a;
|
|
46
|
+
const bAny = b;
|
|
47
|
+
const aKeys = Object.keys(aAny);
|
|
48
|
+
const bKeys = Object.keys(bAny);
|
|
49
|
+
if (aKeys.length !== bKeys.length)
|
|
50
|
+
return false;
|
|
51
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
52
|
+
const k = aKeys[i];
|
|
53
|
+
if (aAny[k] !== bAny[k])
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function createDefaultState(opts, refetch) {
|
|
62
|
+
return {
|
|
63
|
+
status: "idle",
|
|
64
|
+
updatedAt: void 0,
|
|
65
|
+
staleTime: opts?.staleTime ?? DEFAULT_STALE_TIME,
|
|
66
|
+
isInvalidated: false,
|
|
67
|
+
fetcher: opts?.fetcher,
|
|
68
|
+
equalityFn: opts?.equalityFn ?? shallowEqual,
|
|
69
|
+
placeholderData: opts?.placeholderData,
|
|
70
|
+
usePreviousDataOnError: opts?.usePreviousDataOnError ?? false,
|
|
71
|
+
usePlaceholderOnError: opts?.usePlaceholderOnError ?? false,
|
|
72
|
+
refetchOnSubscribe: opts?.refetchOnSubscribe ?? "stale",
|
|
73
|
+
enabled: opts?.enabled === false ? false : true,
|
|
74
|
+
refetch: refetch || (() => Promise.resolve(void 0))
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/queryManager.ts
|
|
79
|
+
var QueryManager = class {
|
|
80
|
+
constructor() {
|
|
81
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
82
|
+
this.subs = /* @__PURE__ */ new Map();
|
|
83
|
+
this.lastReturnedState = /* @__PURE__ */ new Map();
|
|
84
|
+
this.defaultConfig = {};
|
|
85
|
+
this.throttleTime = THROTTLE_TIME;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Set default configuration for all queries
|
|
89
|
+
*/
|
|
90
|
+
setDefaultConfig({ throttleTime, ...config }) {
|
|
91
|
+
this.defaultConfig = { ...this.defaultConfig, ...config };
|
|
92
|
+
if (throttleTime !== void 0) {
|
|
93
|
+
this.throttleTime = throttleTime;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Ensures a query state exists in cache, creating it if necessary
|
|
98
|
+
* User-friendly with any fallback for better developer experience
|
|
99
|
+
*/
|
|
100
|
+
ensureState(key, opts = {}) {
|
|
101
|
+
const sk = serializeKey(key);
|
|
102
|
+
const state = this.cache.get(sk);
|
|
103
|
+
const mergedOpts = { ...this.defaultConfig, ...opts };
|
|
104
|
+
if (state) {
|
|
105
|
+
Object.assign(state, mergedOpts);
|
|
106
|
+
state.enabled = mergedOpts.enabled === false ? false : true;
|
|
107
|
+
this.cache.set(sk, state);
|
|
108
|
+
} else {
|
|
109
|
+
const newState = createDefaultState(mergedOpts, () => this.fetchQuery(key));
|
|
110
|
+
this.cache.set(sk, newState);
|
|
111
|
+
}
|
|
112
|
+
return this.cache.get(sk);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Notifies all subscribers of a query state change
|
|
116
|
+
*/
|
|
117
|
+
emit(key, state) {
|
|
118
|
+
this.cache.set(serializeKey(key), state);
|
|
119
|
+
const set = this.subs.get(serializeKey(key));
|
|
120
|
+
if (!set)
|
|
121
|
+
return;
|
|
122
|
+
for (const cb of Array.from(set))
|
|
123
|
+
cb();
|
|
124
|
+
}
|
|
125
|
+
registerFetcher(key, opts) {
|
|
126
|
+
this.ensureState(key, opts);
|
|
127
|
+
if (opts.enabled !== false) {
|
|
128
|
+
try {
|
|
129
|
+
void this.fetchQuery(key);
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
fetchQuery(key, opts) {
|
|
135
|
+
const state = this.ensureState(key, opts);
|
|
136
|
+
if (state.fetchPromise)
|
|
137
|
+
return state.fetchPromise;
|
|
138
|
+
const fetcher = state.fetcher;
|
|
139
|
+
if (!fetcher) {
|
|
140
|
+
console.error("No fetcher found for key", key);
|
|
141
|
+
return Promise.resolve(state.data);
|
|
142
|
+
}
|
|
143
|
+
;
|
|
144
|
+
state.status = "fetching";
|
|
145
|
+
state.lastFetchTime = Date.now();
|
|
146
|
+
this.emit(key, state);
|
|
147
|
+
const result = fetcher();
|
|
148
|
+
const promise = Promise.resolve(result);
|
|
149
|
+
state.fetchPromise = promise;
|
|
150
|
+
promise.then((result2) => {
|
|
151
|
+
state.data = result2;
|
|
152
|
+
state.status = "success";
|
|
153
|
+
state.updatedAt = Date.now();
|
|
154
|
+
}).catch((error) => {
|
|
155
|
+
state.error = error;
|
|
156
|
+
state.status = "error";
|
|
157
|
+
}).finally(() => {
|
|
158
|
+
state.fetchPromise = void 0;
|
|
159
|
+
this.emit(key, state);
|
|
160
|
+
});
|
|
161
|
+
return promise;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Manually sets query data without triggering a fetch
|
|
165
|
+
* Marks query as successful
|
|
166
|
+
*/
|
|
167
|
+
setQueryData(key, data) {
|
|
168
|
+
const state = this.ensureState(key);
|
|
169
|
+
const old = state.data;
|
|
170
|
+
if (state.equalityFn(old, data))
|
|
171
|
+
return;
|
|
172
|
+
state.data = data;
|
|
173
|
+
state.updatedAt = Date.now();
|
|
174
|
+
state.error = void 0;
|
|
175
|
+
state.status = "success";
|
|
176
|
+
state.isInvalidated = false;
|
|
177
|
+
this.emit(key, state);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Gets query data
|
|
181
|
+
* Handles mount logic to potentially start fetching
|
|
182
|
+
*/
|
|
183
|
+
getQueryData(key, opts) {
|
|
184
|
+
const state = this.ensureState(key, opts);
|
|
185
|
+
this.handleMountLogic(key, state);
|
|
186
|
+
return state.data ?? state.placeholderData;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Gets comprehensive query state including computed flags
|
|
190
|
+
* Handles placeholder data and error states appropriately
|
|
191
|
+
* Handles mount logic to potentially start fetching
|
|
192
|
+
*/
|
|
193
|
+
getQueryState(key, opts) {
|
|
194
|
+
let state = this.ensureState(key, opts);
|
|
195
|
+
const now = Date.now();
|
|
196
|
+
const isStale = state.updatedAt == null || now - (state.updatedAt || 0) > state.staleTime || state.isInvalidated;
|
|
197
|
+
let returnedData = state.data;
|
|
198
|
+
let isPlaceholderData = false;
|
|
199
|
+
const status = state.status;
|
|
200
|
+
switch (status) {
|
|
201
|
+
case "error":
|
|
202
|
+
if (state.usePlaceholderOnError && state.placeholderData !== void 0) {
|
|
203
|
+
returnedData = state.placeholderData;
|
|
204
|
+
isPlaceholderData = true;
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
case "fetching":
|
|
208
|
+
if (!state.data && state.placeholderData) {
|
|
209
|
+
returnedData = state.placeholderData;
|
|
210
|
+
isPlaceholderData = true;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
case "success":
|
|
214
|
+
case "idle":
|
|
215
|
+
returnedData = state.data ?? state.placeholderData;
|
|
216
|
+
isPlaceholderData = state.data ? false : Boolean(state.placeholderData);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
this.handleMountLogic(key, state);
|
|
220
|
+
const currentState = {
|
|
221
|
+
data: returnedData,
|
|
222
|
+
error: state.error,
|
|
223
|
+
status: state.status,
|
|
224
|
+
updatedAt: state.updatedAt,
|
|
225
|
+
isStale,
|
|
226
|
+
isPlaceholderData,
|
|
227
|
+
isLoading: state.status === "fetching" && !state.updatedAt,
|
|
228
|
+
// true only for first fetch
|
|
229
|
+
isFetching: state.status === "fetching",
|
|
230
|
+
isError: state.status === "error",
|
|
231
|
+
isSuccess: state.status === "success",
|
|
232
|
+
refetch: state.refetch
|
|
233
|
+
};
|
|
234
|
+
const stateKey = serializeKey(key);
|
|
235
|
+
const lastState = this.lastReturnedState?.get(stateKey);
|
|
236
|
+
if (!lastState || !shallowEqual(lastState, currentState)) {
|
|
237
|
+
if (!this.lastReturnedState)
|
|
238
|
+
this.lastReturnedState = /* @__PURE__ */ new Map();
|
|
239
|
+
this.lastReturnedState.set(stateKey, currentState);
|
|
240
|
+
return currentState;
|
|
241
|
+
}
|
|
242
|
+
return lastState;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Marks a query as invalidated, triggering refetch
|
|
246
|
+
*/
|
|
247
|
+
invalidateQuery(key) {
|
|
248
|
+
const state = this.ensureState(key);
|
|
249
|
+
state.isInvalidated = true;
|
|
250
|
+
this.emit(key, state);
|
|
251
|
+
this.fetchQuery(key);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Subscribes to query state changes with automatic subscription management
|
|
255
|
+
* Handles mount logic to potentially start fetching
|
|
256
|
+
*/
|
|
257
|
+
subscribeQuery(key, cb, opts) {
|
|
258
|
+
const sk = serializeKey(key);
|
|
259
|
+
const state = this.ensureState(key, opts);
|
|
260
|
+
if (!this.subs.has(sk))
|
|
261
|
+
this.subs.set(sk, /* @__PURE__ */ new Set());
|
|
262
|
+
this.subs.get(sk).add(cb);
|
|
263
|
+
this.handleMountLogic(key, state);
|
|
264
|
+
return () => {
|
|
265
|
+
this.subs.get(sk).delete(cb);
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Core mount logic that determines when to fetch
|
|
270
|
+
* Implements robust throttling and race condition prevention
|
|
271
|
+
*/
|
|
272
|
+
handleMountLogic(key, state) {
|
|
273
|
+
const isThrottled = state.lastFetchTime && Date.now() - state.lastFetchTime < this.throttleTime;
|
|
274
|
+
if (state?.status === "fetching" || !state?.enabled || isThrottled || !state?.fetcher)
|
|
275
|
+
return;
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
const isStale = state?.updatedAt == null || now - (state.updatedAt || 0) > state.staleTime || state.isInvalidated;
|
|
278
|
+
let shouldRefetch = false;
|
|
279
|
+
if (state.refetchOnSubscribe === "always") {
|
|
280
|
+
shouldRefetch = true;
|
|
281
|
+
}
|
|
282
|
+
if (state.refetchOnSubscribe === "stale") {
|
|
283
|
+
shouldRefetch = isStale;
|
|
284
|
+
}
|
|
285
|
+
if (shouldRefetch) {
|
|
286
|
+
this.fetchQuery(key);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
var queryManager = new QueryManager();
|
|
291
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
292
|
+
0 && (module.exports = {
|
|
293
|
+
QueryManager,
|
|
294
|
+
queryManager,
|
|
295
|
+
serializeKey
|
|
296
|
+
});
|
package/index.mjs
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var DEFAULT_STALE_TIME = 0;
|
|
3
|
+
var THROTTLE_TIME = 50;
|
|
4
|
+
|
|
5
|
+
// src/utils.ts
|
|
6
|
+
function serializeKey(key) {
|
|
7
|
+
return Array.isArray(key) ? key.join(",") : String(key);
|
|
8
|
+
}
|
|
9
|
+
function shallowEqual(a, b) {
|
|
10
|
+
if (a === b)
|
|
11
|
+
return true;
|
|
12
|
+
if (a == null || b == null)
|
|
13
|
+
return a === b;
|
|
14
|
+
if (typeof a !== "object" || typeof b !== "object")
|
|
15
|
+
return false;
|
|
16
|
+
try {
|
|
17
|
+
const aAny = a;
|
|
18
|
+
const bAny = b;
|
|
19
|
+
const aKeys = Object.keys(aAny);
|
|
20
|
+
const bKeys = Object.keys(bAny);
|
|
21
|
+
if (aKeys.length !== bKeys.length)
|
|
22
|
+
return false;
|
|
23
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
24
|
+
const k = aKeys[i];
|
|
25
|
+
if (aAny[k] !== bAny[k])
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function createDefaultState(opts, refetch) {
|
|
34
|
+
return {
|
|
35
|
+
status: "idle",
|
|
36
|
+
updatedAt: void 0,
|
|
37
|
+
staleTime: opts?.staleTime ?? DEFAULT_STALE_TIME,
|
|
38
|
+
isInvalidated: false,
|
|
39
|
+
fetcher: opts?.fetcher,
|
|
40
|
+
equalityFn: opts?.equalityFn ?? shallowEqual,
|
|
41
|
+
placeholderData: opts?.placeholderData,
|
|
42
|
+
usePreviousDataOnError: opts?.usePreviousDataOnError ?? false,
|
|
43
|
+
usePlaceholderOnError: opts?.usePlaceholderOnError ?? false,
|
|
44
|
+
refetchOnSubscribe: opts?.refetchOnSubscribe ?? "stale",
|
|
45
|
+
enabled: opts?.enabled === false ? false : true,
|
|
46
|
+
refetch: refetch || (() => Promise.resolve(void 0))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/queryManager.ts
|
|
51
|
+
var QueryManager = class {
|
|
52
|
+
constructor() {
|
|
53
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
54
|
+
this.subs = /* @__PURE__ */ new Map();
|
|
55
|
+
this.lastReturnedState = /* @__PURE__ */ new Map();
|
|
56
|
+
this.defaultConfig = {};
|
|
57
|
+
this.throttleTime = THROTTLE_TIME;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Set default configuration for all queries
|
|
61
|
+
*/
|
|
62
|
+
setDefaultConfig({ throttleTime, ...config }) {
|
|
63
|
+
this.defaultConfig = { ...this.defaultConfig, ...config };
|
|
64
|
+
if (throttleTime !== void 0) {
|
|
65
|
+
this.throttleTime = throttleTime;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Ensures a query state exists in cache, creating it if necessary
|
|
70
|
+
* User-friendly with any fallback for better developer experience
|
|
71
|
+
*/
|
|
72
|
+
ensureState(key, opts = {}) {
|
|
73
|
+
const sk = serializeKey(key);
|
|
74
|
+
const state = this.cache.get(sk);
|
|
75
|
+
const mergedOpts = { ...this.defaultConfig, ...opts };
|
|
76
|
+
if (state) {
|
|
77
|
+
Object.assign(state, mergedOpts);
|
|
78
|
+
state.enabled = mergedOpts.enabled === false ? false : true;
|
|
79
|
+
this.cache.set(sk, state);
|
|
80
|
+
} else {
|
|
81
|
+
const newState = createDefaultState(mergedOpts, () => this.fetchQuery(key));
|
|
82
|
+
this.cache.set(sk, newState);
|
|
83
|
+
}
|
|
84
|
+
return this.cache.get(sk);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Notifies all subscribers of a query state change
|
|
88
|
+
*/
|
|
89
|
+
emit(key, state) {
|
|
90
|
+
this.cache.set(serializeKey(key), state);
|
|
91
|
+
const set = this.subs.get(serializeKey(key));
|
|
92
|
+
if (!set)
|
|
93
|
+
return;
|
|
94
|
+
for (const cb of Array.from(set))
|
|
95
|
+
cb();
|
|
96
|
+
}
|
|
97
|
+
registerFetcher(key, opts) {
|
|
98
|
+
this.ensureState(key, opts);
|
|
99
|
+
if (opts.enabled !== false) {
|
|
100
|
+
try {
|
|
101
|
+
void this.fetchQuery(key);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
fetchQuery(key, opts) {
|
|
107
|
+
const state = this.ensureState(key, opts);
|
|
108
|
+
if (state.fetchPromise)
|
|
109
|
+
return state.fetchPromise;
|
|
110
|
+
const fetcher = state.fetcher;
|
|
111
|
+
if (!fetcher) {
|
|
112
|
+
console.error("No fetcher found for key", key);
|
|
113
|
+
return Promise.resolve(state.data);
|
|
114
|
+
}
|
|
115
|
+
;
|
|
116
|
+
state.status = "fetching";
|
|
117
|
+
state.lastFetchTime = Date.now();
|
|
118
|
+
this.emit(key, state);
|
|
119
|
+
const result = fetcher();
|
|
120
|
+
const promise = Promise.resolve(result);
|
|
121
|
+
state.fetchPromise = promise;
|
|
122
|
+
promise.then((result2) => {
|
|
123
|
+
state.data = result2;
|
|
124
|
+
state.status = "success";
|
|
125
|
+
state.updatedAt = Date.now();
|
|
126
|
+
}).catch((error) => {
|
|
127
|
+
state.error = error;
|
|
128
|
+
state.status = "error";
|
|
129
|
+
}).finally(() => {
|
|
130
|
+
state.fetchPromise = void 0;
|
|
131
|
+
this.emit(key, state);
|
|
132
|
+
});
|
|
133
|
+
return promise;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Manually sets query data without triggering a fetch
|
|
137
|
+
* Marks query as successful
|
|
138
|
+
*/
|
|
139
|
+
setQueryData(key, data) {
|
|
140
|
+
const state = this.ensureState(key);
|
|
141
|
+
const old = state.data;
|
|
142
|
+
if (state.equalityFn(old, data))
|
|
143
|
+
return;
|
|
144
|
+
state.data = data;
|
|
145
|
+
state.updatedAt = Date.now();
|
|
146
|
+
state.error = void 0;
|
|
147
|
+
state.status = "success";
|
|
148
|
+
state.isInvalidated = false;
|
|
149
|
+
this.emit(key, state);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Gets query data
|
|
153
|
+
* Handles mount logic to potentially start fetching
|
|
154
|
+
*/
|
|
155
|
+
getQueryData(key, opts) {
|
|
156
|
+
const state = this.ensureState(key, opts);
|
|
157
|
+
this.handleMountLogic(key, state);
|
|
158
|
+
return state.data ?? state.placeholderData;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Gets comprehensive query state including computed flags
|
|
162
|
+
* Handles placeholder data and error states appropriately
|
|
163
|
+
* Handles mount logic to potentially start fetching
|
|
164
|
+
*/
|
|
165
|
+
getQueryState(key, opts) {
|
|
166
|
+
let state = this.ensureState(key, opts);
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const isStale = state.updatedAt == null || now - (state.updatedAt || 0) > state.staleTime || state.isInvalidated;
|
|
169
|
+
let returnedData = state.data;
|
|
170
|
+
let isPlaceholderData = false;
|
|
171
|
+
const status = state.status;
|
|
172
|
+
switch (status) {
|
|
173
|
+
case "error":
|
|
174
|
+
if (state.usePlaceholderOnError && state.placeholderData !== void 0) {
|
|
175
|
+
returnedData = state.placeholderData;
|
|
176
|
+
isPlaceholderData = true;
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case "fetching":
|
|
180
|
+
if (!state.data && state.placeholderData) {
|
|
181
|
+
returnedData = state.placeholderData;
|
|
182
|
+
isPlaceholderData = true;
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
case "success":
|
|
186
|
+
case "idle":
|
|
187
|
+
returnedData = state.data ?? state.placeholderData;
|
|
188
|
+
isPlaceholderData = state.data ? false : Boolean(state.placeholderData);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
this.handleMountLogic(key, state);
|
|
192
|
+
const currentState = {
|
|
193
|
+
data: returnedData,
|
|
194
|
+
error: state.error,
|
|
195
|
+
status: state.status,
|
|
196
|
+
updatedAt: state.updatedAt,
|
|
197
|
+
isStale,
|
|
198
|
+
isPlaceholderData,
|
|
199
|
+
isLoading: state.status === "fetching" && !state.updatedAt,
|
|
200
|
+
// true only for first fetch
|
|
201
|
+
isFetching: state.status === "fetching",
|
|
202
|
+
isError: state.status === "error",
|
|
203
|
+
isSuccess: state.status === "success",
|
|
204
|
+
refetch: state.refetch
|
|
205
|
+
};
|
|
206
|
+
const stateKey = serializeKey(key);
|
|
207
|
+
const lastState = this.lastReturnedState?.get(stateKey);
|
|
208
|
+
if (!lastState || !shallowEqual(lastState, currentState)) {
|
|
209
|
+
if (!this.lastReturnedState)
|
|
210
|
+
this.lastReturnedState = /* @__PURE__ */ new Map();
|
|
211
|
+
this.lastReturnedState.set(stateKey, currentState);
|
|
212
|
+
return currentState;
|
|
213
|
+
}
|
|
214
|
+
return lastState;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Marks a query as invalidated, triggering refetch
|
|
218
|
+
*/
|
|
219
|
+
invalidateQuery(key) {
|
|
220
|
+
const state = this.ensureState(key);
|
|
221
|
+
state.isInvalidated = true;
|
|
222
|
+
this.emit(key, state);
|
|
223
|
+
this.fetchQuery(key);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Subscribes to query state changes with automatic subscription management
|
|
227
|
+
* Handles mount logic to potentially start fetching
|
|
228
|
+
*/
|
|
229
|
+
subscribeQuery(key, cb, opts) {
|
|
230
|
+
const sk = serializeKey(key);
|
|
231
|
+
const state = this.ensureState(key, opts);
|
|
232
|
+
if (!this.subs.has(sk))
|
|
233
|
+
this.subs.set(sk, /* @__PURE__ */ new Set());
|
|
234
|
+
this.subs.get(sk).add(cb);
|
|
235
|
+
this.handleMountLogic(key, state);
|
|
236
|
+
return () => {
|
|
237
|
+
this.subs.get(sk).delete(cb);
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Core mount logic that determines when to fetch
|
|
242
|
+
* Implements robust throttling and race condition prevention
|
|
243
|
+
*/
|
|
244
|
+
handleMountLogic(key, state) {
|
|
245
|
+
const isThrottled = state.lastFetchTime && Date.now() - state.lastFetchTime < this.throttleTime;
|
|
246
|
+
if (state?.status === "fetching" || !state?.enabled || isThrottled || !state?.fetcher)
|
|
247
|
+
return;
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
const isStale = state?.updatedAt == null || now - (state.updatedAt || 0) > state.staleTime || state.isInvalidated;
|
|
250
|
+
let shouldRefetch = false;
|
|
251
|
+
if (state.refetchOnSubscribe === "always") {
|
|
252
|
+
shouldRefetch = true;
|
|
253
|
+
}
|
|
254
|
+
if (state.refetchOnSubscribe === "stale") {
|
|
255
|
+
shouldRefetch = isStale;
|
|
256
|
+
}
|
|
257
|
+
if (shouldRefetch) {
|
|
258
|
+
this.fetchQuery(key);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
var queryManager = new QueryManager();
|
|
263
|
+
export {
|
|
264
|
+
QueryManager,
|
|
265
|
+
queryManager,
|
|
266
|
+
serializeKey
|
|
267
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qortex-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic query cache & fetch registry (MFE friendly).",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"import": "./index.mjs",
|
|
12
|
+
"require": "./index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"index.mjs",
|
|
21
|
+
"index.d.ts",
|
|
22
|
+
"README.md"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/Darshan-Naik/qortex.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/Darshan-Naik/qortex#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/Darshan-Naik/qortex/issues"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"qortex",
|
|
34
|
+
"query",
|
|
35
|
+
"cache",
|
|
36
|
+
"runtime",
|
|
37
|
+
"mfe",
|
|
38
|
+
"data-fetching",
|
|
39
|
+
"typescript"
|
|
40
|
+
],
|
|
41
|
+
"author": "Darshan Naik",
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"dependencies": {},
|
|
44
|
+
"peerDependencies": {}
|
|
45
|
+
}
|