@fluixi/core 1.0.0-alpha.100

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 (253) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +334 -0
  3. package/dist/cdn/core.global.js +2 -0
  4. package/dist/cli/generate.cjs +5639 -0
  5. package/dist/cli/generate.d.ts +7 -0
  6. package/dist/cli/generate.d.ts.map +1 -0
  7. package/dist/cli/generate.js +2580 -0
  8. package/dist/cli/generate.mjs +5644 -0
  9. package/dist/cli/index.cjs +5639 -0
  10. package/dist/cli/index.d.ts +7 -0
  11. package/dist/cli/index.d.ts.map +1 -0
  12. package/dist/cli/index.js +2580 -0
  13. package/dist/cli/index.mjs +5644 -0
  14. package/dist/cli/run.cjs +64 -0
  15. package/dist/cli/run.d.ts +3 -0
  16. package/dist/cli/run.d.ts.map +1 -0
  17. package/dist/cli/run.js +62 -0
  18. package/dist/cli/run.mjs +40 -0
  19. package/dist/fluixi.d.ts +5 -0
  20. package/dist/src/cdn/chunk-RKBFEFZP.mjs +987 -0
  21. package/dist/src/cdn/core.cjs +1592 -0
  22. package/dist/src/cdn/core.d.ts +16 -0
  23. package/dist/src/cdn/core.d.ts.map +1 -0
  24. package/dist/src/cdn/core.js +15 -0
  25. package/dist/src/cdn/core.mjs +714 -0
  26. package/dist/src/cdn/router-V2THK27H.mjs +74 -0
  27. package/dist/src/index.cjs +1711 -0
  28. package/dist/src/index.d.ts +4 -0
  29. package/dist/src/index.d.ts.map +1 -0
  30. package/dist/src/index.js +11 -0
  31. package/dist/src/index.mjs +1833 -0
  32. package/dist/src/jsx-dev-runtime.cjs +26 -0
  33. package/dist/src/jsx-dev-runtime.d.ts +2 -0
  34. package/dist/src/jsx-dev-runtime.d.ts.map +1 -0
  35. package/dist/src/jsx-dev-runtime.js +1 -0
  36. package/dist/src/jsx-dev-runtime.mjs +24 -0
  37. package/dist/src/jsx-runtime.cjs +26 -0
  38. package/dist/src/jsx-runtime.d.ts +7 -0
  39. package/dist/src/jsx-runtime.d.ts.map +1 -0
  40. package/dist/src/jsx-runtime.js +8 -0
  41. package/dist/src/jsx-runtime.mjs +24 -0
  42. package/dist/src/lib/client/chunk-FDOPDB6Z.mjs +53 -0
  43. package/dist/src/lib/client/index.cjs +1210 -0
  44. package/dist/src/lib/client/index.d.ts +51 -0
  45. package/dist/src/lib/client/index.d.ts.map +1 -0
  46. package/dist/src/lib/client/index.js +174 -0
  47. package/dist/src/lib/client/index.mjs +293 -0
  48. package/dist/src/lib/client/router-4PTBPZLQ.mjs +891 -0
  49. package/dist/src/lib/context/context.cjs +81 -0
  50. package/dist/src/lib/context/context.d.ts +73 -0
  51. package/dist/src/lib/context/context.d.ts.map +1 -0
  52. package/dist/src/lib/context/context.js +93 -0
  53. package/dist/src/lib/context/context.mjs +64 -0
  54. package/dist/src/lib/context/index.cjs +83 -0
  55. package/dist/src/lib/context/index.d.ts +3 -0
  56. package/dist/src/lib/context/index.d.ts.map +1 -0
  57. package/dist/src/lib/context/index.js +1 -0
  58. package/dist/src/lib/context/index.mjs +64 -0
  59. package/dist/src/lib/control-flow.cjs +35 -0
  60. package/dist/src/lib/control-flow.d.ts +4 -0
  61. package/dist/src/lib/control-flow.d.ts.map +1 -0
  62. package/dist/src/lib/control-flow.js +1 -0
  63. package/dist/src/lib/control-flow.mjs +25 -0
  64. package/dist/src/lib/core.cjs +1710 -0
  65. package/dist/src/lib/core.d.ts +91 -0
  66. package/dist/src/lib/core.d.ts.map +1 -0
  67. package/dist/src/lib/core.js +158 -0
  68. package/dist/src/lib/core.mjs +1833 -0
  69. package/dist/src/lib/env/index.cjs +44 -0
  70. package/dist/src/lib/env/index.d.ts +26 -0
  71. package/dist/src/lib/env/index.d.ts.map +1 -0
  72. package/dist/src/lib/env/index.js +31 -0
  73. package/dist/src/lib/env/index.mjs +23 -0
  74. package/dist/src/lib/i18n/index.cjs +123 -0
  75. package/dist/src/lib/i18n/index.d.ts +110 -0
  76. package/dist/src/lib/i18n/index.d.ts.map +1 -0
  77. package/dist/src/lib/i18n/index.js +164 -0
  78. package/dist/src/lib/i18n/index.mjs +103 -0
  79. package/dist/src/lib/index.cjs +598 -0
  80. package/dist/src/lib/index.d.ts +81 -0
  81. package/dist/src/lib/index.d.ts.map +1 -0
  82. package/dist/src/lib/index.js +130 -0
  83. package/dist/src/lib/index.mjs +688 -0
  84. package/dist/src/lib/isomorphic.cjs +90 -0
  85. package/dist/src/lib/isomorphic.d.ts +114 -0
  86. package/dist/src/lib/isomorphic.d.ts.map +1 -0
  87. package/dist/src/lib/isomorphic.js +132 -0
  88. package/dist/src/lib/isomorphic.mjs +69 -0
  89. package/dist/src/lib/jsx-runtime/index.cjs +21 -0
  90. package/dist/src/lib/jsx-runtime/index.d.ts +13 -0
  91. package/dist/src/lib/jsx-runtime/index.d.ts.map +1 -0
  92. package/dist/src/lib/jsx-runtime/index.js +13 -0
  93. package/dist/src/lib/jsx-runtime/index.mjs +4 -0
  94. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.cjs +21 -0
  95. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.d.ts +2 -0
  96. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.d.ts.map +1 -0
  97. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.js +1 -0
  98. package/dist/src/lib/jsx-runtime/jsx-dev-runtime.mjs +4 -0
  99. package/dist/src/lib/plugins/fluixi-routes-plugin.cjs +302 -0
  100. package/dist/src/lib/plugins/fluixi-routes-plugin.d.ts +35 -0
  101. package/dist/src/lib/plugins/fluixi-routes-plugin.d.ts.map +1 -0
  102. package/dist/src/lib/plugins/fluixi-routes-plugin.js +118 -0
  103. package/dist/src/lib/plugins/fluixi-routes-plugin.mjs +271 -0
  104. package/dist/src/lib/plugins/index.cjs +311 -0
  105. package/dist/src/lib/plugins/index.d.ts +6 -0
  106. package/dist/src/lib/plugins/index.d.ts.map +1 -0
  107. package/dist/src/lib/plugins/index.js +7 -0
  108. package/dist/src/lib/plugins/index.mjs +279 -0
  109. package/dist/src/lib/plugins/route-codegen.cjs +73 -0
  110. package/dist/src/lib/plugins/route-codegen.d.ts +12 -0
  111. package/dist/src/lib/plugins/route-codegen.d.ts.map +1 -0
  112. package/dist/src/lib/plugins/route-codegen.js +80 -0
  113. package/dist/src/lib/plugins/route-codegen.mjs +52 -0
  114. package/dist/src/lib/plugins/route-scanner.cjs +189 -0
  115. package/dist/src/lib/plugins/route-scanner.d.ts +38 -0
  116. package/dist/src/lib/plugins/route-scanner.d.ts.map +1 -0
  117. package/dist/src/lib/plugins/route-scanner.js +215 -0
  118. package/dist/src/lib/plugins/route-scanner.mjs +158 -0
  119. package/dist/src/lib/plugins/vite.cjs +27 -0
  120. package/dist/src/lib/plugins/vite.d.ts +3 -0
  121. package/dist/src/lib/plugins/vite.d.ts.map +1 -0
  122. package/dist/src/lib/plugins/vite.js +1 -0
  123. package/dist/src/lib/plugins/vite.mjs +6 -0
  124. package/dist/src/lib/render/await.cjs +38 -0
  125. package/dist/src/lib/render/await.d.ts +26 -0
  126. package/dist/src/lib/render/await.d.ts.map +1 -0
  127. package/dist/src/lib/render/await.js +45 -0
  128. package/dist/src/lib/render/await.mjs +17 -0
  129. package/dist/src/lib/render/component.cjs +221 -0
  130. package/dist/src/lib/render/component.d.ts +296 -0
  131. package/dist/src/lib/render/component.d.ts.map +1 -0
  132. package/dist/src/lib/render/component.js +514 -0
  133. package/dist/src/lib/render/component.mjs +214 -0
  134. package/dist/src/lib/render/deferred.cjs +175 -0
  135. package/dist/src/lib/render/deferred.d.ts +36 -0
  136. package/dist/src/lib/render/deferred.d.ts.map +1 -0
  137. package/dist/src/lib/render/deferred.js +141 -0
  138. package/dist/src/lib/render/deferred.mjs +158 -0
  139. package/dist/src/lib/render/index.cjs +573 -0
  140. package/dist/src/lib/render/index.d.ts +98 -0
  141. package/dist/src/lib/render/index.d.ts.map +1 -0
  142. package/dist/src/lib/render/index.js +180 -0
  143. package/dist/src/lib/render/index.mjs +590 -0
  144. package/dist/src/lib/render/lazy.cjs +67 -0
  145. package/dist/src/lib/render/lazy.d.ts +49 -0
  146. package/dist/src/lib/render/lazy.d.ts.map +1 -0
  147. package/dist/src/lib/render/lazy.js +105 -0
  148. package/dist/src/lib/render/lazy.mjs +50 -0
  149. package/dist/src/lib/render/suspense.cjs +141 -0
  150. package/dist/src/lib/render/suspense.d.ts +65 -0
  151. package/dist/src/lib/render/suspense.d.ts.map +1 -0
  152. package/dist/src/lib/render/suspense.js +196 -0
  153. package/dist/src/lib/render/suspense.mjs +136 -0
  154. package/dist/src/lib/render/versions.cjs +35 -0
  155. package/dist/src/lib/render/versions.d.ts +2 -0
  156. package/dist/src/lib/render/versions.d.ts.map +1 -0
  157. package/dist/src/lib/render/versions.js +21 -0
  158. package/dist/src/lib/render/versions.mjs +14 -0
  159. package/dist/src/lib/router/data.cjs +228 -0
  160. package/dist/src/lib/router/data.d.ts +142 -0
  161. package/dist/src/lib/router/data.d.ts.map +1 -0
  162. package/dist/src/lib/router/data.js +258 -0
  163. package/dist/src/lib/router/data.mjs +214 -0
  164. package/dist/src/lib/router/index.cjs +921 -0
  165. package/dist/src/lib/router/index.d.ts +274 -0
  166. package/dist/src/lib/router/index.d.ts.map +1 -0
  167. package/dist/src/lib/router/index.js +904 -0
  168. package/dist/src/lib/router/index.mjs +938 -0
  169. package/dist/src/lib/router/scroll.cjs +79 -0
  170. package/dist/src/lib/router/scroll.d.ts +19 -0
  171. package/dist/src/lib/router/scroll.d.ts.map +1 -0
  172. package/dist/src/lib/router/scroll.js +68 -0
  173. package/dist/src/lib/router/scroll.mjs +58 -0
  174. package/dist/src/lib/router/transition.cjs +75 -0
  175. package/dist/src/lib/router/transition.d.ts +81 -0
  176. package/dist/src/lib/router/transition.d.ts.map +1 -0
  177. package/dist/src/lib/router/transition.js +136 -0
  178. package/dist/src/lib/router/transition.mjs +54 -0
  179. package/dist/src/lib/server/hydration.cjs +169 -0
  180. package/dist/src/lib/server/hydration.d.ts +198 -0
  181. package/dist/src/lib/server/hydration.d.ts.map +1 -0
  182. package/dist/src/lib/server/hydration.js +345 -0
  183. package/dist/src/lib/server/hydration.mjs +148 -0
  184. package/dist/src/lib/server/index.cjs +406 -0
  185. package/dist/src/lib/server/index.d.ts +115 -0
  186. package/dist/src/lib/server/index.d.ts.map +1 -0
  187. package/dist/src/lib/server/index.js +164 -0
  188. package/dist/src/lib/server/index.mjs +384 -0
  189. package/dist/src/lib/server/reactive.cjs +197 -0
  190. package/dist/src/lib/server/reactive.d.ts +239 -0
  191. package/dist/src/lib/server/reactive.d.ts.map +1 -0
  192. package/dist/src/lib/server/reactive.js +413 -0
  193. package/dist/src/lib/server/reactive.mjs +176 -0
  194. package/dist/src/lib/storage/cache.cjs +51 -0
  195. package/dist/src/lib/storage/cache.d.ts +18 -0
  196. package/dist/src/lib/storage/cache.d.ts.map +1 -0
  197. package/dist/src/lib/storage/cache.js +42 -0
  198. package/dist/src/lib/storage/cache.mjs +30 -0
  199. package/dist/src/lib/storage/index.cjs +246 -0
  200. package/dist/src/lib/storage/index.d.ts +83 -0
  201. package/dist/src/lib/storage/index.d.ts.map +1 -0
  202. package/dist/src/lib/storage/index.js +179 -0
  203. package/dist/src/lib/storage/index.mjs +231 -0
  204. package/dist/src/lib/storage/persisted.cjs +91 -0
  205. package/dist/src/lib/storage/persisted.d.ts +53 -0
  206. package/dist/src/lib/storage/persisted.d.ts.map +1 -0
  207. package/dist/src/lib/storage/persisted.js +75 -0
  208. package/dist/src/lib/storage/persisted.mjs +70 -0
  209. package/dist/src/lib/theme/color-scheme.cjs +281 -0
  210. package/dist/src/lib/theme/color-scheme.d.ts +61 -0
  211. package/dist/src/lib/theme/color-scheme.d.ts.map +1 -0
  212. package/dist/src/lib/theme/color-scheme.js +120 -0
  213. package/dist/src/lib/theme/color-scheme.mjs +265 -0
  214. package/dist/src/lib/theme/index.cjs +267 -0
  215. package/dist/src/lib/theme/index.d.ts +78 -0
  216. package/dist/src/lib/theme/index.d.ts.map +1 -0
  217. package/dist/src/lib/theme/index.js +135 -0
  218. package/dist/src/lib/theme/index.mjs +251 -0
  219. package/dist/src/lib/theme/provider.cjs +346 -0
  220. package/dist/src/lib/theme/provider.d.ts +34 -0
  221. package/dist/src/lib/theme/provider.d.ts.map +1 -0
  222. package/dist/src/lib/theme/provider.js +60 -0
  223. package/dist/src/lib/theme/provider.mjs +332 -0
  224. package/dist/src/lib/utils/index.cjs +209 -0
  225. package/dist/src/lib/utils/index.d.ts +286 -0
  226. package/dist/src/lib/utils/index.d.ts.map +1 -0
  227. package/dist/src/lib/utils/index.js +434 -0
  228. package/dist/src/lib/utils/index.mjs +188 -0
  229. package/dist/src/routes.cjs +37 -0
  230. package/dist/src/routes.d.ts +22 -0
  231. package/dist/src/routes.d.ts.map +1 -0
  232. package/dist/src/routes.js +20 -0
  233. package/dist/src/routes.mjs +17 -0
  234. package/dist/src/server.cjs +405 -0
  235. package/dist/src/server.d.ts +63 -0
  236. package/dist/src/server.d.ts.map +1 -0
  237. package/dist/src/server.js +62 -0
  238. package/dist/src/server.mjs +382 -0
  239. package/dist/src/utils.cjs +21 -0
  240. package/dist/src/utils.d.ts +10 -0
  241. package/dist/src/utils.d.ts.map +1 -0
  242. package/dist/src/utils.js +9 -0
  243. package/dist/src/utils.mjs +4 -0
  244. package/dist/src/version.generated.cjs +26 -0
  245. package/dist/src/version.generated.d.ts +3 -0
  246. package/dist/src/version.generated.d.ts.map +1 -0
  247. package/dist/src/version.generated.js +3 -0
  248. package/dist/src/version.generated.mjs +5 -0
  249. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  250. package/package.json +164 -0
  251. package/src/index.ts +19 -0
  252. package/types/fluixi.d.ts +5 -0
  253. package/types/vinxi.d.ts +50 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * @module @fluixi/core/router/data
3
+ */
4
+ import { createResource, createSignal } from '@fluixi/reactive/signal';
5
+ import { currentResourceCache } from '@fluixi/reactive/signal';
6
+ import { useContext, createContext } from '../context/context.js';
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
+ };
18
+ /**
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.
24
+ *
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
+ * ```
32
+ */
33
+ export function cache(fn, name, options = {}) {
34
+ const { ttl = Infinity, staleWhileRevalidate = true } = options;
35
+ return (...args) => {
36
+ const store = options.cache ?? currentResourceCache();
37
+ const key = name + ':' + JSON.stringify(args);
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);
60
+ }
61
+ return fetchFresh();
62
+ };
63
+ }
64
+ /**
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.
68
+ */
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 */
94
+ }
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);
112
+ // ============================================================================
113
+ // ASYNC DATA PRIMITIVES
114
+ // ============================================================================
115
+ /**
116
+ * Create an async resource from a promise or cache function
117
+ */
118
+ export function createAsync(fetcher, source) {
119
+ const [data] = createResource(source || (() => true), fetcher);
120
+ return data;
121
+ // return () => '' as any;
122
+ }
123
+ // ============================================================================
124
+ // ACTIONS
125
+ // ============================================================================
126
+ const submissionState = new Map();
127
+ function getSubmissionSignal(url) {
128
+ if (!submissionState.has(url)) {
129
+ submissionState.set(url, createSignal(undefined));
130
+ }
131
+ return submissionState.get(url);
132
+ }
133
+ /**
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.
137
+ */
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 = {}) {
145
+ const actionUrl = name || fn.name || 'action';
146
+ const submit = async (...args) => {
147
+ // What the submission shows as its input: the argument, or all of them when there are
148
+ // several. Every argument reaches `fn` either way.
149
+ const input = (args.length > 1 ? args : args[0]);
150
+ const [, setSubmission] = getSubmissionSignal(actionUrl);
151
+ const [, setSubmissions] = getSubmissionsSignal(actionUrl);
152
+ // This call's entry in the list: replaced, never mutated, so readers see each change.
153
+ let entry;
154
+ const drop = () => setSubmissions((list) => list.filter((s) => s !== entry));
155
+ const settle = (next) => {
156
+ setSubmissions((list) => list.map((s) => (s === entry ? next : s)));
157
+ entry = next;
158
+ };
159
+ // Reset state
160
+ const clear = () => {
161
+ setSubmission(undefined);
162
+ drop();
163
+ };
164
+ const retry = () => {
165
+ drop();
166
+ return submit(...args);
167
+ };
168
+ entry = {
169
+ input,
170
+ pending: true,
171
+ clear,
172
+ retry,
173
+ };
174
+ setSubmission(entry);
175
+ setSubmissions((list) => [...list, entry]);
176
+ try {
177
+ const result = await fn(...args);
178
+ setSubmission({
179
+ input,
180
+ result,
181
+ pending: false,
182
+ clear,
183
+ retry,
184
+ });
185
+ // Done: the result is in whatever the page reads now, so the call leaves the list.
186
+ drop();
187
+ const stale = typeof options.invalidates === 'function' ? options.invalidates(result) : options.invalidates;
188
+ for (const key of stale ?? [])
189
+ revalidate(key);
190
+ return result;
191
+ }
192
+ catch (error) {
193
+ const failed = {
194
+ input,
195
+ error,
196
+ pending: false,
197
+ clear,
198
+ retry,
199
+ };
200
+ setSubmission(failed);
201
+ // Failed: it stays, with its error, until retried or cleared.
202
+ settle(failed);
203
+ throw error;
204
+ }
205
+ };
206
+ submit.url = actionUrl;
207
+ submit.with = (args) => action((...a) => fn(args, ...a), name, options);
208
+ return submit;
209
+ }
210
+ /**
211
+ * Hook to track action submission state
212
+ */
213
+ export function useSubmission(actionFn) {
214
+ const [submission] = getSubmissionSignal(actionFn.url);
215
+ return submission;
216
+ }
217
+ /**
218
+ * Every call of `actionFn` still in flight, and the ones that failed, oldest first. One entry
219
+ * per call, so concurrent calls do not overwrite each other. A call that succeeds leaves the
220
+ * list; one that fails stays with its `error` until `retry()` or `clear()`.
221
+ *
222
+ * The optimistic pattern: render the server's rows, then the pending entries mapped to the
223
+ * same shape. When a call succeeds its entry goes and the server row takes its place; when it
224
+ * fails it stops being pending, so it leaves the optimistic rows by itself.
225
+ *
226
+ * @example
227
+ * ```tsx
228
+ * const moves = useSubmissions(moveDeal);
229
+ * const rows = $memo(() => [
230
+ * ...deals(),
231
+ * ...moves().filter((s) => s.pending).map((s) => ({ ...s.input, pending: true })),
232
+ * ]);
233
+ * ```
234
+ */
235
+ export function useSubmissions(actionFn) {
236
+ const [submissions] = getSubmissionsSignal(actionFn.url);
237
+ return submissions;
238
+ }
239
+ /**
240
+ * Hook to get action executor
241
+ */
242
+ export function useAction(actionFn) {
243
+ return actionFn;
244
+ }
245
+ export const RouteDataContext = createContext(undefined, 'route-data');
246
+ export const RouteDataProvider = RouteDataContext.Provider;
247
+ /**
248
+ * Access the data returned by the nearest ancestor route's `routeData` export.
249
+ *
250
+ * Returns a stable accessor function. Call it to get the current route data:
251
+ *
252
+ * const data = useRouteData<WorkspaceRouteData>();
253
+ * createEffect(() => console.log(data()?.workspace()));
254
+ */
255
+ export function useRouteData() {
256
+ const ctx = useContext(RouteDataContext);
257
+ return typeof ctx === 'function' ? ctx : () => ctx;
258
+ }
@@ -0,0 +1,214 @@
1
+ // src/lib/router/data.ts
2
+ import {
3
+ createResource,
4
+ createSignal
5
+ } from "@fluixi/reactive/signal";
6
+ import { currentResourceCache } from "@fluixi/reactive/signal";
7
+
8
+ // src/lib/context/context.ts
9
+ import { asProvider } from "@fluixi/dom";
10
+ import {
11
+ createContext as createReactiveContext,
12
+ getOwner,
13
+ useContext as useReactiveContext
14
+ } from "@fluixi/reactive/signal";
15
+ function createContext(defaultValue, name) {
16
+ const context = createReactiveContext(defaultValue, name);
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({
22
+ value: props.value,
23
+ get children() {
24
+ return props.children;
25
+ },
26
+ debug: props.debug
27
+ })
28
+ );
29
+ return {
30
+ id: context.id,
31
+ Provider: asProvider(Provider),
32
+ defaultValue
33
+ };
34
+ }
35
+ function useContext(context) {
36
+ const value = useReactiveContext(context);
37
+ return value !== void 0 ? value : context.defaultValue;
38
+ }
39
+
40
+ // src/lib/router/data.ts
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();
53
+ const key = name + ":" + JSON.stringify(args);
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;
66
+ }
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();
78
+ };
79
+ }
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 {
95
+ }
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);
108
+ function createAsync(fetcher, source) {
109
+ const [data] = createResource(source || (() => true), fetcher);
110
+ return data;
111
+ }
112
+ var submissionState = /* @__PURE__ */ new Map();
113
+ function getSubmissionSignal(url) {
114
+ if (!submissionState.has(url)) {
115
+ submissionState.set(
116
+ url,
117
+ createSignal(void 0)
118
+ );
119
+ }
120
+ return submissionState.get(url);
121
+ }
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 = {}) {
128
+ const actionUrl = name || fn.name || "action";
129
+ const submit = async (...args) => {
130
+ const input = args.length > 1 ? args : args[0];
131
+ const [, setSubmission] = getSubmissionSignal(actionUrl);
132
+ const [, setSubmissions] = getSubmissionsSignal(actionUrl);
133
+ let entry;
134
+ const drop2 = () => setSubmissions((list) => list.filter((s) => s !== entry));
135
+ const settle = (next) => {
136
+ setSubmissions((list) => list.map((s) => s === entry ? next : s));
137
+ entry = next;
138
+ };
139
+ const clear = () => {
140
+ setSubmission(void 0);
141
+ drop2();
142
+ };
143
+ const retry = () => {
144
+ drop2();
145
+ return submit(...args);
146
+ };
147
+ entry = {
148
+ input,
149
+ pending: true,
150
+ clear,
151
+ retry
152
+ };
153
+ setSubmission(entry);
154
+ setSubmissions((list) => [...list, entry]);
155
+ try {
156
+ const result = await fn(...args);
157
+ setSubmission({
158
+ input,
159
+ result,
160
+ pending: false,
161
+ clear,
162
+ retry
163
+ });
164
+ drop2();
165
+ const stale = typeof options.invalidates === "function" ? options.invalidates(result) : options.invalidates;
166
+ for (const key of stale ?? []) revalidate(key);
167
+ return result;
168
+ } catch (error) {
169
+ const failed = {
170
+ input,
171
+ error,
172
+ pending: false,
173
+ clear,
174
+ retry
175
+ };
176
+ setSubmission(failed);
177
+ settle(failed);
178
+ throw error;
179
+ }
180
+ };
181
+ submit.url = actionUrl;
182
+ submit.with = (args) => action((...a) => fn(args, ...a), name, options);
183
+ return submit;
184
+ }
185
+ function useSubmission(actionFn) {
186
+ const [submission] = getSubmissionSignal(actionFn.url);
187
+ return submission;
188
+ }
189
+ function useSubmissions(actionFn) {
190
+ const [submissions] = getSubmissionsSignal(actionFn.url);
191
+ return submissions;
192
+ }
193
+ function useAction(actionFn) {
194
+ return actionFn;
195
+ }
196
+ var RouteDataContext = createContext(void 0, "route-data");
197
+ var RouteDataProvider = RouteDataContext.Provider;
198
+ function useRouteData() {
199
+ const ctx = useContext(RouteDataContext);
200
+ return typeof ctx === "function" ? ctx : () => ctx;
201
+ }
202
+ export {
203
+ RouteDataContext,
204
+ RouteDataProvider,
205
+ action,
206
+ cache,
207
+ createAsync,
208
+ onRevalidate,
209
+ revalidate,
210
+ useAction,
211
+ useRouteData,
212
+ useSubmission,
213
+ useSubmissions
214
+ };