create-aziz-app 3.1.2 → 3.1.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/README.md +658 -207
- package/bin/create-aziz-app.js +302 -302
- package/package.json +1 -1
- package/template/README.md +402 -402
- package/template/_framework/api.js +219 -219
- package/template/_framework/boot.js +33 -33
- package/template/_framework/hooks.js +367 -291
- package/template/_framework/next-response.js +128 -128
- package/template/_framework/route-match.js +145 -145
- package/template/_framework/router.js +465 -259
- package/template/_framework/runtime.js +264 -264
- package/template/app/layout.js +18 -18
- package/template/app/page.js +33 -33
- package/template/docs/01-getting-started.md +151 -151
- package/template/docs/02-project-structure.md +109 -109
- package/template/docs/03-pages-and-layouts.md +140 -140
- package/template/docs/04-routing.md +120 -120
- package/template/docs/05-hooks.md +169 -169
- package/template/docs/06-api-routes.md +210 -210
- package/template/docs/07-api-client.md +151 -151
- package/template/docs/08-styling.md +94 -94
- package/template/docs/09-imports-and-modules.md +97 -97
- package/template/docs/10-database-and-models.md +154 -154
- package/template/docs/11-authentication.md +171 -171
- package/template/docs/12-production-and-deployment.md +121 -121
- package/template/docs/13-internals.md +179 -179
- package/template/docs/README.md +67 -67
- package/template/index.html +20 -20
- package/template/package.json +16 -16
- package/template/scripts/dev-server.js +493 -381
- package/template/scripts/lib/api-router.js +525 -525
- package/template/scripts/lib/js-transform.js +213 -195
- package/template/scripts/lib/module-resolver.js +132 -132
|
@@ -1,291 +1,367 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Aziz.js Hooks
|
|
3
|
-
// React-identical hooks: useState, useEffect, useMemo,
|
|
4
|
-
// useCallback, useRef, useReducer, useContext, useFetch
|
|
5
|
-
// ============================================================
|
|
6
|
-
|
|
7
|
-
let currentFiber = null; // component instance currently rendering
|
|
8
|
-
let hookIndex = 0; // current hook slot
|
|
9
|
-
|
|
10
|
-
// Called by the runtime around each component render.
|
|
11
|
-
export function prepareHooks(fiber) {
|
|
12
|
-
currentFiber = fiber;
|
|
13
|
-
hookIndex = 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function finishHooks() {
|
|
17
|
-
currentFiber = null;
|
|
18
|
-
hookIndex = 0;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function resetHooks() {
|
|
22
|
-
currentFiber = null;
|
|
23
|
-
hookIndex = 0;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ─── Global registration ─────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
export function initHooks() {
|
|
29
|
-
// Expose all hooks globally (no imports needed in pages)
|
|
30
|
-
window.useState = useState;
|
|
31
|
-
window.useEffect = useEffect;
|
|
32
|
-
window.useMemo = useMemo;
|
|
33
|
-
window.useCallback = useCallback;
|
|
34
|
-
window.useRef = useRef;
|
|
35
|
-
window.useReducer = useReducer;
|
|
36
|
-
window.useContext = useContext;
|
|
37
|
-
window.createContext = createContext;
|
|
38
|
-
window.useLocalStorage = useLocalStorage;
|
|
39
|
-
window.useFetch = useFetch;
|
|
40
|
-
window.useApi = useFetch;
|
|
41
|
-
window.
|
|
42
|
-
window.
|
|
43
|
-
window.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (fiber.mounted
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Aziz.js Hooks
|
|
3
|
+
// React-identical hooks: useState, useEffect, useMemo,
|
|
4
|
+
// useCallback, useRef, useReducer, useContext, useFetch
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
let currentFiber = null; // component instance currently rendering
|
|
8
|
+
let hookIndex = 0; // current hook slot
|
|
9
|
+
|
|
10
|
+
// Called by the runtime around each component render.
|
|
11
|
+
export function prepareHooks(fiber) {
|
|
12
|
+
currentFiber = fiber;
|
|
13
|
+
hookIndex = 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function finishHooks() {
|
|
17
|
+
currentFiber = null;
|
|
18
|
+
hookIndex = 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resetHooks() {
|
|
22
|
+
currentFiber = null;
|
|
23
|
+
hookIndex = 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Global registration ─────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export function initHooks() {
|
|
29
|
+
// Expose all hooks globally (no imports needed in pages)
|
|
30
|
+
window.useState = useState;
|
|
31
|
+
window.useEffect = useEffect;
|
|
32
|
+
window.useMemo = useMemo;
|
|
33
|
+
window.useCallback = useCallback;
|
|
34
|
+
window.useRef = useRef;
|
|
35
|
+
window.useReducer = useReducer;
|
|
36
|
+
window.useContext = useContext;
|
|
37
|
+
window.createContext = createContext;
|
|
38
|
+
window.useLocalStorage = useLocalStorage;
|
|
39
|
+
window.useFetch = useFetch;
|
|
40
|
+
window.useApi = useFetch;
|
|
41
|
+
window.onLoadState = onLoadState;
|
|
42
|
+
window.getPendingLoads = getPendingLoads;
|
|
43
|
+
window.clearFetchCache = clearFetchCache;
|
|
44
|
+
window.resetHooks = resetHooks;
|
|
45
|
+
window.prepareHooks = prepareHooks;
|
|
46
|
+
window.finishHooks = finishHooks;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── useState ────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
export function useState(initialValue) {
|
|
52
|
+
const fiber = currentFiber;
|
|
53
|
+
const idx = hookIndex++;
|
|
54
|
+
|
|
55
|
+
if (!fiber) throw new Error('useState called outside component');
|
|
56
|
+
|
|
57
|
+
if (fiber.hooks[idx] === undefined) {
|
|
58
|
+
fiber.hooks[idx] = {
|
|
59
|
+
value: typeof initialValue === 'function' ? initialValue() : initialValue
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const hook = fiber.hooks[idx];
|
|
64
|
+
|
|
65
|
+
const setState = (next) => {
|
|
66
|
+
if (fiber.mounted === false) return; // component unmounted
|
|
67
|
+
const nextVal = typeof next === 'function' ? next(hook.value) : next;
|
|
68
|
+
if (nextVal !== hook.value) {
|
|
69
|
+
hook.value = nextVal;
|
|
70
|
+
scheduleRender();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return [hook.value, setState];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── useReducer ──────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
export function useReducer(reducer, initialState, init) {
|
|
80
|
+
const [state, setState] = useState(init ? init(initialState) : initialState);
|
|
81
|
+
const dispatch = (action) => setState(prev => reducer(prev, action));
|
|
82
|
+
return [state, dispatch];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── useEffect ───────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export function useEffect(callback, deps) {
|
|
88
|
+
const fiber = currentFiber;
|
|
89
|
+
const idx = hookIndex++;
|
|
90
|
+
|
|
91
|
+
if (!fiber) throw new Error('useEffect called outside component');
|
|
92
|
+
|
|
93
|
+
const prev = fiber.hooks[idx];
|
|
94
|
+
const changed = !prev || !deps || deps.some((d, i) => d !== prev.deps?.[i]);
|
|
95
|
+
|
|
96
|
+
if (changed) {
|
|
97
|
+
queueMicrotask(() => {
|
|
98
|
+
if (fiber.mounted === false) return; // unmounted before flush
|
|
99
|
+
if (prev && typeof prev.cleanup === 'function') prev.cleanup();
|
|
100
|
+
const cleanup = callback();
|
|
101
|
+
if (fiber.mounted !== false) {
|
|
102
|
+
fiber.hooks[idx] = fiber.hooks[idx] || {};
|
|
103
|
+
fiber.hooks[idx].cleanup = typeof cleanup === 'function' ? cleanup : undefined;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (prev) {
|
|
109
|
+
// Preserve deps/cleanup slots on re-render
|
|
110
|
+
prev.deps = deps;
|
|
111
|
+
fiber.hooks[idx] = prev;
|
|
112
|
+
} else {
|
|
113
|
+
fiber.hooks[idx] = { deps, cleanup: undefined };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── useMemo / useCallback ───────────────────────────────────
|
|
118
|
+
|
|
119
|
+
export function useMemo(factory, deps) {
|
|
120
|
+
const fiber = currentFiber;
|
|
121
|
+
const idx = hookIndex++;
|
|
122
|
+
|
|
123
|
+
if (!fiber) throw new Error('useMemo called outside component');
|
|
124
|
+
|
|
125
|
+
const prev = fiber.hooks[idx];
|
|
126
|
+
const changed = !prev || !deps || deps.some((d, i) => d !== prev.deps?.[i]);
|
|
127
|
+
|
|
128
|
+
if (changed) {
|
|
129
|
+
fiber.hooks[idx] = { value: factory(), deps };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return fiber.hooks[idx].value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function useCallback(fn, deps) {
|
|
136
|
+
return useMemo(() => fn, deps);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── useRef ──────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
export function useRef(initial) {
|
|
142
|
+
const fiber = currentFiber;
|
|
143
|
+
const idx = hookIndex++;
|
|
144
|
+
|
|
145
|
+
if (!fiber) throw new Error('useRef called outside component');
|
|
146
|
+
|
|
147
|
+
if (!fiber.hooks[idx]) {
|
|
148
|
+
fiber.hooks[idx] = { current: initial };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return fiber.hooks[idx];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ─── Context ─────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
const ctxStack = []; // [{ ctx, value }] — active during provider children render
|
|
157
|
+
|
|
158
|
+
// Injected by runtime.js to materialize provider children inside the provider scope.
|
|
159
|
+
let materializeChildren = (children) => children;
|
|
160
|
+
|
|
161
|
+
export function setMaterializer(fn) {
|
|
162
|
+
materializeChildren = fn;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function createContext(defaultValue) {
|
|
166
|
+
const ctx = { _defaultValue: defaultValue, Provider: null };
|
|
167
|
+
|
|
168
|
+
function ContextProvider(props) {
|
|
169
|
+
ctxStack.push({ ctx, value: props.value });
|
|
170
|
+
try {
|
|
171
|
+
return materializeChildren(props.children);
|
|
172
|
+
} finally {
|
|
173
|
+
ctxStack.pop();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
ContextProvider.displayName = 'Context.Provider';
|
|
177
|
+
|
|
178
|
+
ctx.Provider = ContextProvider;
|
|
179
|
+
return ctx;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function useContext(ctx) {
|
|
183
|
+
for (let i = ctxStack.length - 1; i >= 0; i--) {
|
|
184
|
+
if (ctxStack[i].ctx === ctx) return ctxStack[i].value;
|
|
185
|
+
}
|
|
186
|
+
return ctx._defaultValue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── useLocalStorage ─────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
export function useLocalStorage(key, initial) {
|
|
192
|
+
const [value, setValue] = useState(() => {
|
|
193
|
+
try {
|
|
194
|
+
const stored = localStorage.getItem(key);
|
|
195
|
+
return stored !== null ? JSON.parse(stored) : initial;
|
|
196
|
+
} catch { return initial; }
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Keep multiple tabs in sync
|
|
200
|
+
useEffect(() => {
|
|
201
|
+
function onStorage(e) {
|
|
202
|
+
if (e.key !== key || e.newValue === null) return;
|
|
203
|
+
try { setValue(JSON.parse(e.newValue)); } catch { /* ignore */ }
|
|
204
|
+
}
|
|
205
|
+
window.addEventListener('storage', onStorage);
|
|
206
|
+
return () => window.removeEventListener('storage', onStorage);
|
|
207
|
+
}, [key]);
|
|
208
|
+
|
|
209
|
+
const set = (next) => {
|
|
210
|
+
const val = typeof next === 'function' ? next(value) : next;
|
|
211
|
+
setValue(val);
|
|
212
|
+
try { localStorage.setItem(key, JSON.stringify(val)); } catch { /* quota */ }
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
return [value, set];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ─── Load tracking ───────────────────────────────────────────
|
|
219
|
+
//
|
|
220
|
+
// A tiny registry of in-flight requests so the router can tell when a route
|
|
221
|
+
// has settled. This is what lets app/<route>/loading.js work without a real
|
|
222
|
+
// Suspense renderer: after boot every page module is already imported, so
|
|
223
|
+
// data fetching is the only genuinely async part of a navigation.
|
|
224
|
+
|
|
225
|
+
let pendingLoads = 0;
|
|
226
|
+
const loadListeners = new Set();
|
|
227
|
+
|
|
228
|
+
function emitLoadState() {
|
|
229
|
+
for (const fn of loadListeners) {
|
|
230
|
+
try { fn(pendingLoads); } catch { /* listener errors must not break fetching */ }
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function beginLoad() {
|
|
235
|
+
pendingLoads++;
|
|
236
|
+
emitLoadState();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function endLoad() {
|
|
240
|
+
pendingLoads = Math.max(0, pendingLoads - 1);
|
|
241
|
+
emitLoadState();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function getPendingLoads() {
|
|
245
|
+
return pendingLoads;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function onLoadState(fn) {
|
|
249
|
+
loadListeners.add(fn);
|
|
250
|
+
fn(pendingLoads);
|
|
251
|
+
return () => loadListeners.delete(fn);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─── Response cache ──────────────────────────────────────────
|
|
255
|
+
//
|
|
256
|
+
// Re-mounting a page (for example swapping loading.js out for it) must not
|
|
257
|
+
// re-hit the network. Entries are keyed by URL and expire quickly.
|
|
258
|
+
|
|
259
|
+
const responseCache = new Map();
|
|
260
|
+
const CACHE_TTL = 5000;
|
|
261
|
+
|
|
262
|
+
export function clearFetchCache() {
|
|
263
|
+
responseCache.clear();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function cacheKey(url, options) {
|
|
267
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
268
|
+
return method === 'GET' ? url : url + '|' + JSON.stringify(options.body || null);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── useFetch ────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
export function useFetch(url, options = {}) {
|
|
274
|
+
const [state, setState] = useState({ data: null, loading: !!url, error: null, status: null });
|
|
275
|
+
const abortRef = useRef(null);
|
|
276
|
+
const [nonce, setNonce] = useState(0);
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (!url) {
|
|
280
|
+
setState({ data: null, loading: false, error: null, status: null });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Cancel any in-flight request from a previous render
|
|
285
|
+
if (abortRef.current) abortRef.current.abort();
|
|
286
|
+
const controller = new AbortController();
|
|
287
|
+
abortRef.current = controller;
|
|
288
|
+
|
|
289
|
+
// Serve a fresh cache entry immediately instead of refetching. This is
|
|
290
|
+
// what makes swapping loading.js out for the page cheap.
|
|
291
|
+
const key = cacheKey(url, options);
|
|
292
|
+
const cached = responseCache.get(key);
|
|
293
|
+
if (cached && Date.now() - cached.at < CACHE_TTL && (options.force || cached.data !== undefined)) {
|
|
294
|
+
setState({ data: cached.data, loading: false, error: null, status: cached.status });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
setState(prev => ({ ...prev, loading: true, error: null }));
|
|
299
|
+
|
|
300
|
+
beginLoad();
|
|
301
|
+
let counted = true;
|
|
302
|
+
const settle = () => {
|
|
303
|
+
if (!counted) return;
|
|
304
|
+
counted = false;
|
|
305
|
+
endLoad();
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
fetch(url, { ...options, signal: controller.signal })
|
|
309
|
+
.then(async (res) => {
|
|
310
|
+
const contentType = (res.headers.get('content-type') || '').toLowerCase();
|
|
311
|
+
let data = null;
|
|
312
|
+
if (res.status !== 204) {
|
|
313
|
+
if (contentType.includes('application/json')) {
|
|
314
|
+
data = await res.json().catch(() => null);
|
|
315
|
+
} else {
|
|
316
|
+
data = await res.text();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!res.ok) {
|
|
320
|
+
const message = (data && (data.error || data.message)) || `HTTP ${res.status}`;
|
|
321
|
+
const err = new Error(typeof message === 'string' ? message : JSON.stringify(message));
|
|
322
|
+
err.status = res.status;
|
|
323
|
+
err.data = data;
|
|
324
|
+
throw err;
|
|
325
|
+
}
|
|
326
|
+
responseCache.set(key, { data, status: res.status, at: Date.now() });
|
|
327
|
+
setState({ data, loading: false, error: null, status: res.status });
|
|
328
|
+
settle();
|
|
329
|
+
})
|
|
330
|
+
.catch((err) => {
|
|
331
|
+
if (err.name === 'AbortError') { settle(); return; }
|
|
332
|
+
setState(prev => ({ ...prev, loading: false, error: err.message, status: err.status || null }));
|
|
333
|
+
settle();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return () => { controller.abort(); settle(); };
|
|
337
|
+
}, [url, nonce]);
|
|
338
|
+
|
|
339
|
+
const refetch = useCallback(() => setNonce(n => n + 1), []);
|
|
340
|
+
|
|
341
|
+
return { ...state, refetch };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Render scheduler ────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
let renderScheduled = false;
|
|
347
|
+
|
|
348
|
+
function scheduleRender() {
|
|
349
|
+
if (renderScheduled) return;
|
|
350
|
+
renderScheduled = true;
|
|
351
|
+
|
|
352
|
+
const run = () => {
|
|
353
|
+
if (!renderScheduled) return;
|
|
354
|
+
renderScheduled = false;
|
|
355
|
+
if (typeof window.__rerenderPage === 'function') {
|
|
356
|
+
window.__rerenderPage();
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
361
|
+
requestAnimationFrame(run);
|
|
362
|
+
// Fallback: rAF is paused in background tabs
|
|
363
|
+
setTimeout(run, 120);
|
|
364
|
+
} else {
|
|
365
|
+
setTimeout(run, 0);
|
|
366
|
+
}
|
|
367
|
+
}
|