@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.
Files changed (113) hide show
  1. package/dist/cdn/core.global.js +2 -2
  2. package/dist/src/cdn/{chunk-MDJLC3PE.mjs → chunk-7XM7Q5VR.mjs} +271 -229
  3. package/dist/src/cdn/core.cjs +360 -253
  4. package/dist/src/cdn/core.mjs +92 -20
  5. package/dist/src/cdn/{router-JYFZP3EZ.mjs → router-VOY43YNM.mjs} +7 -3
  6. package/dist/src/index.cjs +547 -232
  7. package/dist/src/index.mjs +547 -223
  8. package/dist/src/lib/client/chunk-QT3RFKH6.mjs +53 -0
  9. package/dist/src/lib/client/index.cjs +310 -176
  10. package/dist/src/lib/client/index.d.ts.map +1 -1
  11. package/dist/src/lib/client/index.js +33 -18
  12. package/dist/src/lib/client/index.mjs +86 -15
  13. package/dist/src/lib/client/{router-PHLUYVQU.mjs → router-ADTS55OP.mjs} +244 -99
  14. package/dist/src/lib/context/context.cjs +35 -72
  15. package/dist/src/lib/context/context.d.ts +45 -291
  16. package/dist/src/lib/context/context.d.ts.map +1 -1
  17. package/dist/src/lib/context/context.js +65 -397
  18. package/dist/src/lib/context/context.mjs +38 -73
  19. package/dist/src/lib/context/index.cjs +35 -72
  20. package/dist/src/lib/context/index.d.ts +2 -2
  21. package/dist/src/lib/context/index.d.ts.map +1 -1
  22. package/dist/src/lib/context/index.js +1 -5
  23. package/dist/src/lib/context/index.mjs +38 -73
  24. package/dist/src/lib/core.cjs +546 -231
  25. package/dist/src/lib/core.d.ts +3 -1
  26. package/dist/src/lib/core.d.ts.map +1 -1
  27. package/dist/src/lib/core.js +5 -3
  28. package/dist/src/lib/core.mjs +546 -222
  29. package/dist/src/lib/env/index.cjs +1 -17
  30. package/dist/src/lib/env/index.d.ts +2 -14
  31. package/dist/src/lib/env/index.d.ts.map +1 -1
  32. package/dist/src/lib/env/index.js +2 -26
  33. package/dist/src/lib/env/index.mjs +1 -17
  34. package/dist/src/lib/i18n/index.cjs +17 -2
  35. package/dist/src/lib/i18n/index.d.ts +10 -0
  36. package/dist/src/lib/i18n/index.d.ts.map +1 -1
  37. package/dist/src/lib/i18n/index.js +17 -1
  38. package/dist/src/lib/i18n/index.mjs +17 -2
  39. package/dist/src/lib/index.cjs +64 -99
  40. package/dist/src/lib/index.mjs +43 -76
  41. package/dist/src/lib/isomorphic.cjs +9 -4
  42. package/dist/src/lib/isomorphic.d.ts +9 -2
  43. package/dist/src/lib/isomorphic.d.ts.map +1 -1
  44. package/dist/src/lib/isomorphic.js +11 -4
  45. package/dist/src/lib/isomorphic.mjs +9 -4
  46. package/dist/src/lib/jsx-runtime/index.cjs +1 -1
  47. package/dist/src/lib/jsx-runtime/index.mjs +1 -1
  48. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.cjs +1 -1
  49. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.mjs +1 -1
  50. package/dist/src/lib/plugins/fluixi-routes-plugin.cjs +2 -2
  51. package/dist/src/lib/plugins/fluixi-routes-plugin.mjs +2 -2
  52. package/dist/src/lib/plugins/index.cjs +3 -3
  53. package/dist/src/lib/plugins/index.mjs +3 -3
  54. package/dist/src/lib/plugins/route-codegen.cjs +2 -2
  55. package/dist/src/lib/plugins/route-codegen.js +1 -1
  56. package/dist/src/lib/plugins/route-codegen.mjs +2 -2
  57. package/dist/src/lib/render/index.cjs +1 -1
  58. package/dist/src/lib/render/index.mjs +1 -1
  59. package/dist/src/lib/render/versions.cjs +1 -1
  60. package/dist/src/lib/render/versions.mjs +1 -1
  61. package/dist/src/lib/router/data.cjs +113 -115
  62. package/dist/src/lib/router/data.d.ts +63 -10
  63. package/dist/src/lib/router/data.d.ts.map +1 -1
  64. package/dist/src/lib/router/data.js +158 -68
  65. package/dist/src/lib/router/data.mjs +112 -112
  66. package/dist/src/lib/router/index.cjs +260 -163
  67. package/dist/src/lib/router/index.d.ts +22 -2
  68. package/dist/src/lib/router/index.d.ts.map +1 -1
  69. package/dist/src/lib/router/index.js +165 -22
  70. package/dist/src/lib/router/index.mjs +244 -143
  71. package/dist/src/lib/server/index.cjs +1 -1
  72. package/dist/src/lib/server/index.mjs +1 -1
  73. package/dist/src/lib/storage/cache.cjs +51 -0
  74. package/dist/src/lib/storage/cache.d.ts +18 -0
  75. package/dist/src/lib/storage/cache.d.ts.map +1 -0
  76. package/dist/src/lib/storage/cache.js +42 -0
  77. package/dist/src/lib/storage/cache.mjs +30 -0
  78. package/dist/src/lib/storage/index.cjs +246 -0
  79. package/dist/src/lib/storage/index.d.ts +83 -0
  80. package/dist/src/lib/storage/index.d.ts.map +1 -0
  81. package/dist/src/lib/storage/index.js +179 -0
  82. package/dist/src/lib/storage/index.mjs +231 -0
  83. package/dist/src/lib/storage/persisted.cjs +91 -0
  84. package/dist/src/lib/storage/persisted.d.ts +53 -0
  85. package/dist/src/lib/storage/persisted.d.ts.map +1 -0
  86. package/dist/src/lib/storage/persisted.js +75 -0
  87. package/dist/src/lib/storage/persisted.mjs +70 -0
  88. package/dist/src/lib/theme/color-scheme.cjs +281 -0
  89. package/dist/src/lib/theme/color-scheme.d.ts +61 -0
  90. package/dist/src/lib/theme/color-scheme.d.ts.map +1 -0
  91. package/dist/src/lib/theme/color-scheme.js +120 -0
  92. package/dist/src/lib/theme/color-scheme.mjs +265 -0
  93. package/dist/src/lib/theme/index.cjs +194 -37
  94. package/dist/src/lib/theme/index.d.ts +23 -11
  95. package/dist/src/lib/theme/index.d.ts.map +1 -1
  96. package/dist/src/lib/theme/index.js +73 -32
  97. package/dist/src/lib/theme/index.mjs +197 -35
  98. package/dist/src/lib/theme/provider.cjs +346 -0
  99. package/dist/src/lib/theme/provider.d.ts +34 -0
  100. package/dist/src/lib/theme/provider.d.ts.map +1 -0
  101. package/dist/src/lib/theme/provider.js +60 -0
  102. package/dist/src/lib/theme/provider.mjs +332 -0
  103. package/dist/src/routes.cjs +1 -1
  104. package/dist/src/routes.mjs +1 -1
  105. package/dist/src/utils.cjs +1 -1
  106. package/dist/src/utils.mjs +1 -1
  107. package/dist/src/version.generated.cjs +1 -1
  108. package/dist/src/version.generated.d.ts +1 -1
  109. package/dist/src/version.generated.js +1 -1
  110. package/dist/src/version.generated.mjs +1 -1
  111. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  112. package/package.json +18 -11
  113. 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 { getHydrationData, setHydrationData, isServerEnvironment, isHydrating, } from '../server/hydration.js';
5
+ import { currentResourceCache } from '@fluixi/reactive/signal';
6
6
  import { useContext, createContext } from '../context/context.js';
7
- // ============================================================================
8
- // CACHE (QUERY) SYSTEM
9
- // ============================================================================
10
- const globalCache = new Map();
11
- const inflightRequests = new Map();
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
- * Cache an async function for data fetching
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
- * Supports:
16
- * - Deduplication
17
- * - Hydration (Server -> Client)
18
- * - Revalidation
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
- return async (...args) => {
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
- // 1. Check Hydration Data (Client-side hydration)
24
- if (isHydrating() || typeof window !== 'undefined') {
25
- const hydrated = getHydrationData(key);
26
- if (hydrated !== undefined) {
27
- return hydrated;
28
- }
29
- }
30
- // 2. Check Memory Cache
31
- if (globalCache.has(key)) {
32
- return globalCache.get(key);
33
- }
34
- // 3. Request Deduplication
35
- if (inflightRequests.has(key)) {
36
- return inflightRequests.get(key);
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
- // 4. Execute
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
- * Global revalidation signal. Tracked by createAsync so that
57
- * when a key is revalidated, matching resources re-fetch.
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
- const [revalidationVersion, setRevalidationVersion] = createSignal(0);
60
- const revalidationKeys = new Set();
61
- /**
62
- * Revalidate a cache key. Clears the memory cache and triggers
63
- * reactive invalidation so resources using createAsync re-fetch.
64
- */
65
- export function revalidate(key) {
66
- const predicate = typeof key === 'string' ? (k) => k.startsWith(key) : key;
67
- for (const k of globalCache.keys()) {
68
- if (predicate(k)) {
69
- globalCache.delete(k);
70
- revalidationKeys.add(k);
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
- * Create a server action
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
- export function action(fn, name) {
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 = () => setSubmission(undefined);
106
- const retry = () => submit(input);
107
- setSubmission({
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
- setSubmission({
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
- readChildren
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
- if (props.debug === true) {
67
- }
68
- return context.Provider({
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 globalCache = /* @__PURE__ */ new Map();
93
- var inflightRequests = /* @__PURE__ */ new Map();
94
- function cache(fn, name) {
95
- return async (...args) => {
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
- if (isHydrating() || typeof window !== "undefined") {
98
- const hydrated = getHydrationData(key);
99
- if (hydrated !== void 0) {
100
- return hydrated;
101
- }
102
- }
103
- if (globalCache.has(key)) {
104
- return globalCache.get(key);
105
- }
106
- if (inflightRequests.has(key)) {
107
- return inflightRequests.get(key);
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
- return val;
117
- },
118
- (err) => {
119
- inflightRequests.delete(key);
120
- throw err;
121
- }
122
- );
123
- inflightRequests.set(key, promise);
124
- return promise;
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
- var [revalidationVersion, setRevalidationVersion] = createSignal(0);
128
- var revalidationKeys = /* @__PURE__ */ new Set();
129
- function revalidate(key) {
130
- const predicate = typeof key === "string" ? (k) => k.startsWith(key) : key;
131
- for (const k of globalCache.keys()) {
132
- if (predicate(k)) {
133
- globalCache.delete(k);
134
- revalidationKeys.add(k);
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
- function action(fn, name) {
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 clear = () => setSubmission(void 0);
158
- const retry = () => submit(input);
159
- setSubmission({
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
- setSubmission({
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
  };