@epoch32/visage 0.0.0-stage → 0.0.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/LICENSE +373 -0
- package/README.md +110 -2
- package/dist/bundle.dev.js +4875 -0
- package/dist/index.js +9633 -0
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/dist/vite/index.js +707 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- package/src/vite/types.ts +65 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export type OnlineListener = (online: boolean) => void;
|
|
2
|
+
|
|
3
|
+
export class OnlineManager {
|
|
4
|
+
private _online?: boolean;
|
|
5
|
+
private _listeners = new Set<OnlineListener>();
|
|
6
|
+
private _cleanup?: () => void;
|
|
7
|
+
|
|
8
|
+
constructor() {
|
|
9
|
+
this.setupEventListener();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private setupEventListener() {
|
|
13
|
+
if (typeof window === "undefined" || typeof window.addEventListener === "undefined") {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const onlineListener = () => this.notify(true);
|
|
18
|
+
const offlineListener = () => this.notify(false);
|
|
19
|
+
|
|
20
|
+
window.addEventListener("online", onlineListener, false);
|
|
21
|
+
window.addEventListener("offline", offlineListener, false);
|
|
22
|
+
|
|
23
|
+
this._cleanup = () => {
|
|
24
|
+
window.removeEventListener("online", onlineListener);
|
|
25
|
+
window.removeEventListener("offline", offlineListener);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setOnline(online?: boolean) {
|
|
30
|
+
this._online = online;
|
|
31
|
+
this.notify(this.isOnline());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
isOnline(): boolean {
|
|
35
|
+
if (typeof this._online === "boolean") {
|
|
36
|
+
return this._online;
|
|
37
|
+
}
|
|
38
|
+
if (typeof navigator !== "undefined" && typeof navigator.onLine === "boolean") {
|
|
39
|
+
return navigator.onLine;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
subscribe(listener: OnlineListener): () => void {
|
|
45
|
+
this._listeners.add(listener);
|
|
46
|
+
return () => {
|
|
47
|
+
this._listeners.delete(listener);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private notify(online: boolean) {
|
|
52
|
+
for (const listener of this._listeners) {
|
|
53
|
+
try {
|
|
54
|
+
listener(online);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error("Error in online listener:", err);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
cleanup() {
|
|
62
|
+
this._cleanup?.();
|
|
63
|
+
this._listeners.clear();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const onlineManager = new OnlineManager();
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { signal, type WritableSignal } from "../signals";
|
|
2
|
+
import type { QueryClient } from "./query-client";
|
|
3
|
+
import type {
|
|
4
|
+
FetchStatus,
|
|
5
|
+
QueryFilters,
|
|
6
|
+
QueryKey,
|
|
7
|
+
QueryOptions,
|
|
8
|
+
QueryState,
|
|
9
|
+
QueryStatus,
|
|
10
|
+
RetryDelayValue,
|
|
11
|
+
RetryValue
|
|
12
|
+
} from "./types";
|
|
13
|
+
import { hashKey, matchQuery, sleep } from "./utils";
|
|
14
|
+
|
|
15
|
+
export interface QueryObserver<TData = any, TError = any> {
|
|
16
|
+
onQueryUpdate(): void;
|
|
17
|
+
options: QueryOptions<TData, TError, any>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function shouldRetry(failureCount: number, error: any, retry: RetryValue | undefined): boolean {
|
|
21
|
+
if (retry === false) return false;
|
|
22
|
+
if (retry === true) return true;
|
|
23
|
+
if (typeof retry === "number") return failureCount <= retry;
|
|
24
|
+
if (typeof retry === "function") return retry(failureCount, error);
|
|
25
|
+
// Default: 3 retries
|
|
26
|
+
return failureCount < 3;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getRetryDelay(
|
|
30
|
+
attempt: number,
|
|
31
|
+
error: any,
|
|
32
|
+
retryDelay: RetryDelayValue | undefined
|
|
33
|
+
): number {
|
|
34
|
+
if (typeof retryDelay === "function") return retryDelay(attempt, error);
|
|
35
|
+
if (typeof retryDelay === "number") return retryDelay;
|
|
36
|
+
return Math.min(1000 * 2 ** attempt, 30000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Query<TData = unknown, TError = any> {
|
|
40
|
+
readonly queryKey: QueryKey;
|
|
41
|
+
readonly queryHash: string;
|
|
42
|
+
cache: QueryCache;
|
|
43
|
+
|
|
44
|
+
readonly data: WritableSignal<TData | undefined>;
|
|
45
|
+
readonly error: WritableSignal<TError | null>;
|
|
46
|
+
readonly status: WritableSignal<QueryStatus>;
|
|
47
|
+
readonly fetchStatus: WritableSignal<FetchStatus>;
|
|
48
|
+
readonly dataUpdatedAt: WritableSignal<number>;
|
|
49
|
+
readonly errorUpdatedAt: WritableSignal<number>;
|
|
50
|
+
readonly failureCount: WritableSignal<number>;
|
|
51
|
+
readonly isInvalidated: WritableSignal<boolean>;
|
|
52
|
+
|
|
53
|
+
observers = new Set<QueryObserver<TData, TError>>();
|
|
54
|
+
private _gcTimeout: any = null;
|
|
55
|
+
private _abortController: AbortController | null = null;
|
|
56
|
+
promise: Promise<TData> | null = null;
|
|
57
|
+
defaultGcTime = 5 * 60 * 1000; // 5 minutes
|
|
58
|
+
|
|
59
|
+
constructor(options: {
|
|
60
|
+
queryKey: QueryKey;
|
|
61
|
+
queryHash: string;
|
|
62
|
+
cache: QueryCache;
|
|
63
|
+
initialData?: TData | (() => TData);
|
|
64
|
+
initialDataUpdatedAt?: number | (() => number);
|
|
65
|
+
}) {
|
|
66
|
+
this.queryKey = options.queryKey;
|
|
67
|
+
this.queryHash = options.queryHash;
|
|
68
|
+
this.cache = options.cache;
|
|
69
|
+
|
|
70
|
+
const initialData =
|
|
71
|
+
typeof options.initialData === "function"
|
|
72
|
+
? (options.initialData as () => TData)()
|
|
73
|
+
: options.initialData;
|
|
74
|
+
|
|
75
|
+
const initialUpdatedAt =
|
|
76
|
+
initialData !== undefined
|
|
77
|
+
? typeof options.initialDataUpdatedAt === "function"
|
|
78
|
+
? options.initialDataUpdatedAt()
|
|
79
|
+
: (options.initialDataUpdatedAt ?? Date.now())
|
|
80
|
+
: 0;
|
|
81
|
+
|
|
82
|
+
this.data = signal<TData | undefined>(initialData);
|
|
83
|
+
this.error = signal<TError | null>(null);
|
|
84
|
+
this.status = signal<QueryStatus>(initialData !== undefined ? "success" : "pending");
|
|
85
|
+
this.fetchStatus = signal<FetchStatus>("idle");
|
|
86
|
+
this.dataUpdatedAt = signal<number>(initialUpdatedAt);
|
|
87
|
+
this.errorUpdatedAt = signal<number>(0);
|
|
88
|
+
this.failureCount = signal<number>(0);
|
|
89
|
+
this.isInvalidated = signal<boolean>(false);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get state(): QueryState<TData, TError> {
|
|
93
|
+
return {
|
|
94
|
+
data: this.data(),
|
|
95
|
+
error: this.error(),
|
|
96
|
+
status: this.status(),
|
|
97
|
+
fetchStatus: this.fetchStatus(),
|
|
98
|
+
dataUpdatedAt: this.dataUpdatedAt(),
|
|
99
|
+
errorUpdatedAt: this.errorUpdatedAt(),
|
|
100
|
+
failureCount: this.failureCount(),
|
|
101
|
+
isInvalidated: this.isInvalidated()
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
isStale(staleTime = 0): boolean {
|
|
106
|
+
if (this.isInvalidated()) return true;
|
|
107
|
+
if (this.dataUpdatedAt() === 0) return true;
|
|
108
|
+
if (staleTime === Number.POSITIVE_INFINITY) return false;
|
|
109
|
+
return Date.now() - this.dataUpdatedAt() >= staleTime;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
addObserver(observer: QueryObserver<TData, TError>) {
|
|
113
|
+
this.observers.add(observer);
|
|
114
|
+
this.clearGcTimeout();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
removeObserver(observer: QueryObserver<TData, TError>) {
|
|
118
|
+
this.observers.delete(observer);
|
|
119
|
+
if (this.observers.size === 0) {
|
|
120
|
+
this.scheduleGc(observer.options.gcTime);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
scheduleGc(gcTime?: number) {
|
|
125
|
+
this.clearGcTimeout();
|
|
126
|
+
const duration = gcTime !== undefined ? gcTime : this.defaultGcTime;
|
|
127
|
+
if (duration === Number.POSITIVE_INFINITY) return;
|
|
128
|
+
if (duration <= 0) {
|
|
129
|
+
this.cache.remove(this);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this._gcTimeout = setTimeout(() => {
|
|
133
|
+
if (this.observers.size === 0) {
|
|
134
|
+
this.cache.remove(this);
|
|
135
|
+
}
|
|
136
|
+
}, duration);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
clearGcTimeout() {
|
|
140
|
+
if (this._gcTimeout !== null) {
|
|
141
|
+
clearTimeout(this._gcTimeout);
|
|
142
|
+
this._gcTimeout = null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setData(
|
|
147
|
+
updater: TData | ((prev: TData | undefined) => TData),
|
|
148
|
+
options?: { updatedAt?: number }
|
|
149
|
+
): TData {
|
|
150
|
+
const prev = this.data();
|
|
151
|
+
const nextData = typeof updater === "function" ? (updater as any)(prev) : updater;
|
|
152
|
+
|
|
153
|
+
this.data.set(nextData);
|
|
154
|
+
this.error.set(null);
|
|
155
|
+
this.status.set("success");
|
|
156
|
+
this.isInvalidated.set(false);
|
|
157
|
+
this.dataUpdatedAt.set(options?.updatedAt ?? Date.now());
|
|
158
|
+
|
|
159
|
+
this.notifyObservers();
|
|
160
|
+
this.cache.notify({ type: "updated", query: this });
|
|
161
|
+
return nextData;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
invalidate() {
|
|
165
|
+
this.isInvalidated.set(true);
|
|
166
|
+
if (this.observers.size > 0) {
|
|
167
|
+
for (const observer of this.observers) {
|
|
168
|
+
observer.onQueryUpdate();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
cancel() {
|
|
174
|
+
if (this._abortController) {
|
|
175
|
+
this._abortController.abort();
|
|
176
|
+
this._abortController = null;
|
|
177
|
+
}
|
|
178
|
+
this.fetchStatus.set("idle");
|
|
179
|
+
this.promise = null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
reset() {
|
|
183
|
+
this.cancel();
|
|
184
|
+
this.data.set(undefined);
|
|
185
|
+
this.error.set(null);
|
|
186
|
+
this.status.set("pending");
|
|
187
|
+
this.fetchStatus.set("idle");
|
|
188
|
+
this.dataUpdatedAt.set(0);
|
|
189
|
+
this.errorUpdatedAt.set(0);
|
|
190
|
+
this.failureCount.set(0);
|
|
191
|
+
this.isInvalidated.set(false);
|
|
192
|
+
this.notifyObservers();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async fetch(
|
|
196
|
+
options?: Partial<QueryOptions<TData, TError, any>>,
|
|
197
|
+
fetchOptions?: { cancelRefetch?: boolean }
|
|
198
|
+
): Promise<TData> {
|
|
199
|
+
if (this.promise && !fetchOptions?.cancelRefetch) {
|
|
200
|
+
return this.promise;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (this._abortController && fetchOptions?.cancelRefetch) {
|
|
204
|
+
this.cancel();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const controller = new AbortController();
|
|
208
|
+
this._abortController = controller;
|
|
209
|
+
this.fetchStatus.set("fetching");
|
|
210
|
+
|
|
211
|
+
const queryFn = options?.queryFn;
|
|
212
|
+
if (!queryFn) {
|
|
213
|
+
this.fetchStatus.set("idle");
|
|
214
|
+
throw new Error(`Missing queryFn for queryKey: ${JSON.stringify(this.queryKey)}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const execFetch = async (): Promise<TData> => {
|
|
218
|
+
let attempt = 0;
|
|
219
|
+
|
|
220
|
+
while (true) {
|
|
221
|
+
if (controller.signal.aborted) {
|
|
222
|
+
this.fetchStatus.set("idle");
|
|
223
|
+
this.promise = null;
|
|
224
|
+
throw new Error("Query was cancelled");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const result = await queryFn({
|
|
229
|
+
queryKey: this.queryKey,
|
|
230
|
+
signal: controller.signal,
|
|
231
|
+
meta: options?.meta
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (controller.signal.aborted) {
|
|
235
|
+
throw new Error("Query was cancelled");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.setData(result);
|
|
239
|
+
this.failureCount.set(0);
|
|
240
|
+
this.fetchStatus.set("idle");
|
|
241
|
+
this.promise = null;
|
|
242
|
+
return result;
|
|
243
|
+
} catch (err: any) {
|
|
244
|
+
if (controller.signal.aborted) {
|
|
245
|
+
this.fetchStatus.set("idle");
|
|
246
|
+
this.promise = null;
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
attempt++;
|
|
251
|
+
this.failureCount.set(attempt);
|
|
252
|
+
|
|
253
|
+
const canRetry = shouldRetry(attempt, err, options?.retry);
|
|
254
|
+
if (canRetry) {
|
|
255
|
+
const delay = getRetryDelay(attempt, err, options?.retryDelay);
|
|
256
|
+
if (delay > 0) {
|
|
257
|
+
await sleep(delay);
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
this.error.set(err);
|
|
263
|
+
this.errorUpdatedAt.set(Date.now());
|
|
264
|
+
this.status.set("error");
|
|
265
|
+
this.fetchStatus.set("idle");
|
|
266
|
+
this.promise = null;
|
|
267
|
+
this.notifyObservers();
|
|
268
|
+
throw err;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
this.promise = execFetch();
|
|
274
|
+
return this.promise;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private notifyObservers() {
|
|
278
|
+
for (const observer of this.observers) {
|
|
279
|
+
observer.onQueryUpdate();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
destroy() {
|
|
284
|
+
this.cancel();
|
|
285
|
+
this.clearGcTimeout();
|
|
286
|
+
this.observers.clear();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export class QueryCache {
|
|
291
|
+
queries = new Map<string, Query<any, any>>();
|
|
292
|
+
private _subscribers = new Set<(event: { type: string; query?: Query<any, any> }) => void>();
|
|
293
|
+
|
|
294
|
+
subscribe(listener: (event: { type: string; query?: Query<any, any> }) => void): () => void {
|
|
295
|
+
this._subscribers.add(listener);
|
|
296
|
+
return () => this._subscribers.delete(listener);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
notify(event: { type: string; query?: Query<any, any> }) {
|
|
300
|
+
for (const sub of this._subscribers) {
|
|
301
|
+
sub(event);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
getAll(): Query<any, any>[] {
|
|
306
|
+
return Array.from(this.queries.values());
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
build<TData, TError>(
|
|
310
|
+
_client: QueryClient,
|
|
311
|
+
options: QueryOptions<TData, TError, any>
|
|
312
|
+
): Query<TData, TError> {
|
|
313
|
+
const queryHash = hashKey(options.queryKey);
|
|
314
|
+
let query = this.queries.get(queryHash);
|
|
315
|
+
|
|
316
|
+
if (!query) {
|
|
317
|
+
query = new Query({
|
|
318
|
+
queryKey: options.queryKey,
|
|
319
|
+
queryHash,
|
|
320
|
+
cache: this,
|
|
321
|
+
initialData: options.initialData,
|
|
322
|
+
initialDataUpdatedAt: options.initialDataUpdatedAt
|
|
323
|
+
});
|
|
324
|
+
this.add(query);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return query;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
get<TData = unknown, TError = any>(queryHash: string): Query<TData, TError> | undefined {
|
|
331
|
+
return this.queries.get(queryHash);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
add(query: Query<any, any>) {
|
|
335
|
+
this.queries.set(query.queryHash, query);
|
|
336
|
+
this.notify({ type: "added", query });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
remove(query: Query<any, any>) {
|
|
340
|
+
const existing = this.queries.get(query.queryHash);
|
|
341
|
+
if (existing) {
|
|
342
|
+
existing.destroy();
|
|
343
|
+
this.queries.delete(query.queryHash);
|
|
344
|
+
this.notify({ type: "removed", query: existing });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
find<TData = unknown, TError = any>(filters: QueryFilters): Query<TData, TError> | undefined {
|
|
349
|
+
return this.findAll<TData, TError>(filters)[0];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
findAll<TData = unknown, TError = any>(filters?: QueryFilters): Query<TData, TError>[] {
|
|
353
|
+
const results: Query<TData, TError>[] = [];
|
|
354
|
+
for (const query of this.queries.values()) {
|
|
355
|
+
if (matchQuery(filters, query)) {
|
|
356
|
+
results.push(query);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return results;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
clear() {
|
|
363
|
+
for (const query of this.queries.values()) {
|
|
364
|
+
query.destroy();
|
|
365
|
+
}
|
|
366
|
+
this.queries.clear();
|
|
367
|
+
this.notify({ type: "cleared" });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { Child } from "../dom/render";
|
|
2
|
+
import { type Context, createContext, useContext } from "../signals/context";
|
|
3
|
+
import { focusManager } from "./focus-manager";
|
|
4
|
+
import { MutationCache } from "./mutation-cache";
|
|
5
|
+
import { onlineManager } from "./online-manager";
|
|
6
|
+
import { QueryCache } from "./query-cache";
|
|
7
|
+
import type { DefaultOptions, QueryFilters, QueryKey, QueryOptions, QueryState } from "./types";
|
|
8
|
+
import { hashKey } from "./utils";
|
|
9
|
+
|
|
10
|
+
export interface QueryClientConfig {
|
|
11
|
+
defaultOptions?: DefaultOptions;
|
|
12
|
+
queryCache?: QueryCache;
|
|
13
|
+
mutationCache?: MutationCache;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class QueryClient {
|
|
17
|
+
queryCache: QueryCache;
|
|
18
|
+
mutationCache: MutationCache;
|
|
19
|
+
defaultOptions: DefaultOptions;
|
|
20
|
+
|
|
21
|
+
private _unsubscribeFocus?: () => void;
|
|
22
|
+
private _unsubscribeOnline?: () => void;
|
|
23
|
+
private _isMounted = false;
|
|
24
|
+
|
|
25
|
+
constructor(config: QueryClientConfig = {}) {
|
|
26
|
+
this.defaultOptions = config.defaultOptions ?? {};
|
|
27
|
+
this.queryCache = config.queryCache ?? new QueryCache();
|
|
28
|
+
this.mutationCache = config.mutationCache ?? new MutationCache();
|
|
29
|
+
this.mount();
|
|
30
|
+
|
|
31
|
+
if (typeof window !== "undefined" && (window as any).__VISAGE_DEVTOOLS__) {
|
|
32
|
+
(window as any).__VISAGE_DEVTOOLS__.attachQueryClient(this);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
mount() {
|
|
37
|
+
if (this._isMounted) return;
|
|
38
|
+
this._isMounted = true;
|
|
39
|
+
|
|
40
|
+
this._unsubscribeFocus = focusManager.subscribe((focused) => {
|
|
41
|
+
if (focused) {
|
|
42
|
+
this.onWindowFocus();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this._unsubscribeOnline = onlineManager.subscribe((online) => {
|
|
47
|
+
if (online) {
|
|
48
|
+
this.onOnline();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
unmount() {
|
|
54
|
+
this._isMounted = false;
|
|
55
|
+
this._unsubscribeFocus?.();
|
|
56
|
+
this._unsubscribeOnline?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private onWindowFocus() {
|
|
60
|
+
const activeQueries = this.queryCache.findAll({ type: "active" });
|
|
61
|
+
for (const query of activeQueries) {
|
|
62
|
+
const observer = Array.from(query.observers)[0];
|
|
63
|
+
const options = observer?.options ?? this.defaultOptions.queries ?? {};
|
|
64
|
+
const refetchOnFocus = options.refetchOnWindowFocus ?? true;
|
|
65
|
+
|
|
66
|
+
if (refetchOnFocus === "always" || (refetchOnFocus && query.isStale(options.staleTime))) {
|
|
67
|
+
query.fetch(options).catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private onOnline() {
|
|
73
|
+
const activeQueries = this.queryCache.findAll({ type: "active" });
|
|
74
|
+
for (const query of activeQueries) {
|
|
75
|
+
const observer = Array.from(query.observers)[0];
|
|
76
|
+
const options = observer?.options ?? this.defaultOptions.queries ?? {};
|
|
77
|
+
const refetchOnReconnect = options.refetchOnReconnect ?? true;
|
|
78
|
+
|
|
79
|
+
if (
|
|
80
|
+
refetchOnReconnect === "always" ||
|
|
81
|
+
(refetchOnReconnect && query.isStale(options.staleTime))
|
|
82
|
+
) {
|
|
83
|
+
query.fetch(options).catch(() => {});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
getDefaultOptions(): DefaultOptions {
|
|
89
|
+
return this.defaultOptions;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setDefaultOptions(options: DefaultOptions) {
|
|
93
|
+
this.defaultOptions = options;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getQueryCache(): QueryCache {
|
|
97
|
+
return this.queryCache;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getMutationCache(): MutationCache {
|
|
101
|
+
return this.mutationCache;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getQueryData<TData = unknown>(queryKey: QueryKey): TData | undefined {
|
|
105
|
+
const queryHash = hashKey(queryKey);
|
|
106
|
+
const query = this.queryCache.get<TData>(queryHash);
|
|
107
|
+
return query?.data();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setQueryData<TData = unknown>(
|
|
111
|
+
queryKey: QueryKey,
|
|
112
|
+
updater: TData | ((prev: TData | undefined) => TData),
|
|
113
|
+
options?: { updatedAt?: number }
|
|
114
|
+
): TData {
|
|
115
|
+
const query = this.queryCache.build<TData, any>(this, {
|
|
116
|
+
queryKey,
|
|
117
|
+
...this.defaultOptions.queries
|
|
118
|
+
});
|
|
119
|
+
return query.setData(updater, options);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
getQueryState<TData = unknown, TError = any>(
|
|
123
|
+
queryKey: QueryKey
|
|
124
|
+
): QueryState<TData, TError> | undefined {
|
|
125
|
+
const queryHash = hashKey(queryKey);
|
|
126
|
+
const query = this.queryCache.get<TData, TError>(queryHash);
|
|
127
|
+
return query?.state;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async fetchQuery<TData = unknown, TError = any>(
|
|
131
|
+
options: QueryOptions<TData, TError, any>
|
|
132
|
+
): Promise<TData> {
|
|
133
|
+
const mergedOptions: QueryOptions<TData, TError, any> = {
|
|
134
|
+
...this.defaultOptions.queries,
|
|
135
|
+
...options
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const query = this.queryCache.build<TData, TError>(this, mergedOptions);
|
|
139
|
+
|
|
140
|
+
if (!query.isStale(mergedOptions.staleTime) && query.data() !== undefined) {
|
|
141
|
+
return query.data() as TData;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return query.fetch(mergedOptions);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async prefetchQuery<TData = unknown, TError = any>(
|
|
148
|
+
options: QueryOptions<TData, TError, any>
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
try {
|
|
151
|
+
await this.fetchQuery(options);
|
|
152
|
+
} catch {
|
|
153
|
+
// Prefetch errors are swallowed
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async invalidateQueries(filters?: QueryFilters): Promise<void> {
|
|
158
|
+
const queries = this.queryCache.findAll(filters);
|
|
159
|
+
const promises: Promise<any>[] = [];
|
|
160
|
+
|
|
161
|
+
for (const query of queries) {
|
|
162
|
+
query.invalidate();
|
|
163
|
+
if (query.observers.size > 0) {
|
|
164
|
+
const observer = Array.from(query.observers)[0];
|
|
165
|
+
const options = observer?.options ?? this.defaultOptions.queries ?? {};
|
|
166
|
+
promises.push(query.fetch(options).catch(() => {}));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
await Promise.all(promises);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async refetchQueries(filters?: QueryFilters): Promise<void> {
|
|
174
|
+
const queries = this.queryCache.findAll(filters);
|
|
175
|
+
const promises: Promise<any>[] = [];
|
|
176
|
+
|
|
177
|
+
for (const query of queries) {
|
|
178
|
+
if (query.observers.size > 0) {
|
|
179
|
+
const observer = Array.from(query.observers)[0];
|
|
180
|
+
const options = observer?.options ?? this.defaultOptions.queries ?? {};
|
|
181
|
+
promises.push(query.fetch(options, { cancelRefetch: true }).catch(() => {}));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await Promise.all(promises);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
cancelQueries(filters?: QueryFilters): void {
|
|
189
|
+
const queries = this.queryCache.findAll(filters);
|
|
190
|
+
for (const query of queries) {
|
|
191
|
+
query.cancel();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
removeQueries(filters?: QueryFilters): void {
|
|
196
|
+
const queries = this.queryCache.findAll(filters);
|
|
197
|
+
for (const query of queries) {
|
|
198
|
+
this.queryCache.remove(query);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async resetQueries(filters?: QueryFilters): Promise<void> {
|
|
203
|
+
const queries = this.queryCache.findAll(filters);
|
|
204
|
+
const promises: Promise<any>[] = [];
|
|
205
|
+
|
|
206
|
+
for (const query of queries) {
|
|
207
|
+
query.reset();
|
|
208
|
+
if (query.observers.size > 0) {
|
|
209
|
+
const observer = Array.from(query.observers)[0];
|
|
210
|
+
const options = observer?.options ?? this.defaultOptions.queries ?? {};
|
|
211
|
+
promises.push(query.fetch(options).catch(() => {}));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await Promise.all(promises);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
clear() {
|
|
219
|
+
this.queryCache.clear();
|
|
220
|
+
this.mutationCache.clear();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const QueryClientContext: Context<QueryClient | null> = createContext<QueryClient | null>(
|
|
225
|
+
null
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
let defaultGlobalClient: QueryClient | null = null;
|
|
229
|
+
|
|
230
|
+
function getDefaultGlobalClient(): QueryClient {
|
|
231
|
+
if (!defaultGlobalClient) {
|
|
232
|
+
defaultGlobalClient = new QueryClient();
|
|
233
|
+
}
|
|
234
|
+
return defaultGlobalClient;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function QueryClientProvider(props: { client: QueryClient; children: Child }): any {
|
|
238
|
+
return QueryClientContext.Provider({
|
|
239
|
+
value: props.client,
|
|
240
|
+
children: props.children
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function useQueryClient(client?: QueryClient): QueryClient {
|
|
245
|
+
if (client) return client;
|
|
246
|
+
const ctxClient = useContext(QueryClientContext);
|
|
247
|
+
return ctxClient ?? getDefaultGlobalClient();
|
|
248
|
+
}
|