@fluixi/core 1.0.0-alpha.92 → 1.0.0-alpha.93
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/dist/cdn/core.global.js +2 -2
- package/dist/src/cdn/{chunk-MDJLC3PE.mjs → chunk-7XM7Q5VR.mjs} +271 -229
- package/dist/src/cdn/core.cjs +360 -253
- package/dist/src/cdn/core.mjs +92 -20
- package/dist/src/cdn/{router-JYFZP3EZ.mjs → router-VOY43YNM.mjs} +7 -3
- package/dist/src/index.cjs +547 -232
- package/dist/src/index.mjs +547 -223
- package/dist/src/lib/client/chunk-QT3RFKH6.mjs +53 -0
- package/dist/src/lib/client/index.cjs +310 -176
- package/dist/src/lib/client/index.d.ts.map +1 -1
- package/dist/src/lib/client/index.js +33 -18
- package/dist/src/lib/client/index.mjs +86 -15
- package/dist/src/lib/client/{router-PHLUYVQU.mjs → router-ADTS55OP.mjs} +244 -99
- package/dist/src/lib/context/context.cjs +35 -72
- package/dist/src/lib/context/context.d.ts +45 -291
- package/dist/src/lib/context/context.d.ts.map +1 -1
- package/dist/src/lib/context/context.js +65 -397
- package/dist/src/lib/context/context.mjs +38 -73
- package/dist/src/lib/context/index.cjs +35 -72
- package/dist/src/lib/context/index.d.ts +2 -2
- package/dist/src/lib/context/index.d.ts.map +1 -1
- package/dist/src/lib/context/index.js +1 -5
- package/dist/src/lib/context/index.mjs +38 -73
- package/dist/src/lib/core.cjs +546 -231
- package/dist/src/lib/core.d.ts +3 -1
- package/dist/src/lib/core.d.ts.map +1 -1
- package/dist/src/lib/core.js +5 -3
- package/dist/src/lib/core.mjs +546 -222
- package/dist/src/lib/env/index.cjs +1 -17
- package/dist/src/lib/env/index.d.ts +2 -14
- package/dist/src/lib/env/index.d.ts.map +1 -1
- package/dist/src/lib/env/index.js +2 -26
- package/dist/src/lib/env/index.mjs +1 -17
- package/dist/src/lib/i18n/index.cjs +17 -2
- package/dist/src/lib/i18n/index.d.ts +10 -0
- package/dist/src/lib/i18n/index.d.ts.map +1 -1
- package/dist/src/lib/i18n/index.js +17 -1
- package/dist/src/lib/i18n/index.mjs +17 -2
- package/dist/src/lib/index.cjs +64 -99
- package/dist/src/lib/index.mjs +43 -76
- package/dist/src/lib/isomorphic.cjs +9 -4
- package/dist/src/lib/isomorphic.d.ts +9 -2
- package/dist/src/lib/isomorphic.d.ts.map +1 -1
- package/dist/src/lib/isomorphic.js +11 -4
- package/dist/src/lib/isomorphic.mjs +9 -4
- package/dist/src/lib/jsx-runtime/index.cjs +1 -1
- package/dist/src/lib/jsx-runtime/index.mjs +1 -1
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.cjs +1 -1
- package/dist/src/lib/jsx-runtime/jsx-dev-runtime.mjs +1 -1
- package/dist/src/lib/plugins/fluixi-routes-plugin.cjs +2 -2
- package/dist/src/lib/plugins/fluixi-routes-plugin.mjs +2 -2
- package/dist/src/lib/plugins/index.cjs +3 -3
- package/dist/src/lib/plugins/index.mjs +3 -3
- package/dist/src/lib/plugins/route-codegen.cjs +2 -2
- package/dist/src/lib/plugins/route-codegen.js +1 -1
- package/dist/src/lib/plugins/route-codegen.mjs +2 -2
- package/dist/src/lib/render/index.cjs +1 -1
- package/dist/src/lib/render/index.mjs +1 -1
- package/dist/src/lib/render/versions.cjs +1 -1
- package/dist/src/lib/render/versions.mjs +1 -1
- package/dist/src/lib/router/data.cjs +113 -115
- package/dist/src/lib/router/data.d.ts +63 -10
- package/dist/src/lib/router/data.d.ts.map +1 -1
- package/dist/src/lib/router/data.js +158 -68
- package/dist/src/lib/router/data.mjs +112 -112
- package/dist/src/lib/router/index.cjs +260 -163
- package/dist/src/lib/router/index.d.ts +22 -2
- package/dist/src/lib/router/index.d.ts.map +1 -1
- package/dist/src/lib/router/index.js +165 -22
- package/dist/src/lib/router/index.mjs +244 -143
- package/dist/src/lib/server/index.cjs +1 -1
- package/dist/src/lib/server/index.mjs +1 -1
- package/dist/src/lib/storage/cache.cjs +51 -0
- package/dist/src/lib/storage/cache.d.ts +18 -0
- package/dist/src/lib/storage/cache.d.ts.map +1 -0
- package/dist/src/lib/storage/cache.js +42 -0
- package/dist/src/lib/storage/cache.mjs +30 -0
- package/dist/src/lib/storage/index.cjs +246 -0
- package/dist/src/lib/storage/index.d.ts +83 -0
- package/dist/src/lib/storage/index.d.ts.map +1 -0
- package/dist/src/lib/storage/index.js +179 -0
- package/dist/src/lib/storage/index.mjs +231 -0
- package/dist/src/lib/storage/persisted.cjs +91 -0
- package/dist/src/lib/storage/persisted.d.ts +53 -0
- package/dist/src/lib/storage/persisted.d.ts.map +1 -0
- package/dist/src/lib/storage/persisted.js +75 -0
- package/dist/src/lib/storage/persisted.mjs +70 -0
- package/dist/src/lib/theme/color-scheme.cjs +281 -0
- package/dist/src/lib/theme/color-scheme.d.ts +61 -0
- package/dist/src/lib/theme/color-scheme.d.ts.map +1 -0
- package/dist/src/lib/theme/color-scheme.js +120 -0
- package/dist/src/lib/theme/color-scheme.mjs +265 -0
- package/dist/src/lib/theme/index.cjs +194 -37
- package/dist/src/lib/theme/index.d.ts +23 -11
- package/dist/src/lib/theme/index.d.ts.map +1 -1
- package/dist/src/lib/theme/index.js +73 -32
- package/dist/src/lib/theme/index.mjs +197 -35
- package/dist/src/lib/theme/provider.cjs +346 -0
- package/dist/src/lib/theme/provider.d.ts +34 -0
- package/dist/src/lib/theme/provider.d.ts.map +1 -0
- package/dist/src/lib/theme/provider.js +60 -0
- package/dist/src/lib/theme/provider.mjs +332 -0
- package/dist/src/routes.cjs +1 -1
- package/dist/src/routes.mjs +1 -1
- package/dist/src/utils.cjs +1 -1
- package/dist/src/utils.mjs +1 -1
- package/dist/src/version.generated.cjs +1 -1
- package/dist/src/version.generated.d.ts +1 -1
- package/dist/src/version.generated.js +1 -1
- package/dist/src/version.generated.mjs +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +18 -11
- package/dist/src/lib/client/chunk-VB567ABU.mjs +0 -133
|
@@ -2,77 +2,113 @@
|
|
|
2
2
|
* @module @fluixi/core/router/data
|
|
3
3
|
*/
|
|
4
4
|
import { createResource, createSignal } from '@fluixi/reactive/signal';
|
|
5
|
-
import {
|
|
5
|
+
import { currentResourceCache } from '@fluixi/reactive/signal';
|
|
6
6
|
import { useContext, createContext } from '../context/context.js';
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
// Mounted route layers: each refetches its data when something is revalidated.
|
|
8
|
+
const revalidationListeners = new Set();
|
|
9
|
+
/** @internal: the router registers each layer's refetch here. */
|
|
10
|
+
export function onRevalidate(listener) {
|
|
11
|
+
revalidationListeners.add(listener);
|
|
12
|
+
return () => void revalidationListeners.delete(listener);
|
|
13
|
+
}
|
|
14
|
+
const reloadMounted = () => {
|
|
15
|
+
for (const listener of [...revalidationListeners])
|
|
16
|
+
listener();
|
|
17
|
+
};
|
|
12
18
|
/**
|
|
13
|
-
*
|
|
19
|
+
* A function whose results are kept by name and arguments: route data that should not be
|
|
20
|
+
* fetched again on every visit.
|
|
21
|
+
*
|
|
22
|
+
* Readers of the same key share one request in flight. Entries live in the request's cache on
|
|
23
|
+
* the server, so visitors never share one, and in the page's cache in a browser.
|
|
14
24
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const organisations = cache(() => api.get('/api/v1/organizations'), 'orgs', { ttl: 30_000 });
|
|
28
|
+
* export const routeData = () => organisations();
|
|
29
|
+
* // after a change:
|
|
30
|
+
* revalidate('orgs');
|
|
31
|
+
* ```
|
|
19
32
|
*/
|
|
20
|
-
export function cache(fn, name) {
|
|
21
|
-
|
|
33
|
+
export function cache(fn, name, options = {}) {
|
|
34
|
+
const { ttl = Infinity, staleWhileRevalidate = true } = options;
|
|
35
|
+
return (...args) => {
|
|
36
|
+
const store = options.cache ?? currentResourceCache();
|
|
22
37
|
const key = name + ':' + JSON.stringify(args);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
38
|
+
const fetchFresh = () => {
|
|
39
|
+
const running = store.inflight.get(key);
|
|
40
|
+
if (running)
|
|
41
|
+
return running;
|
|
42
|
+
const promise = Promise.resolve(fn(...args)).then((value) => {
|
|
43
|
+
store.set(key, { value, timestamp: Date.now() });
|
|
44
|
+
store.inflight.delete(key);
|
|
45
|
+
return value;
|
|
46
|
+
}, (error) => {
|
|
47
|
+
store.inflight.delete(key);
|
|
48
|
+
throw error;
|
|
49
|
+
});
|
|
50
|
+
store.inflight.set(key, promise);
|
|
51
|
+
return promise;
|
|
52
|
+
};
|
|
53
|
+
const hit = store.get(key);
|
|
54
|
+
if (hit && (ttl === Infinity || Date.now() - hit.timestamp <= ttl))
|
|
55
|
+
return Promise.resolve(hit.value);
|
|
56
|
+
if (hit && staleWhileRevalidate) {
|
|
57
|
+
// The page gets the old value now; when the new one lands, the routes load again from it.
|
|
58
|
+
fetchFresh().then(reloadMounted, () => undefined);
|
|
59
|
+
return Promise.resolve(hit.value);
|
|
37
60
|
}
|
|
38
|
-
|
|
39
|
-
const promise = Promise.resolve(fn(...args)).then((val) => {
|
|
40
|
-
globalCache.set(key, val);
|
|
41
|
-
inflightRequests.delete(key);
|
|
42
|
-
// Save for hydration if on server
|
|
43
|
-
if (isServerEnvironment()) {
|
|
44
|
-
setHydrationData(key, val);
|
|
45
|
-
}
|
|
46
|
-
return val;
|
|
47
|
-
}, (err) => {
|
|
48
|
-
inflightRequests.delete(key);
|
|
49
|
-
throw err;
|
|
50
|
-
});
|
|
51
|
-
inflightRequests.set(key, promise);
|
|
52
|
-
return promise;
|
|
61
|
+
return fetchFresh();
|
|
53
62
|
};
|
|
54
63
|
}
|
|
55
64
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
65
|
+
* Drops cached entries, by name (`'orgs'` matches every `orgs:` key), several names, or a
|
|
66
|
+
* predicate, then loads the data of the routes on screen again. What is still cached is
|
|
67
|
+
* served from the cache, so only the dropped entries are fetched.
|
|
58
68
|
*/
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
export function revalidate(key, store) {
|
|
70
|
+
const names = typeof key === 'string' ? [key] : Array.isArray(key) ? key : null;
|
|
71
|
+
drop(names ?? key, store ?? currentResourceCache());
|
|
72
|
+
// The other tabs of this site cache the same data in their own pages. A predicate cannot
|
|
73
|
+
// travel, and a cache passed in by hand is not theirs, so only names on the page cache go.
|
|
74
|
+
if (names && !store)
|
|
75
|
+
tellOtherTabs(names);
|
|
76
|
+
}
|
|
77
|
+
function drop(what, store) {
|
|
78
|
+
const matches = Array.isArray(what) ? (k) => what.some((n) => k === n || k.startsWith(n + ':')) : what;
|
|
79
|
+
for (const k of [...store.keys()])
|
|
80
|
+
if (matches(k))
|
|
81
|
+
store.delete(k);
|
|
82
|
+
reloadMounted();
|
|
83
|
+
}
|
|
84
|
+
// Other tabs hear a write to this key through the `storage` event, which a browser fires in
|
|
85
|
+
// every other tab of the origin and never in the one that wrote. The value changes each time
|
|
86
|
+
// (`at`), or a repeat of the same names would not fire.
|
|
87
|
+
const TABS_KEY = 'fluixi:revalidate';
|
|
88
|
+
function tellOtherTabs(names) {
|
|
89
|
+
try {
|
|
90
|
+
localStorage.setItem(TABS_KEY, JSON.stringify({ names, at: Date.now() + Math.random() }));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* no storage here (server, private mode): the other tabs refresh on their own schedule */
|
|
72
94
|
}
|
|
73
|
-
// Bump version to trigger reactive refresh in createAsync consumers
|
|
74
|
-
setRevalidationVersion((v) => v + 1);
|
|
75
95
|
}
|
|
96
|
+
if (typeof window !== 'undefined') {
|
|
97
|
+
window.addEventListener('storage', (event) => {
|
|
98
|
+
if (event.key !== TABS_KEY || !event.newValue)
|
|
99
|
+
return;
|
|
100
|
+
try {
|
|
101
|
+
const names = JSON.parse(event.newValue).names;
|
|
102
|
+
if (Array.isArray(names) && names.every((n) => typeof n === 'string'))
|
|
103
|
+
drop(names, currentResourceCache());
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* not ours */
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// `@fluixi/api/client` applies `x-fx-revalidate` through this, without importing the router.
|
|
111
|
+
globalThis[Symbol.for('fluixi.revalidate')] = (names) => revalidate(names);
|
|
76
112
|
// ============================================================================
|
|
77
113
|
// ASYNC DATA PRIMITIVES
|
|
78
114
|
// ============================================================================
|
|
@@ -95,21 +131,45 @@ function getSubmissionSignal(url) {
|
|
|
95
131
|
return submissionState.get(url);
|
|
96
132
|
}
|
|
97
133
|
/**
|
|
98
|
-
*
|
|
134
|
+
* Every call of an action, by action name: the ones in flight and the ones that failed.
|
|
135
|
+
* `submissionState` above keeps only the latest call, so two calls at once overwrote each
|
|
136
|
+
* other; this list is what an optimistic UI renders from.
|
|
99
137
|
*/
|
|
100
|
-
|
|
138
|
+
const submissionsState = new Map();
|
|
139
|
+
function getSubmissionsSignal(url) {
|
|
140
|
+
if (!submissionsState.has(url))
|
|
141
|
+
submissionsState.set(url, createSignal([]));
|
|
142
|
+
return submissionsState.get(url);
|
|
143
|
+
}
|
|
144
|
+
export function action(fn, name, options = {}) {
|
|
101
145
|
const actionUrl = name || fn.name || 'action';
|
|
102
146
|
const submit = async (input) => {
|
|
103
147
|
const [, setSubmission] = getSubmissionSignal(actionUrl);
|
|
148
|
+
const [, setSubmissions] = getSubmissionsSignal(actionUrl);
|
|
149
|
+
// This call's entry in the list: replaced, never mutated, so readers see each change.
|
|
150
|
+
let entry;
|
|
151
|
+
const drop = () => setSubmissions((list) => list.filter((s) => s !== entry));
|
|
152
|
+
const settle = (next) => {
|
|
153
|
+
setSubmissions((list) => list.map((s) => (s === entry ? next : s)));
|
|
154
|
+
entry = next;
|
|
155
|
+
};
|
|
104
156
|
// Reset state
|
|
105
|
-
const clear = () =>
|
|
106
|
-
|
|
107
|
-
|
|
157
|
+
const clear = () => {
|
|
158
|
+
setSubmission(undefined);
|
|
159
|
+
drop();
|
|
160
|
+
};
|
|
161
|
+
const retry = () => {
|
|
162
|
+
drop();
|
|
163
|
+
return submit(input);
|
|
164
|
+
};
|
|
165
|
+
entry = {
|
|
108
166
|
input: input, // Approximate type for tracking
|
|
109
167
|
pending: true,
|
|
110
168
|
clear,
|
|
111
169
|
retry,
|
|
112
|
-
}
|
|
170
|
+
};
|
|
171
|
+
setSubmission(entry);
|
|
172
|
+
setSubmissions((list) => [...list, entry]);
|
|
113
173
|
try {
|
|
114
174
|
// Cast to any to handle potential argument mismatch in generic types
|
|
115
175
|
// fn expects tuple Args, input is single value or FormData
|
|
@@ -121,21 +181,29 @@ export function action(fn, name) {
|
|
|
121
181
|
clear,
|
|
122
182
|
retry,
|
|
123
183
|
});
|
|
184
|
+
// Done: the result is in whatever the page reads now, so the call leaves the list.
|
|
185
|
+
drop();
|
|
186
|
+
const stale = typeof options.invalidates === 'function' ? options.invalidates(result) : options.invalidates;
|
|
187
|
+
for (const key of stale ?? [])
|
|
188
|
+
revalidate(key);
|
|
124
189
|
return result;
|
|
125
190
|
}
|
|
126
191
|
catch (error) {
|
|
127
|
-
|
|
192
|
+
const failed = {
|
|
128
193
|
input: input,
|
|
129
194
|
error,
|
|
130
195
|
pending: false,
|
|
131
196
|
clear,
|
|
132
197
|
retry,
|
|
133
|
-
}
|
|
198
|
+
};
|
|
199
|
+
setSubmission(failed);
|
|
200
|
+
// Failed: it stays, with its error, until retried or cleared.
|
|
201
|
+
settle(failed);
|
|
134
202
|
throw error;
|
|
135
203
|
}
|
|
136
204
|
};
|
|
137
205
|
submit.url = actionUrl;
|
|
138
|
-
submit.with = (args) => action((...a) => fn(args, ...a), name);
|
|
206
|
+
submit.with = (args) => action((...a) => fn(args, ...a), name, options);
|
|
139
207
|
return submit;
|
|
140
208
|
}
|
|
141
209
|
/**
|
|
@@ -145,6 +213,28 @@ export function useSubmission(actionFn) {
|
|
|
145
213
|
const [submission] = getSubmissionSignal(actionFn.url);
|
|
146
214
|
return submission;
|
|
147
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Every call of `actionFn` still in flight, and the ones that failed, oldest first. One entry
|
|
218
|
+
* per call, so concurrent calls do not overwrite each other. A call that succeeds leaves the
|
|
219
|
+
* list; one that fails stays with its `error` until `retry()` or `clear()`.
|
|
220
|
+
*
|
|
221
|
+
* The optimistic pattern: render the server's rows, then the pending entries mapped to the
|
|
222
|
+
* same shape. When a call succeeds its entry goes and the server row takes its place; when it
|
|
223
|
+
* fails it stops being pending, so it leaves the optimistic rows by itself.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```tsx
|
|
227
|
+
* const moves = useSubmissions(moveDeal);
|
|
228
|
+
* const rows = $memo(() => [
|
|
229
|
+
* ...deals(),
|
|
230
|
+
* ...moves().filter((s) => s.pending).map((s) => ({ ...s.input, pending: true })),
|
|
231
|
+
* ]);
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export function useSubmissions(actionFn) {
|
|
235
|
+
const [submissions] = getSubmissionsSignal(actionFn.url);
|
|
236
|
+
return submissions;
|
|
237
|
+
}
|
|
148
238
|
/**
|
|
149
239
|
* Hook to get action executor
|
|
150
240
|
*/
|
|
@@ -3,80 +3,29 @@ import {
|
|
|
3
3
|
createResource,
|
|
4
4
|
createSignal
|
|
5
5
|
} from "@fluixi/reactive/signal";
|
|
6
|
-
|
|
7
|
-
// src/lib/server/hydration.ts
|
|
8
|
-
import { isServer as domIsServer } from "@fluixi/dom";
|
|
9
|
-
var hydrating = false;
|
|
10
|
-
var hydrationDataMap = /* @__PURE__ */ new Map();
|
|
11
|
-
function isHydrating() {
|
|
12
|
-
return hydrating;
|
|
13
|
-
}
|
|
14
|
-
function setHydrationData(key, data) {
|
|
15
|
-
hydrationDataMap.set(key, data);
|
|
16
|
-
}
|
|
17
|
-
function getHydrationData(key) {
|
|
18
|
-
return hydrationDataMap.get(key);
|
|
19
|
-
}
|
|
20
|
-
function clearHydrationData() {
|
|
21
|
-
hydrationDataMap.clear();
|
|
22
|
-
}
|
|
23
|
-
function cleanupHydration() {
|
|
24
|
-
if (typeof window === "undefined") {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const script = document.getElementById("__FLUIXI_HYDRATION__");
|
|
28
|
-
if (script) {
|
|
29
|
-
script.remove();
|
|
30
|
-
}
|
|
31
|
-
const walker = document.createTreeWalker(
|
|
32
|
-
document.body,
|
|
33
|
-
NodeFilter.SHOW_COMMENT,
|
|
34
|
-
null
|
|
35
|
-
);
|
|
36
|
-
const comments = [];
|
|
37
|
-
let node;
|
|
38
|
-
while (node = walker.nextNode()) {
|
|
39
|
-
const comment = node;
|
|
40
|
-
const text = comment.nodeValue || "";
|
|
41
|
-
if (text.startsWith("#") || text.startsWith("/")) {
|
|
42
|
-
comments.push(comment);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
comments.forEach((comment) => comment.remove());
|
|
46
|
-
clearHydrationData();
|
|
47
|
-
}
|
|
48
|
-
if (typeof window !== "undefined") {
|
|
49
|
-
window.addEventListener("load", () => {
|
|
50
|
-
setTimeout(cleanupHydration, 100);
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
function isServerEnvironment() {
|
|
54
|
-
return domIsServer();
|
|
55
|
-
}
|
|
6
|
+
import { currentResourceCache } from "@fluixi/reactive/signal";
|
|
56
7
|
|
|
57
8
|
// src/lib/context/context.ts
|
|
58
9
|
import { asProvider } from "@fluixi/dom";
|
|
59
|
-
import { createContext as createReactiveContext, useContext as useReactiveContext } from "@fluixi/reactive/signal";
|
|
60
10
|
import {
|
|
61
|
-
|
|
11
|
+
createContext as createReactiveContext,
|
|
12
|
+
getOwner,
|
|
13
|
+
useContext as useReactiveContext
|
|
62
14
|
} from "@fluixi/reactive/signal";
|
|
63
15
|
function createContext(defaultValue, name) {
|
|
64
16
|
const context = createReactiveContext(defaultValue, name);
|
|
65
|
-
const Provider = (props) =>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
17
|
+
const Provider = (props) => (
|
|
18
|
+
// Children stay a getter: the reactive Provider reads them after stamping the value on
|
|
19
|
+
// its owner. Reading `props.children` here would render them, and any <Outlet/> in them,
|
|
20
|
+
// before the context exists.
|
|
21
|
+
context.Provider({
|
|
69
22
|
value: props.value,
|
|
70
23
|
get children() {
|
|
71
24
|
return props.children;
|
|
72
25
|
},
|
|
73
26
|
debug: props.debug
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
if (typeof Symbol !== "undefined" && Symbol.for) {
|
|
77
|
-
Provider.$$typeof = /* @__PURE__ */ Symbol.for("react.provider");
|
|
78
|
-
}
|
|
79
|
-
Provider.$$typeof = /* @__PURE__ */ Symbol.for("react.provider");
|
|
27
|
+
})
|
|
28
|
+
);
|
|
80
29
|
return {
|
|
81
30
|
id: context.id,
|
|
82
31
|
Provider: asProvider(Provider),
|
|
@@ -89,53 +38,73 @@ function useContext(context) {
|
|
|
89
38
|
}
|
|
90
39
|
|
|
91
40
|
// src/lib/router/data.ts
|
|
92
|
-
var
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return
|
|
41
|
+
var revalidationListeners = /* @__PURE__ */ new Set();
|
|
42
|
+
function onRevalidate(listener) {
|
|
43
|
+
revalidationListeners.add(listener);
|
|
44
|
+
return () => void revalidationListeners.delete(listener);
|
|
45
|
+
}
|
|
46
|
+
var reloadMounted = () => {
|
|
47
|
+
for (const listener of [...revalidationListeners]) listener();
|
|
48
|
+
};
|
|
49
|
+
function cache(fn, name, options = {}) {
|
|
50
|
+
const { ttl = Infinity, staleWhileRevalidate = true } = options;
|
|
51
|
+
return (...args) => {
|
|
52
|
+
const store = options.cache ?? currentResourceCache();
|
|
96
53
|
const key = name + ":" + JSON.stringify(args);
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const promise = Promise.resolve(fn(...args)).then(
|
|
110
|
-
(val) => {
|
|
111
|
-
globalCache.set(key, val);
|
|
112
|
-
inflightRequests.delete(key);
|
|
113
|
-
if (isServerEnvironment()) {
|
|
114
|
-
setHydrationData(key, val);
|
|
54
|
+
const fetchFresh = () => {
|
|
55
|
+
const running = store.inflight.get(key);
|
|
56
|
+
if (running) return running;
|
|
57
|
+
const promise = Promise.resolve(fn(...args)).then(
|
|
58
|
+
(value) => {
|
|
59
|
+
store.set(key, { value, timestamp: Date.now() });
|
|
60
|
+
store.inflight.delete(key);
|
|
61
|
+
return value;
|
|
62
|
+
},
|
|
63
|
+
(error) => {
|
|
64
|
+
store.inflight.delete(key);
|
|
65
|
+
throw error;
|
|
115
66
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
|
|
67
|
+
);
|
|
68
|
+
store.inflight.set(key, promise);
|
|
69
|
+
return promise;
|
|
70
|
+
};
|
|
71
|
+
const hit = store.get(key);
|
|
72
|
+
if (hit && (ttl === Infinity || Date.now() - hit.timestamp <= ttl)) return Promise.resolve(hit.value);
|
|
73
|
+
if (hit && staleWhileRevalidate) {
|
|
74
|
+
fetchFresh().then(reloadMounted, () => void 0);
|
|
75
|
+
return Promise.resolve(hit.value);
|
|
76
|
+
}
|
|
77
|
+
return fetchFresh();
|
|
125
78
|
};
|
|
126
79
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
80
|
+
function revalidate(key, store) {
|
|
81
|
+
const names = typeof key === "string" ? [key] : Array.isArray(key) ? key : null;
|
|
82
|
+
drop(names ?? key, store ?? currentResourceCache());
|
|
83
|
+
if (names && !store) tellOtherTabs(names);
|
|
84
|
+
}
|
|
85
|
+
function drop(what, store) {
|
|
86
|
+
const matches = Array.isArray(what) ? (k) => what.some((n) => k === n || k.startsWith(n + ":")) : what;
|
|
87
|
+
for (const k of [...store.keys()]) if (matches(k)) store.delete(k);
|
|
88
|
+
reloadMounted();
|
|
89
|
+
}
|
|
90
|
+
var TABS_KEY = "fluixi:revalidate";
|
|
91
|
+
function tellOtherTabs(names) {
|
|
92
|
+
try {
|
|
93
|
+
localStorage.setItem(TABS_KEY, JSON.stringify({ names, at: Date.now() + Math.random() }));
|
|
94
|
+
} catch {
|
|
136
95
|
}
|
|
137
|
-
setRevalidationVersion((v) => v + 1);
|
|
138
96
|
}
|
|
97
|
+
if (typeof window !== "undefined") {
|
|
98
|
+
window.addEventListener("storage", (event) => {
|
|
99
|
+
if (event.key !== TABS_KEY || !event.newValue) return;
|
|
100
|
+
try {
|
|
101
|
+
const names = JSON.parse(event.newValue).names;
|
|
102
|
+
if (Array.isArray(names) && names.every((n) => typeof n === "string")) drop(names, currentResourceCache());
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
globalThis[/* @__PURE__ */ Symbol.for("fluixi.revalidate")] = (names) => revalidate(names);
|
|
139
108
|
function createAsync(fetcher, source) {
|
|
140
109
|
const [data] = createResource(source || (() => true), fetcher);
|
|
141
110
|
return data;
|
|
@@ -150,19 +119,39 @@ function getSubmissionSignal(url) {
|
|
|
150
119
|
}
|
|
151
120
|
return submissionState.get(url);
|
|
152
121
|
}
|
|
153
|
-
|
|
122
|
+
var submissionsState = /* @__PURE__ */ new Map();
|
|
123
|
+
function getSubmissionsSignal(url) {
|
|
124
|
+
if (!submissionsState.has(url)) submissionsState.set(url, createSignal([]));
|
|
125
|
+
return submissionsState.get(url);
|
|
126
|
+
}
|
|
127
|
+
function action(fn, name, options = {}) {
|
|
154
128
|
const actionUrl = name || fn.name || "action";
|
|
155
129
|
const submit = async (input) => {
|
|
156
130
|
const [, setSubmission] = getSubmissionSignal(actionUrl);
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
131
|
+
const [, setSubmissions] = getSubmissionsSignal(actionUrl);
|
|
132
|
+
let entry;
|
|
133
|
+
const drop2 = () => setSubmissions((list) => list.filter((s) => s !== entry));
|
|
134
|
+
const settle = (next) => {
|
|
135
|
+
setSubmissions((list) => list.map((s) => s === entry ? next : s));
|
|
136
|
+
entry = next;
|
|
137
|
+
};
|
|
138
|
+
const clear = () => {
|
|
139
|
+
setSubmission(void 0);
|
|
140
|
+
drop2();
|
|
141
|
+
};
|
|
142
|
+
const retry = () => {
|
|
143
|
+
drop2();
|
|
144
|
+
return submit(input);
|
|
145
|
+
};
|
|
146
|
+
entry = {
|
|
160
147
|
input,
|
|
161
148
|
// Approximate type for tracking
|
|
162
149
|
pending: true,
|
|
163
150
|
clear,
|
|
164
151
|
retry
|
|
165
|
-
}
|
|
152
|
+
};
|
|
153
|
+
setSubmission(entry);
|
|
154
|
+
setSubmissions((list) => [...list, entry]);
|
|
166
155
|
try {
|
|
167
156
|
const result = await fn(input);
|
|
168
157
|
setSubmission({
|
|
@@ -172,26 +161,35 @@ function action(fn, name) {
|
|
|
172
161
|
clear,
|
|
173
162
|
retry
|
|
174
163
|
});
|
|
164
|
+
drop2();
|
|
165
|
+
const stale = typeof options.invalidates === "function" ? options.invalidates(result) : options.invalidates;
|
|
166
|
+
for (const key of stale ?? []) revalidate(key);
|
|
175
167
|
return result;
|
|
176
168
|
} catch (error) {
|
|
177
|
-
|
|
169
|
+
const failed = {
|
|
178
170
|
input,
|
|
179
171
|
error,
|
|
180
172
|
pending: false,
|
|
181
173
|
clear,
|
|
182
174
|
retry
|
|
183
|
-
}
|
|
175
|
+
};
|
|
176
|
+
setSubmission(failed);
|
|
177
|
+
settle(failed);
|
|
184
178
|
throw error;
|
|
185
179
|
}
|
|
186
180
|
};
|
|
187
181
|
submit.url = actionUrl;
|
|
188
|
-
submit.with = (args) => action((...a) => fn(args, ...a), name);
|
|
182
|
+
submit.with = (args) => action((...a) => fn(args, ...a), name, options);
|
|
189
183
|
return submit;
|
|
190
184
|
}
|
|
191
185
|
function useSubmission(actionFn) {
|
|
192
186
|
const [submission] = getSubmissionSignal(actionFn.url);
|
|
193
187
|
return submission;
|
|
194
188
|
}
|
|
189
|
+
function useSubmissions(actionFn) {
|
|
190
|
+
const [submissions] = getSubmissionsSignal(actionFn.url);
|
|
191
|
+
return submissions;
|
|
192
|
+
}
|
|
195
193
|
function useAction(actionFn) {
|
|
196
194
|
return actionFn;
|
|
197
195
|
}
|
|
@@ -207,8 +205,10 @@ export {
|
|
|
207
205
|
action,
|
|
208
206
|
cache,
|
|
209
207
|
createAsync,
|
|
208
|
+
onRevalidate,
|
|
210
209
|
revalidate,
|
|
211
210
|
useAction,
|
|
212
211
|
useRouteData,
|
|
213
|
-
useSubmission
|
|
212
|
+
useSubmission,
|
|
213
|
+
useSubmissions
|
|
214
214
|
};
|