@bgub/fig 0.0.1
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/CHANGELOG.md +1 -0
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/data-CHJh_xU9.d.ts +79 -0
- package/dist/data-h46EcMnE.js +37 -0
- package/dist/data-h46EcMnE.js.map +1 -0
- package/dist/data-store-CRnNAT9-.js +611 -0
- package/dist/data-store-CRnNAT9-.js.map +1 -0
- package/dist/data-store-EQOLQsW4.d.ts +32 -0
- package/dist/element-Bh_k9c3N.js +179 -0
- package/dist/element-Bh_k9c3N.js.map +1 -0
- package/dist/element-CCOOi4Ka.d.ts +209 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/internal.d.ts +35 -0
- package/dist/internal.js +200 -0
- package/dist/internal.js.map +1 -0
- package/dist/jsx-runtime.d.ts +16 -0
- package/dist/jsx-runtime.js +23 -0
- package/dist/jsx-runtime.js.map +1 -0
- package/dist/server.browser.d.ts +9 -0
- package/dist/server.browser.js +8 -0
- package/dist/server.browser.js.map +1 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +9 -0
- package/dist/server.js.map +1 -0
- package/dist/transition-Cl6mAPcx.d.ts +96 -0
- package/dist/transition-DEqcImne.js +275 -0
- package/dist/transition-DEqcImne.js.map +1 -0
- package/package.json +64 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Changelog
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Ben Gubler
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# @bgub/fig
|
|
2
|
+
|
|
3
|
+
Core primitives for Fig, a TypeScript re-implementation of React's modern
|
|
4
|
+
component model. Renderers live in `@bgub/fig-dom` and `@bgub/fig-server`.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pnpm add @bgub/fig
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Fig packages are ESM-only, require Node `^20.19.0 || >=22.12.0` for Node
|
|
13
|
+
runtime entry points, and rely on package `exports`. TypeScript projects should
|
|
14
|
+
use `moduleResolution: "bundler"`, `"node16"`, or `"nodenext"`, not the legacy
|
|
15
|
+
`"node"` resolver, so subpaths such as `@bgub/fig/jsx-runtime` resolve with
|
|
16
|
+
their types.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { createRoot, on } from "@bgub/fig-dom";
|
|
22
|
+
import { Suspense, readPromise, useState } from "@bgub/fig";
|
|
23
|
+
|
|
24
|
+
const message = Promise.resolve("Ready");
|
|
25
|
+
|
|
26
|
+
function Message({ value }: { value: Promise<string> }) {
|
|
27
|
+
return <span>{readPromise(value)}</span>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function App() {
|
|
31
|
+
const [count, setCount] = useState(0);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<main>
|
|
35
|
+
<button events={[on("click", () => setCount((value) => value + 1))]}>
|
|
36
|
+
Count {count}
|
|
37
|
+
</button>
|
|
38
|
+
<Suspense fallback={<span>Loading</span>}>
|
|
39
|
+
<Message value={message} />
|
|
40
|
+
</Suspense>
|
|
41
|
+
</main>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const container = document.getElementById("root");
|
|
46
|
+
if (container === null) throw new Error("Missing root.");
|
|
47
|
+
|
|
48
|
+
createRoot(container).render(<App />);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Core API
|
|
52
|
+
|
|
53
|
+
- Elements: `createElement`, `isValidElement`, `Fragment`, and the JSX
|
|
54
|
+
runtime.
|
|
55
|
+
- State and memoization: `useState`, `useDeferredValue`, `useMemo`, and
|
|
56
|
+
`useCallback`.
|
|
57
|
+
- Stable identifiers: `useId()` generates IDs that match server render and
|
|
58
|
+
hydration output.
|
|
59
|
+
- Context: `createContext(defaultValue)` plus render-time
|
|
60
|
+
`readContext(context)`.
|
|
61
|
+
- Effects: `useReactive`, `useBeforePaint`, and `useBeforeLayout`. Effects
|
|
62
|
+
receive an `AbortSignal`; attach cleanup to `signal.abort`. An empty deps
|
|
63
|
+
array runs an effect once per mount.
|
|
64
|
+
- Strict development semantics, with no `StrictMode` component or opt-out:
|
|
65
|
+
development builds render components twice per pass (discarding the first
|
|
66
|
+
result) and run first-time effects and renderer `bind` callbacks twice with
|
|
67
|
+
an abort in between, so impure renders and signal-ignoring cleanup surface
|
|
68
|
+
early. Production builds strip these checks.
|
|
69
|
+
- `useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)` reads external
|
|
70
|
+
stores. Server rendering and hydration require `getServerSnapshot`.
|
|
71
|
+
- `useStableEvent(handler)` returns a stable function for effect-held
|
|
72
|
+
callbacks (subscriptions, sockets, timers). The handler always sees the
|
|
73
|
+
latest committed render and follows the Fig event contract: it receives a
|
|
74
|
+
trailing `AbortSignal`, and the previous invocation's signal aborts on
|
|
75
|
+
re-entry and on unmount. Calls after unmount receive an already-aborted
|
|
76
|
+
signal; calling it during render throws.
|
|
77
|
+
- `Suspense` catches pending `readPromise(promise)` reads and shows `fallback`
|
|
78
|
+
until the promise settles.
|
|
79
|
+
- Data resources live in `@bgub/fig`. Use `dataResource(...)` plus
|
|
80
|
+
render-time `readData(...)` for keyed async values that need Suspense,
|
|
81
|
+
deduping, invalidation, refresh, server hydration, and DevTools visibility.
|
|
82
|
+
- `lazy(load)` creates a component that suspends until `load()` resolves to a
|
|
83
|
+
component type.
|
|
84
|
+
- `<Activity mode="visible" | "hidden">` hides a subtree while preserving its
|
|
85
|
+
state: hiding hides the DOM (through portals) and aborts effects, binds, and
|
|
86
|
+
reactive events; revealing restores the DOM and re-runs them. Trees that
|
|
87
|
+
mount hidden defer their effects until first reveal, and updates inside
|
|
88
|
+
hidden trees prerender at idle priority. Server rendering streams hidden
|
|
89
|
+
content inside an inert template; the client keeps it dehydrated — zero
|
|
90
|
+
hydration cost — until reveal, then adopts the server DOM.
|
|
91
|
+
- `ErrorBoundary` catches render and Fig effect errors. Use `onError` for
|
|
92
|
+
reporting and change the boundary key to reset sticky fallback state. Data
|
|
93
|
+
resource load failures report failed keys on `info.dataResourceKeys`, so
|
|
94
|
+
recovery flows can refresh or invalidate those keys before resetting.
|
|
95
|
+
- `transition(callback)` and `useTransition()` mark updates that may preserve
|
|
96
|
+
already-revealed Suspense content while new work is pending. Updates scheduled
|
|
97
|
+
inside the callback run at transition priority, including updates after an
|
|
98
|
+
`await` while an async transition callback is still pending. If the callback
|
|
99
|
+
returns a promise, `useTransition()` keeps `isPending` true until it settles.
|
|
100
|
+
Server rendering runs transition callbacks immediately and never exposes
|
|
101
|
+
pending state.
|
|
102
|
+
- `useActionState(action, initialState)` matches React's argument order while
|
|
103
|
+
staying client-side in Fig today. Actions receive the previous state first,
|
|
104
|
+
then the runner's arguments, then an `AbortSignal` Fig appends — declare the
|
|
105
|
+
trailing signal parameter (it drives `Args` inference):
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
const [count, add, isPending] = useActionState(
|
|
109
|
+
(previous: number, amount: number, signal: AbortSignal) => {
|
|
110
|
+
return fetchNext(previous + amount, { signal });
|
|
111
|
+
},
|
|
112
|
+
0,
|
|
113
|
+
);
|
|
114
|
+
add(2); // Fig appends the signal; callers pass only the args.
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Runs are last-run-wins: a new run aborts the previous one's signal and
|
|
118
|
+
retires it, so a stale settlement (value or rejection) never touches state,
|
|
119
|
+
error, or pending. The signal also aborts on unmount and Activity hide.
|
|
120
|
+
Server actions can layer on top later without changing the hook shape.
|
|
121
|
+
|
|
122
|
+
- Document resources: `assets([...], children)` attaches resources to a
|
|
123
|
+
subtree while rendering only `children` on the client. Resource helpers include
|
|
124
|
+
`stylesheet`, `preload`, `font`, `preconnect`, `title`, `meta`, and `script`.
|
|
125
|
+
Server rendering exposes `title` and `meta` as document head output; stream-safe
|
|
126
|
+
assets such as stylesheets can be emitted near the segments that depend on
|
|
127
|
+
them.
|
|
128
|
+
|
|
129
|
+
## Renderer APIs
|
|
130
|
+
|
|
131
|
+
Use `@bgub/fig-dom` for browser rendering:
|
|
132
|
+
|
|
133
|
+
- `createRoot(container)` renders client roots.
|
|
134
|
+
- `hydrateRoot(container, children, options?)` hydrates server HTML and reports
|
|
135
|
+
recoverable mismatches with `onRecoverableError`.
|
|
136
|
+
- `createPortal(children, container, key?)` renders into external DOM targets.
|
|
137
|
+
- DOM events use `events={[on("click", (event, signal) => ...)]}`.
|
|
138
|
+
- DOM node access uses `bind={(node, signal) => ...}`.
|
|
139
|
+
- Raw trusted HTML uses `unsafeHTML="<p>trusted html</p>"`.
|
|
140
|
+
|
|
141
|
+
Use `@bgub/fig-server` for streaming server rendering with
|
|
142
|
+
`renderToStream` or `renderToHtml`.
|
|
143
|
+
|
|
144
|
+
## Data Resources
|
|
145
|
+
|
|
146
|
+
Key-addressable async data resources are part of the core package. Data
|
|
147
|
+
resources are render-time inputs with stable keys: Fig dedupes loads by
|
|
148
|
+
key, wires missing values to `Suspense`, tracks committed readers, and
|
|
149
|
+
lets applications invalidate or refresh specific entries and key
|
|
150
|
+
prefixes. The store implementation travels with `dataResource` itself, so
|
|
151
|
+
bundles that never define a resource never ship it.
|
|
152
|
+
|
|
153
|
+
### Basic Usage
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
import { Suspense } from "@bgub/fig";
|
|
157
|
+
import { createRoot, on } from "@bgub/fig-dom";
|
|
158
|
+
import { dataResource, readData, refreshData } from "@bgub/fig";
|
|
159
|
+
|
|
160
|
+
const userResource = dataResource({
|
|
161
|
+
key: (id: string) => ["user", id],
|
|
162
|
+
load: async (id: string, { signal }) => {
|
|
163
|
+
const response = await fetch(`/api/users/${id}`, { signal });
|
|
164
|
+
return response.json() as Promise<{ name: string }>;
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
function Profile({ id }: { id: string }) {
|
|
169
|
+
const user = readData(userResource, id);
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<button events={[on("click", () => void refreshData(userResource, id))]}>
|
|
173
|
+
{user.name}
|
|
174
|
+
</button>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
createRoot(document.getElementById("root")!).render(
|
|
179
|
+
<Suspense fallback={<span>Loading</span>}>
|
|
180
|
+
<Profile id="one" />
|
|
181
|
+
</Suspense>,
|
|
182
|
+
);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`readData(resource, ...args)` is render-only. It returns fulfilled values,
|
|
186
|
+
throws pending loads to `Suspense`, and throws failed initial loads to
|
|
187
|
+
`ErrorBoundary`.
|
|
188
|
+
|
|
189
|
+
### Keys
|
|
190
|
+
|
|
191
|
+
Every resource has a key function. Keys must be arrays whose first item is a
|
|
192
|
+
string namespace:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
key: (id: string, locale: string) => ["user", id, { locale }];
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Key values may contain strings, finite numbers, booleans, `null`, arrays, and
|
|
199
|
+
plain objects. Object keys are canonicalized in sorted order. `undefined`,
|
|
200
|
+
`NaN`, infinities, functions, symbols, dates, maps, sets, promises, class
|
|
201
|
+
instances, and cycles are invalid.
|
|
202
|
+
|
|
203
|
+
The key is the whole cache identity. Fig uses the canonical key for reads,
|
|
204
|
+
dedupe, invalidation, refresh, SSR serialization, hydration, diagnostics, and
|
|
205
|
+
DevTools.
|
|
206
|
+
|
|
207
|
+
### Refresh And Invalidate
|
|
208
|
+
|
|
209
|
+
`invalidateData(resource, ...args)` marks an existing entry stale. If the key is
|
|
210
|
+
currently observed, Fig schedules the readers; the next read keeps showing the
|
|
211
|
+
last fulfilled value and starts a background reload. If nobody is observing the
|
|
212
|
+
key, invalidation is lazy and does not fetch.
|
|
213
|
+
|
|
214
|
+
`invalidateDataKey(key)` applies the same stale/reset semantics to one exact
|
|
215
|
+
structured key. `invalidateDataError(error)` invalidates every exact key Fig
|
|
216
|
+
attributed to a caught data error and returns whether any data keys were found.
|
|
217
|
+
Use it from an `ErrorBoundary` fallback before remounting or resetting the
|
|
218
|
+
boundary.
|
|
219
|
+
|
|
220
|
+
`invalidateDataPrefix(prefix)` marks every existing entry whose structured key
|
|
221
|
+
starts with `prefix` stale. For example, `invalidateDataPrefix(["user"])`
|
|
222
|
+
targets entries like `["user", id]`. Prefix matching uses the same canonical
|
|
223
|
+
key encoder as normal reads, so delimiter-shaped strings do not collide.
|
|
224
|
+
|
|
225
|
+
`refreshData(resource, ...args)` fetches immediately and resolves with a result
|
|
226
|
+
instead of rejecting:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const result = await refreshData(userResource, "one");
|
|
230
|
+
|
|
231
|
+
if (result.status === "fulfilled") {
|
|
232
|
+
console.log(result.value);
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Result statuses are `fulfilled`, `rejected`, `aborted`, and `unsupported`.
|
|
237
|
+
Refresh failures preserve the last fulfilled value when one exists, so visible
|
|
238
|
+
UI does not fall back to Suspense or an error boundary just because a refresh
|
|
239
|
+
failed.
|
|
240
|
+
|
|
241
|
+
### Root Store Scope
|
|
242
|
+
|
|
243
|
+
Fig installs the current root data store while rendering and while running
|
|
244
|
+
Fig-managed callbacks such as delegated event handlers. Outside those scopes,
|
|
245
|
+
bind the store explicitly:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const root = createRoot(container);
|
|
249
|
+
|
|
250
|
+
socket.on("user:update", (id) => {
|
|
251
|
+
root.data.run(() => invalidateData(userResource, id));
|
|
252
|
+
});
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`root.data.run(fn)` is synchronous. If you need to invalidate after an `await`,
|
|
256
|
+
keep the handle and re-enter:
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
const data = root.data;
|
|
260
|
+
await save();
|
|
261
|
+
data.run(() => refreshData(userResource, "one"));
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Client roots accept `dataPartition` and `initialData` options. The partition
|
|
265
|
+
separates a store's internal keyspace without changing public resource keys.
|
|
266
|
+
Loaders receive only their resource arguments plus `{ signal }`; app services,
|
|
267
|
+
request cookies, and auth state belong in the surrounding adapter or closure,
|
|
268
|
+
not in the data store.
|
|
269
|
+
|
|
270
|
+
### Server Values And Hydration
|
|
271
|
+
|
|
272
|
+
Server renderers expose fulfilled data entries with `getData()`. Pass those
|
|
273
|
+
entries to the client root as `initialData` to hydrate by key.
|
|
274
|
+
|
|
275
|
+
For server-only data, split the browser-safe key resource from the server
|
|
276
|
+
loader:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { dataResource } from "@bgub/fig";
|
|
280
|
+
import { serverDataResource } from "@bgub/fig/server";
|
|
281
|
+
|
|
282
|
+
export const userKey = (id: string) => ["user", id];
|
|
283
|
+
|
|
284
|
+
export const userResource = dataResource<[string], User>({
|
|
285
|
+
key: userKey,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
export const userServerResource = serverDataResource({
|
|
289
|
+
key: userKey,
|
|
290
|
+
load: async (id, { signal }) => fetchUser(id, signal),
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Client code imports the loader-less resource. If the server streamed a value
|
|
295
|
+
for the same key, the client can read it as a hydrate-only entry. Since the
|
|
296
|
+
resource has no client loader, `refreshData(userResource, id)` resolves with
|
|
297
|
+
`{ status: "unsupported", reason: "no-client-loader" }`; revalidation needs a
|
|
298
|
+
payload or framework refresh path.
|
|
299
|
+
|
|
300
|
+
Browser bundles need the `figData` transform from `@bgub/fig-vite` so imports of
|
|
301
|
+
`.server.ts(x)` modules become loader-free client stubs instead of bundling the
|
|
302
|
+
server loader. Fig Start includes this transform automatically.
|
|
303
|
+
|
|
304
|
+
Direct client refreshes of server data are a framework feature, not a core
|
|
305
|
+
data one: an endpoint has to exist to serve them. Fig Start provides
|
|
306
|
+
`remoteDataResource` (from `@bgub/fig-start/server`), whose browser import
|
|
307
|
+
compiles into a plain `dataResource` with a loader that calls the framework
|
|
308
|
+
data endpoint. Without a framework, define an isomorphic `dataResource` whose
|
|
309
|
+
loader fetches an endpoint you own. Server route payload navigations do not
|
|
310
|
+
make an extra data request either way — data read during the payload render
|
|
311
|
+
streams in that same payload response.
|
|
312
|
+
|
|
313
|
+
### Activity, Errors, And DevTools
|
|
314
|
+
|
|
315
|
+
Data-resource subscriptions are committed even inside hidden `Activity` trees.
|
|
316
|
+
Invalidating a hidden-only key schedules offscreen prerender work and keeps the
|
|
317
|
+
DOM hidden.
|
|
318
|
+
|
|
319
|
+
Initial load errors are keyed. `ErrorBoundary` reports the failed keys on
|
|
320
|
+
`info.dataResourceKeys`; reset flows can call `invalidateDataError(error)` or
|
|
321
|
+
refresh/invalidate those keys before remounting or changing the boundary key.
|
|
322
|
+
|
|
323
|
+
Fig DevTools root snapshots include data-resource entries with their normalized
|
|
324
|
+
keys, statuses, stale state, subscriber counts, current values, and errors. The
|
|
325
|
+
in-page DevTools exposes them in the `Data` tab.
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
MIT
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/data.d.ts
|
|
2
|
+
type DataResourceKeyInput = string | number | boolean | null | readonly DataResourceKeyInput[] | {
|
|
3
|
+
readonly [key: string]: DataResourceKeyInput;
|
|
4
|
+
};
|
|
5
|
+
type DataResourceKey = readonly [string, ...DataResourceKeyInput[]];
|
|
6
|
+
interface DataResourceLoadContext {
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
interface DataResource<TArgs extends unknown[] = unknown[], TValue = unknown> {
|
|
10
|
+
readonly $$typeof: symbol;
|
|
11
|
+
readonly debugArgs?: (...args: TArgs) => DataResourceKeyInput;
|
|
12
|
+
readonly key: (...args: TArgs) => DataResourceKey;
|
|
13
|
+
readonly load?: (...argsAndContext: [...TArgs, DataResourceLoadContext]) => TValue | PromiseLike<TValue>;
|
|
14
|
+
}
|
|
15
|
+
type DataRefreshResult<T> = {
|
|
16
|
+
status: "fulfilled";
|
|
17
|
+
value: T;
|
|
18
|
+
} | {
|
|
19
|
+
status: "rejected";
|
|
20
|
+
error: unknown;
|
|
21
|
+
staleValue?: T;
|
|
22
|
+
} | {
|
|
23
|
+
status: "aborted";
|
|
24
|
+
reason: "superseded" | "store-disposed" | "evicted";
|
|
25
|
+
staleValue?: T;
|
|
26
|
+
} | {
|
|
27
|
+
status: "unsupported";
|
|
28
|
+
reason: "no-client-loader";
|
|
29
|
+
staleValue?: T;
|
|
30
|
+
};
|
|
31
|
+
interface FigDataHydrationEntry {
|
|
32
|
+
key: DataResourceKey;
|
|
33
|
+
value: unknown;
|
|
34
|
+
}
|
|
35
|
+
type FigDataEntryStatus = "pending" | "fulfilled" | "rejected" | "refreshing";
|
|
36
|
+
interface DataStoreEntrySnapshot {
|
|
37
|
+
canonicalKey: string;
|
|
38
|
+
error?: unknown;
|
|
39
|
+
hasValue: boolean;
|
|
40
|
+
key: DataResourceKey;
|
|
41
|
+
pending: boolean;
|
|
42
|
+
refreshError?: unknown;
|
|
43
|
+
stale: boolean;
|
|
44
|
+
status: FigDataEntryStatus;
|
|
45
|
+
subscriberCount: number;
|
|
46
|
+
value?: unknown;
|
|
47
|
+
}
|
|
48
|
+
interface FigDataStoreHandle {
|
|
49
|
+
hydrate(entries: readonly FigDataHydrationEntry[]): void;
|
|
50
|
+
invalidateData<TArgs extends unknown[], TValue>(resource: DataResource<TArgs, TValue>, ...args: TArgs): void;
|
|
51
|
+
invalidateDataError(error: unknown): boolean;
|
|
52
|
+
invalidateDataKey(key: DataResourceKey): void;
|
|
53
|
+
invalidateDataPrefix(prefix: DataResourceKey): void;
|
|
54
|
+
preloadData<TArgs extends unknown[], TValue>(resource: DataResource<TArgs, TValue>, ...args: TArgs): void;
|
|
55
|
+
refreshData<TArgs extends unknown[], TValue>(resource: DataResource<TArgs, TValue>, ...args: TArgs): Promise<DataRefreshResult<TValue>>;
|
|
56
|
+
run<T>(callback: () => T): T;
|
|
57
|
+
}
|
|
58
|
+
interface FigDataStore extends FigDataStoreHandle {
|
|
59
|
+
commitDataDependencies(owner: object, previousOwner: object | null): void;
|
|
60
|
+
deleteDataOwner(owner: object): void;
|
|
61
|
+
releaseDataOwner(owner: object): void;
|
|
62
|
+
resetDataDependencies(owner: object): void;
|
|
63
|
+
dispose(): void;
|
|
64
|
+
inspectDataEntries(): DataStoreEntrySnapshot[];
|
|
65
|
+
snapshot(): FigDataHydrationEntry[];
|
|
66
|
+
readData<TArgs extends unknown[], TValue>(resource: DataResource<TArgs, TValue>, args: TArgs, owner: object): TValue;
|
|
67
|
+
}
|
|
68
|
+
interface FigDataStoreHost {
|
|
69
|
+
getLane(): unknown;
|
|
70
|
+
partition?: DataResourceKeyInput;
|
|
71
|
+
schedule(owner: object, lane: unknown): void;
|
|
72
|
+
}
|
|
73
|
+
type FigDataStoreFactory = (host: FigDataStoreHost) => FigDataStore;
|
|
74
|
+
declare function resolveCurrentDataStore(message?: string): FigDataStore;
|
|
75
|
+
declare function setCurrentDataStore(store: FigDataStore | null): FigDataStore | null;
|
|
76
|
+
declare function markDataResourceError(error: unknown, key: DataResourceKey): void;
|
|
77
|
+
declare function dataResourceKeysForError(error: unknown): DataResourceKey[] | undefined;
|
|
78
|
+
//#endregion
|
|
79
|
+
export { DataResourceLoadContext as a, FigDataHydrationEntry as c, FigDataStoreHandle as d, FigDataStoreHost as f, setCurrentDataStore as g, resolveCurrentDataStore as h, DataResourceKeyInput as i, FigDataStore as l, markDataResourceError as m, DataResource as n, DataStoreEntrySnapshot as o, dataResourceKeysForError as p, DataResourceKey as r, FigDataEntryStatus as s, DataRefreshResult as t, FigDataStoreFactory as u };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
//#region src/data.ts
|
|
2
|
+
const objectDataErrors = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
let currentDataStore = null;
|
|
4
|
+
function resolveCurrentDataStore(message = "Data resource APIs require a Fig data store.") {
|
|
5
|
+
if (currentDataStore === null) throw new Error(message);
|
|
6
|
+
return currentDataStore;
|
|
7
|
+
}
|
|
8
|
+
function setCurrentDataStore(store) {
|
|
9
|
+
const previousStore = currentDataStore;
|
|
10
|
+
currentDataStore = store;
|
|
11
|
+
return previousStore;
|
|
12
|
+
}
|
|
13
|
+
function markDataResourceError(error, key) {
|
|
14
|
+
if (!isObjectKey(error)) return;
|
|
15
|
+
let keys = objectDataErrors.get(error);
|
|
16
|
+
if (keys === void 0) {
|
|
17
|
+
keys = [];
|
|
18
|
+
objectDataErrors.set(error, keys);
|
|
19
|
+
}
|
|
20
|
+
if (keys.some((existing) => sameDataResourceKey(existing, key))) return;
|
|
21
|
+
keys.push(key);
|
|
22
|
+
}
|
|
23
|
+
function dataResourceKeysForError(error) {
|
|
24
|
+
if (!isObjectKey(error)) return void 0;
|
|
25
|
+
const keys = objectDataErrors.get(error);
|
|
26
|
+
return keys === void 0 || keys.length === 0 ? void 0 : [...keys];
|
|
27
|
+
}
|
|
28
|
+
function sameDataResourceKey(a, b) {
|
|
29
|
+
return a.length === b.length && a.every((value, index) => Object.is(value, b[index]));
|
|
30
|
+
}
|
|
31
|
+
function isObjectKey(value) {
|
|
32
|
+
return (typeof value === "object" || typeof value === "function") && value !== null;
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { setCurrentDataStore as i, markDataResourceError as n, resolveCurrentDataStore as r, dataResourceKeysForError as t };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=data-h46EcMnE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-h46EcMnE.js","names":[],"sources":["../src/data.ts"],"sourcesContent":["export type DataResourceKeyInput =\n | string\n | number\n | boolean\n | null\n | readonly DataResourceKeyInput[]\n | { readonly [key: string]: DataResourceKeyInput };\n\nexport type DataResourceKey = readonly [string, ...DataResourceKeyInput[]];\n\nexport interface DataResourceLoadContext {\n signal: AbortSignal;\n}\n\nexport interface DataResource<\n TArgs extends unknown[] = unknown[],\n TValue = unknown,\n> {\n readonly $$typeof: symbol;\n readonly debugArgs?: (...args: TArgs) => DataResourceKeyInput;\n readonly key: (...args: TArgs) => DataResourceKey;\n readonly load?: (\n ...argsAndContext: [...TArgs, DataResourceLoadContext]\n ) => TValue | PromiseLike<TValue>;\n}\n\nexport type DataRefreshResult<T> =\n | { status: \"fulfilled\"; value: T }\n | { status: \"rejected\"; error: unknown; staleValue?: T }\n | {\n status: \"aborted\";\n reason: \"superseded\" | \"store-disposed\" | \"evicted\";\n staleValue?: T;\n }\n | {\n status: \"unsupported\";\n reason: \"no-client-loader\";\n staleValue?: T;\n };\n\nexport interface FigDataHydrationEntry {\n key: DataResourceKey;\n value: unknown;\n}\n\nexport type FigDataEntryStatus =\n | \"pending\"\n | \"fulfilled\"\n | \"rejected\"\n | \"refreshing\";\n\nexport interface DataStoreEntrySnapshot {\n canonicalKey: string;\n error?: unknown;\n hasValue: boolean;\n key: DataResourceKey;\n pending: boolean;\n refreshError?: unknown;\n stale: boolean;\n status: FigDataEntryStatus;\n subscriberCount: number;\n value?: unknown;\n}\n\n// The explicit, app-facing store surface (FigRoot.data, readDataStore()).\n// The free functions in @bgub/fig resolve the ambient store slot, which\n// is only set while Fig executes synchronously — render, event handlers, the\n// synchronous prefix of actions and transitions, and effects. After an\n// `await` the slot is gone, so async flows capture this handle first and call\n// its methods instead.\nexport interface FigDataStoreHandle {\n hydrate(entries: readonly FigDataHydrationEntry[]): void;\n invalidateData<TArgs extends unknown[], TValue>(\n resource: DataResource<TArgs, TValue>,\n ...args: TArgs\n ): void;\n invalidateDataError(error: unknown): boolean;\n invalidateDataKey(key: DataResourceKey): void;\n invalidateDataPrefix(prefix: DataResourceKey): void;\n preloadData<TArgs extends unknown[], TValue>(\n resource: DataResource<TArgs, TValue>,\n ...args: TArgs\n ): void;\n refreshData<TArgs extends unknown[], TValue>(\n resource: DataResource<TArgs, TValue>,\n ...args: TArgs\n ): Promise<DataRefreshResult<TValue>>;\n run<T>(callback: () => T): T;\n}\n\nexport interface FigDataStore extends FigDataStoreHandle {\n commitDataDependencies(owner: object, previousOwner: object | null): void;\n deleteDataOwner(owner: object): void;\n releaseDataOwner(owner: object): void;\n resetDataDependencies(owner: object): void;\n dispose(): void;\n inspectDataEntries(): DataStoreEntrySnapshot[];\n snapshot(): FigDataHydrationEntry[];\n // Renderer plumbing, not handle surface: args stay an array because the\n // subscribing owner trails them.\n readData<TArgs extends unknown[], TValue>(\n resource: DataResource<TArgs, TValue>,\n args: TArgs,\n owner: object,\n ): TValue;\n}\n\n// The host callbacks a renderer hands to the data-store factory. Structurally\n// compatible with @bgub/fig's DataStoreHost so its createDataStore can\n// register directly.\nexport interface FigDataStoreHost {\n getLane(): unknown;\n partition?: DataResourceKeyInput;\n schedule(owner: object, lane: unknown): void;\n}\n\nexport type FigDataStoreFactory = (host: FigDataStoreHost) => FigDataStore;\n\nconst objectDataErrors = new WeakMap<object, DataResourceKey[]>();\n\nlet currentDataStore: FigDataStore | null = null;\n\nexport function resolveCurrentDataStore(\n message = \"Data resource APIs require a Fig data store.\",\n): FigDataStore {\n if (currentDataStore === null) throw new Error(message);\n return currentDataStore;\n}\n\nexport function setCurrentDataStore(\n store: FigDataStore | null,\n): FigDataStore | null {\n const previousStore = currentDataStore;\n currentDataStore = store;\n return previousStore;\n}\n\nexport function markDataResourceError(\n error: unknown,\n key: DataResourceKey,\n): void {\n // Only object errors are attributed: the WeakMap keys them by identity, so the\n // registry is GC-safe and cannot cross-attribute. Primitive rejection values\n // would collide by value and accumulate forever in a plain Map, so a thrown\n // primitive simply carries no resource-key metadata.\n if (!isObjectKey(error)) return;\n\n let keys = objectDataErrors.get(error);\n if (keys === undefined) {\n keys = [];\n objectDataErrors.set(error, keys);\n }\n\n if (keys.some((existing) => sameDataResourceKey(existing, key))) return;\n\n keys.push(key);\n}\n\nexport function dataResourceKeysForError(\n error: unknown,\n): DataResourceKey[] | undefined {\n if (!isObjectKey(error)) return undefined;\n\n const keys = objectDataErrors.get(error);\n return keys === undefined || keys.length === 0 ? undefined : [...keys];\n}\n\nfunction sameDataResourceKey(a: DataResourceKey, b: DataResourceKey): boolean {\n return (\n a.length === b.length &&\n a.every((value, index) => Object.is(value, b[index]))\n );\n}\n\nfunction isObjectKey(value: unknown): value is object {\n return (\n (typeof value === \"object\" || typeof value === \"function\") && value !== null\n );\n}\n"],"mappings":";AAsHA,MAAM,mCAAmB,IAAI,QAAmC;AAEhE,IAAI,mBAAwC;AAE5C,SAAgB,wBACd,UAAU,gDACI;CACd,IAAI,qBAAqB,MAAM,MAAM,IAAI,MAAM,OAAO;CACtD,OAAO;AACT;AAEA,SAAgB,oBACd,OACqB;CACrB,MAAM,gBAAgB;CACtB,mBAAmB;CACnB,OAAO;AACT;AAEA,SAAgB,sBACd,OACA,KACM;CAKN,IAAI,CAAC,YAAY,KAAK,GAAG;CAEzB,IAAI,OAAO,iBAAiB,IAAI,KAAK;CACrC,IAAI,SAAS,KAAA,GAAW;EACtB,OAAO,CAAC;EACR,iBAAiB,IAAI,OAAO,IAAI;CAClC;CAEA,IAAI,KAAK,MAAM,aAAa,oBAAoB,UAAU,GAAG,CAAC,GAAG;CAEjE,KAAK,KAAK,GAAG;AACf;AAEA,SAAgB,yBACd,OAC+B;CAC/B,IAAI,CAAC,YAAY,KAAK,GAAG,OAAO,KAAA;CAEhC,MAAM,OAAO,iBAAiB,IAAI,KAAK;CACvC,OAAO,SAAS,KAAA,KAAa,KAAK,WAAW,IAAI,KAAA,IAAY,CAAC,GAAG,IAAI;AACvE;AAEA,SAAS,oBAAoB,GAAoB,GAA6B;CAC5E,OACE,EAAE,WAAW,EAAE,UACf,EAAE,OAAO,OAAO,UAAU,OAAO,GAAG,OAAO,EAAE,MAAM,CAAC;AAExD;AAEA,SAAS,YAAY,OAAiC;CACpD,QACG,OAAO,UAAU,YAAY,OAAO,UAAU,eAAe,UAAU;AAE5E"}
|