@vielzeug/codex 26.10.7 → 26.10.9
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/data/catalog.json +73 -103
- package/data/llms-full.txt +756 -128
- package/data/llms.txt +3 -3
- package/data/manifest.json +1 -1
- package/data/packages/archive.json +3 -3
- package/data/packages/arsenal.json +19 -5
- package/data/packages/assay.json +6 -4
- package/data/packages/courier.json +26 -17
- package/data/packages/ore.json +10 -7
- package/data/packages/refine.json +3 -3
- package/data/packages/ripple.json +24 -29
- package/data/packages/sourcerer.json +42 -26
- package/data/refine.json +200 -240
- package/data/search.json +66 -46
- package/package.json +2 -2
package/data/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vielzeug: Full Documentation
|
|
2
2
|
|
|
3
|
-
> Complete documentation for 42 packages. Version: 26.10.
|
|
3
|
+
> Complete documentation for 42 packages. Version: 26.10.8
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -105,7 +105,7 @@ console.log(parsed.entries);
|
|
|
105
105
|
- `archiveAuthHealthSchema` + `AUTH_SCHEMA_VERSION` — typed issuer readiness checked against the issuer DDL stamp
|
|
106
106
|
- `archiveErrorResponseSchema` — the one error envelope, code-typed across the wire
|
|
107
107
|
- `createAuthWorker` — scanner-safe confirmation, live session-version enforcement, durable budgets, metadata export and coordinated erasure
|
|
108
|
-
- `AUTH_ISSUER_SERVICE` —
|
|
108
|
+
- `AUTH_ISSUER_SERVICE` — signing-key retrieval and an uncached issuer check after local catalog JWT verification
|
|
109
109
|
- `ArchiveDerivationError` — distinguishes faulty collection code from invalid submitted content
|
|
110
110
|
- Moderation — reader reports, the operator soft-hide and the report queue, keyed on a verified `roles` claim
|
|
111
111
|
- `worker.tap()` / `issuer.tap()` — typed sanitized runtime events; neither core logs by default
|
|
@@ -147,7 +147,7 @@ console.log(parsed.entries);
|
|
|
147
147
|
| Import | Purpose |
|
|
148
148
|
| --- | --- |
|
|
149
149
|
| `@vielzeug/archive` | Browser-safe collections, constants, schemas and errors |
|
|
150
|
-
| `@vielzeug/archive/worker` | Catalog factory, issuer-session service binding and event types |
|
|
150
|
+
| `@vielzeug/archive/worker` | Catalog factory, issuer signing-key/session service binding and event types |
|
|
151
151
|
| `@vielzeug/archive/auth` | Issuer factory and binding types |
|
|
152
152
|
|
|
153
153
|
## Collections
|
|
@@ -243,8 +243,13 @@ export default createArchiveWorker([notes]);
|
|
|
243
243
|
| `tap(handler, { signal? }?)` | Returns unsubscribe; a pre-aborted signal registers nothing |
|
|
244
244
|
|
|
245
245
|
`AUTH_ISSUER_SERVICE` is a required `fetch`-shaped service binding to the issuer.
|
|
246
|
+
The catalog fetches signing keys from the configured `JWT_JWKS_URL` through that
|
|
247
|
+
binding, not through global `fetch`. The URL still selects the issuer's JWKS
|
|
248
|
+
route and must satisfy the environment's transport rules. Key requests use GET
|
|
249
|
+
without a session cookie and do not follow redirects.
|
|
246
250
|
The catalog verifies the signed JWT locally, then asks the issuer to validate its
|
|
247
|
-
current account session version.
|
|
251
|
+
current account session version. JOSE caches signing keys; the catalog does not
|
|
252
|
+
cache successful issuer session verdicts.
|
|
248
253
|
If the service exchange fails or exceeds its fixed 5000 ms timeout (body decoding
|
|
249
254
|
included), the catalog returns `503 auth-unavailable`; the timeout does not cancel
|
|
250
255
|
work already started by the issuer.
|
|
@@ -404,8 +409,10 @@ clear the cookie. This revokes all sessions, including the caller's, after a
|
|
|
404
409
|
successful response. Requests authorized before revocation may finish; there is
|
|
405
410
|
no cross-database transactional or in-flight cancellation guarantee.
|
|
406
411
|
|
|
407
|
-
The catalog requires `AUTH_ISSUER_SERVICE`, a service binding to the issuer
|
|
408
|
-
|
|
412
|
+
The catalog requires `AUTH_ISSUER_SERVICE`, a service binding to the issuer for
|
|
413
|
+
both JWKS retrieval and live session validation. Signing-key retrieval uses
|
|
414
|
+
`JWT_JWKS_URL` through the binding, so it does not depend on a public same-zone
|
|
415
|
+
Worker-to-Worker fetch. After local JWT signature/claims verification, it requests
|
|
409
416
|
`GET /api/v1/auth/session` through that binding on every private request. Only a
|
|
410
417
|
strictly validated 401 `unauthenticated` remains a 401. Malformed, unexpected,
|
|
411
418
|
provider and 5xx responses become 503 `auth-unavailable`; there is no authority
|
|
@@ -927,8 +934,8 @@ well-formed, or the route answers 503 without naming what is missing:
|
|
|
927
934
|
| ------------------------ | ---------------------------------------------------------------- |
|
|
928
935
|
| `APP_ENV` | `local` or `production` (transport rules and the pepper policy) |
|
|
929
936
|
| `ALLOWED_ORIGINS` | Exact canonical origins; never wildcards |
|
|
930
|
-
| `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` |
|
|
931
|
-
| `AUTH_ISSUER_SERVICE` | Required issuer service binding for live session-version checks
|
|
937
|
+
| `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` | Exact issuer claim, audience and issuer JWKS endpoint URL |
|
|
938
|
+
| `AUTH_ISSUER_SERVICE` | Required issuer service binding for signing-key retrieval and live session-version checks |
|
|
932
939
|
| `LIKES_ENABLED` / `PUBLISHING_ENABLED` | Feature flags |
|
|
933
940
|
| `RATE_LIMIT_KEY_SECRET` | HMAC pepper. Required ≥ 32 chars in production; local may fall back |
|
|
934
941
|
| `DB` / `READ_LIMIT` / `WRITE_LIMIT` | D1 + rate-limit bindings |
|
|
@@ -941,6 +948,14 @@ make the header lie.
|
|
|
941
948
|
Generated Env types (wrangler's `worker-configuration.d.ts`) satisfy the package's
|
|
942
949
|
structural `ArchiveWorkerEnv` — no `@cloudflare/workers-types` dependency, no casts.
|
|
943
950
|
|
|
951
|
+
Signing keys are fetched from `JWT_JWKS_URL` through `AUTH_ISSUER_SERVICE`, not
|
|
952
|
+
global `fetch`. Point the URL at the bound issuer's `/api/v1/auth/jwks.json`
|
|
953
|
+
route; retain the HTTPS production or loopback HTTP local configuration rules.
|
|
954
|
+
This transport supports Workers on the same Cloudflare zone without a public
|
|
955
|
+
Worker-to-Worker fetch. JOSE caches signing keys, but every private request still
|
|
956
|
+
checks the current issuer session version. A signing-key provider failure is
|
|
957
|
+
`503 auth-unavailable`, not a reason to clear the browser's signed-in state.
|
|
958
|
+
|
|
944
959
|
## The auth issuer
|
|
945
960
|
|
|
946
961
|
`createAuthWorker()` from `@vielzeug/archive/auth` is the reference passwordless
|
|
@@ -1377,6 +1392,8 @@ console.log(byRole, health, profile);
|
|
|
1377
1392
|
- **`tryParseJson`**: preserve JSON syntax failures from `/object`
|
|
1378
1393
|
- **`allocate`**: lossless proportional distribution from `/math`
|
|
1379
1394
|
- **`chunk`, `zip`, `compact`**: typed collection transforms from `/array`
|
|
1395
|
+
- **`paginate`**: one page plus its totals from an in-memory list, in a one- or zero-based page base
|
|
1396
|
+
- **`sort`**: selector, `{ key: direction }`, or ordered grid criteria from one entry point
|
|
1380
1397
|
- **`range`, `average`, `median`**: common numeric operations from `/math`
|
|
1381
1398
|
- **`assert`, `tap`**: typed control-flow helpers from `/function`
|
|
1382
1399
|
- **`isEqual`**: structural equality from package root
|
|
@@ -1404,6 +1421,8 @@ console.log(byRole, health, profile);
|
|
|
1404
1421
|
| --- | --- | --- | --- |
|
|
1405
1422
|
| `groupBy` | Group values by key (prototype-pollution guarded) | Sync | Root export |
|
|
1406
1423
|
| `chunk` / `zip` / `compact` | Typed collection transforms | Sync | Available from `/array` |
|
|
1424
|
+
| `paginate` | Cut one page and its totals out of a list | Sync | Pages past the last return empty `items`, unclamped |
|
|
1425
|
+
| `sort` | Sort by a selector, keyed fields, or ordered path criteria | Sync | Criteria paths are resolved safely; mixed value types throw |
|
|
1407
1426
|
| `range` / `average` / `median` | Numeric generation and aggregation | Sync | Available from `/math` |
|
|
1408
1427
|
| `assert` | Assert and narrow a condition | Sync | Available from `/function` |
|
|
1409
1428
|
| `retry` | Retry async work | Async | Rethrows final error |
|
|
@@ -1424,7 +1443,7 @@ console.log(byRole, health, profile);
|
|
|
1424
1443
|
| Import | Purpose |
|
|
1425
1444
|
| --- | --- |
|
|
1426
1445
|
| `@vielzeug/arsenal` | Curated common utilities |
|
|
1427
|
-
| `@vielzeug/arsenal/array` | Immutable transforms, set operations, sorting, fuzzy search |
|
|
1446
|
+
| `@vielzeug/arsenal/array` | Immutable transforms, pagination, set operations, sorting, fuzzy search |
|
|
1428
1447
|
| `@vielzeug/arsenal/async` | Retry, cancellation, task pool, timing |
|
|
1429
1448
|
| `@vielzeug/arsenal/cache` | In-memory cache and memoization |
|
|
1430
1449
|
| `@vielzeug/arsenal/encoding` | Base64url and native byte compression |
|
|
@@ -1452,6 +1471,35 @@ unzip(rows): tupleOfArrays
|
|
|
1452
1471
|
|
|
1453
1472
|
`take`, `takeLast`, `drop`, and `dropLast` return new arrays and normalize negative counts to zero. Set operations accept optional selectors for object identity.
|
|
1454
1473
|
|
|
1474
|
+
### paginate
|
|
1475
|
+
|
|
1476
|
+
```ts
|
|
1477
|
+
paginate(items: readonly T[], options?: PaginateOptions): PageEnvelope
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
Cuts one page out of an in-memory list and returns the items with the arithmetic that produced them. `pageOffset` selects the caller's page base: `1` (default) for one-based paging, `0` for zero-based servers such as Spring. The default `page` is the first page in that base, and the default `pageSize` is `20`. `totalPages` is at least 1, even for an empty list, so `page (array: readonly T[], selector: (item: T) => unknown, direction?: SortDirection): T[];
|
|
1481
|
+
sort(array: readonly T[], selectors: SortSelectors): T[];
|
|
1482
|
+
sort(array: readonly T[], criteria: ReadonlyArray>): T[];
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
Three forms, one entry point. The selector form sorts by one extracted value; the selectors form accepts an object mapping property keys to directions; the criteria form sorts by an ordered list of paths, which is the shape a data grid hands over when its sortable columns are runtime strings.
|
|
1486
|
+
|
|
1487
|
+
```ts
|
|
1488
|
+
import { sort } from '@vielzeug/arsenal/array';
|
|
1489
|
+
|
|
1490
|
+
const users = [{ name: 'Ada', age: 41 }, { name: 'Grace', age: 30 }];
|
|
1491
|
+
sort(users, (user) => user.name);
|
|
1492
|
+
sort(users, { name: 'asc', age: 'desc' });
|
|
1493
|
+
|
|
1494
|
+
const rows = [
|
|
1495
|
+
{ address: { city: 'Berlin' }, age: 41 },
|
|
1496
|
+
{ address: { city: 'Aachen' }, age: 30 },
|
|
1497
|
+
];
|
|
1498
|
+
sort(rows, [{ key: 'address.city' }, { key: 'age', direction: 'desc' }]);
|
|
1499
|
+
```
|
|
1500
|
+
|
|
1501
|
+
Criteria paths are resolved with `getPath`, so dotted and bracketed paths (`'address.city'`, `'tags[0]'`) work and prototype-access segments are refused. The first criterion that differs wins and earlier ties keep their input order. Ordering is delegated to `compare`, so a column that mixes value types (a number beside a string) throws a `TypeError` instead of guessing an order.
|
|
1502
|
+
|
|
1455
1503
|
### fuzzyFilter / fuzzyScore
|
|
1456
1504
|
|
|
1457
1505
|
```ts
|
|
@@ -1717,6 +1765,16 @@ type FuzzySelection = FuzzyOptions & {
|
|
|
1717
1765
|
|
|
1718
1766
|
type ScoredResult = { item: T; score: number };
|
|
1719
1767
|
|
|
1768
|
+
type PageEnvelope = Readonly;
|
|
1769
|
+
|
|
1770
|
+
type PaginateOptions = Readonly;
|
|
1771
|
+
|
|
1772
|
+
type SortDirection = 'asc' | 'desc';
|
|
1773
|
+
|
|
1774
|
+
type SortSelectors = Partial>;
|
|
1775
|
+
|
|
1776
|
+
type SortCriterion = Readonly;
|
|
1777
|
+
|
|
1720
1778
|
type CacheOptions = {
|
|
1721
1779
|
capacity?: number;
|
|
1722
1780
|
now?: () => number;
|
|
@@ -1791,6 +1849,45 @@ const byPrice = sort(products, (product) => product.price);
|
|
|
1791
1849
|
console.log(publishedLabels, byId, byPrice);
|
|
1792
1850
|
```
|
|
1793
1851
|
|
|
1852
|
+
## Page an In-Memory List
|
|
1853
|
+
|
|
1854
|
+
Use `paginate` when the whole list is local and a view needs one page plus its totals.
|
|
1855
|
+
|
|
1856
|
+
```ts
|
|
1857
|
+
import { paginate } from '@vielzeug/arsenal/array';
|
|
1858
|
+
|
|
1859
|
+
const rows = Array.from({ length: 45 }, (_, index) => ({ id: index + 1 }));
|
|
1860
|
+
|
|
1861
|
+
const first = paginate(rows, { pageSize: 10 });
|
|
1862
|
+
const past = paginate(rows, { page: 9, pageSize: 10 });
|
|
1863
|
+
|
|
1864
|
+
console.log(first.totalPages, past.items); // 5 []
|
|
1865
|
+
```
|
|
1866
|
+
|
|
1867
|
+
`page` is echoed rather than clamped, so a stale page number reads as an empty page instead of
|
|
1868
|
+
silently jumping elsewhere. Pass `{ pageOffset: 0 }` for a zero-based caller and the envelope comes
|
|
1869
|
+
back in that same base.
|
|
1870
|
+
|
|
1871
|
+
## Sort by Grid Criteria
|
|
1872
|
+
|
|
1873
|
+
`sort` accepts the ordering a data grid already holds: an ordered list of column paths, each with a direction.
|
|
1874
|
+
|
|
1875
|
+
```ts
|
|
1876
|
+
import { sort } from '@vielzeug/arsenal/array';
|
|
1877
|
+
|
|
1878
|
+
const rows = [
|
|
1879
|
+
{ address: { city: 'Berlin' }, age: 41 },
|
|
1880
|
+
{ address: { city: 'Aachen' }, age: 30 },
|
|
1881
|
+
{ address: { city: 'Berlin' }, age: 27 },
|
|
1882
|
+
];
|
|
1883
|
+
|
|
1884
|
+
const ordered = sort(rows, [{ key: 'address.city' }, { key: 'age', direction: 'desc' }]);
|
|
1885
|
+
```
|
|
1886
|
+
|
|
1887
|
+
Paths are resolved safely, so nested and indexed columns work and prototype access is refused. The
|
|
1888
|
+
first criterion that differs decides the order and ties keep their input order. Keep each column
|
|
1889
|
+
typed: a column mixing numbers and strings throws a `TypeError` rather than guessing an order.
|
|
1890
|
+
|
|
1794
1891
|
## Search Explicit Fields
|
|
1795
1892
|
|
|
1796
1893
|
Search string arrays directly. Object collections require `select`, so callers define exactly what can match.
|
|
@@ -1972,8 +2069,10 @@ const settings = parsed.ok ? Settings.parse(parsed.value) : { theme: 'system' };
|
|
|
1972
2069
|
- filterMap - Filter and map array elements (id: `array-filter`)
|
|
1973
2070
|
- groupBy - Group array by key (id: `array-group`)
|
|
1974
2071
|
- filterMap - Transform array elements (id: `array-map`)
|
|
2072
|
+
- paginate - Page an array (id: `array-paginate`)
|
|
1975
2073
|
- fuzzyFilter - Explicit searchable fields (id: `array-search`)
|
|
1976
2074
|
- fuzzyFilter - Unicode normalization (id: `array-search-normalize`)
|
|
2075
|
+
- sort - Selector, selectors, or grid criteria (id: `array-sort-criteria`)
|
|
1977
2076
|
- uniq - Remove duplicates (id: `array-uniq`)
|
|
1978
2077
|
- attempt - Safe async execution with isFail/isOk helpers (id: `async-attempt`)
|
|
1979
2078
|
- parallel - Controlled parallel execution (id: `async-parallel`)
|
|
@@ -2072,6 +2171,8 @@ await waitUntil(() => view.queryByText('Saved') !== null);
|
|
|
2072
2171
|
- `fireClick`, `fireInput`, `fireKeyDown`, and peers dispatch exact synchronous events.
|
|
2073
2172
|
- `waitUntil`, `eventually`, and `waitForEvent` provide bounded, abortable async waiting.
|
|
2074
2173
|
- `delay` models explicit cancellable timer scheduling.
|
|
2174
|
+
- `walkFlatTree` traverses the flat tree across slot boundaries jsdom's light-DOM queries miss.
|
|
2175
|
+
- `installFormInternalsPolyfill` gives jsdom the `ElementInternals` form-association API.
|
|
2075
2176
|
- `AssayError`, `AssayQueryError`, and `AssayTimeoutError` provide typed failures.
|
|
2076
2177
|
|
|
2077
2178
|
## Documentation
|
|
@@ -2098,12 +2199,14 @@ await waitUntil(() => view.queryByText('Saved') !== null);
|
|
|
2098
2199
|
| `fire*` / `dispatch` | Dispatches platform event instances | Sync | Does not reproduce browser default behavior |
|
|
2099
2200
|
| `waitUntil` / `eventually` / `waitForEvent` | Waits for conditions, assertions, or events | Async | Use a signal or timeout for bounded waits |
|
|
2100
2201
|
| `delay` | Schedules a cancellable timer | Async | Pass a signal to cancel a pending wait |
|
|
2202
|
+
| `walkFlatTree` | Walks the flat tree across slot boundaries | Sync | jsdom-only need: browsers already use the flat tree |
|
|
2203
|
+
| `installFormInternalsPolyfill` | Polyfills `ElementInternals` form association for jsdom | Sync | Global patches: only install in suites that need them |
|
|
2101
2204
|
|
|
2102
2205
|
## Package Entry Point
|
|
2103
2206
|
|
|
2104
2207
|
| Import | Purpose |
|
|
2105
2208
|
| ----------------- | ---------------------------------------------------- |
|
|
2106
|
-
| `@vielzeug/assay` | DOM queries, events, wait helpers, errors, and types |
|
|
2209
|
+
| `@vielzeug/assay` | DOM queries, events, wait helpers, test-environment helpers, errors, and types |
|
|
2107
2210
|
|
|
2108
2211
|
## Queries
|
|
2109
2212
|
|
|
@@ -2195,6 +2298,57 @@ await delay(100, { signal });
|
|
|
2195
2298
|
|
|
2196
2299
|
`timeout` and delay milliseconds must be finite, non-negative, and no greater than 2,147,483,647 ms; `interval` must be finite, positive, and within the same timer limit. Invalid durations throw `RangeError`. `eventually()` also accepts `message` for diagnostic timeout context.
|
|
2197
2300
|
|
|
2301
|
+
## Test-environment helpers
|
|
2302
|
+
|
|
2303
|
+
### `walkFlatTree(root, visit)`
|
|
2304
|
+
|
|
2305
|
+
```ts
|
|
2306
|
+
function walkFlatTree(root: Element, visit: (element: HTMLElement) => void): void;
|
|
2307
|
+
```
|
|
2308
|
+
|
|
2309
|
+
Walks the flat tree below `root`, calling `visit` for each assigned or ordinary descendant (not for `root` itself).
|
|
2310
|
+
At each `` it expands `assignedElements()` instead of descending into its (always empty, in jsdom) literal
|
|
2311
|
+
children. jsdom only
|
|
2312
|
+
implements the light DOM tree, so `querySelectorAll()` never crosses a slot into its assigned elements; real
|
|
2313
|
+
browsers use the flat tree for that. This helper closes the gap for tests that must find form-associated (or
|
|
2314
|
+
otherwise slotted) elements through shadow boundaries.
|
|
2315
|
+
|
|
2316
|
+
```ts
|
|
2317
|
+
const named: HTMLElement[] = [];
|
|
2318
|
+
|
|
2319
|
+
walkFlatTree(form, (el) => {
|
|
2320
|
+
if (el.hasAttribute('name')) named.push(el);
|
|
2321
|
+
});
|
|
2322
|
+
```
|
|
2323
|
+
|
|
2324
|
+
### `installFormInternalsPolyfill()`
|
|
2325
|
+
|
|
2326
|
+
```ts
|
|
2327
|
+
function installFormInternalsPolyfill(): () => void;
|
|
2328
|
+
```
|
|
2329
|
+
|
|
2330
|
+
jsdom implements none of the `ElementInternals` form-association API: not `setFormValue`, not `setValidity`, not
|
|
2331
|
+
the `checkValidity`/`reportValidity`/`validity`/`validationMessage` mixin real browsers put on every
|
|
2332
|
+
`formAssociated: true` element, not even `FormData` collecting a form-associated element's set value, or
|
|
2333
|
+
`.reset()` invoking `formResetCallback()`. Every one of those gaps is exercised by `useField()` from
|
|
2334
|
+
`@vielzeug/ore`, so any package testing a form-associated component needs all of them.
|
|
2335
|
+
|
|
2336
|
+
Call it from a test setup when the suite tests form-associated components; it returns an `uninstall()` that
|
|
2337
|
+
restores every patched global. The patches are global monkey-patches and skip APIs already implemented by the
|
|
2338
|
+
environment, so suites without form-associated components shouldn't install them. If `ElementInternals` is absent,
|
|
2339
|
+
the installer is a no-op. A downstream package should
|
|
2340
|
+
rely on this instead of hand-rolling its own copy.
|
|
2341
|
+
|
|
2342
|
+
```ts
|
|
2343
|
+
// vitest.setup.ts
|
|
2344
|
+
import { installFormInternalsPolyfill } from '@vielzeug/assay';
|
|
2345
|
+
import { install } from '@vielzeug/ore/testing';
|
|
2346
|
+
import { afterAll, afterEach } from 'vitest';
|
|
2347
|
+
|
|
2348
|
+
install(afterEach);
|
|
2349
|
+
afterAll(installFormInternalsPolyfill());
|
|
2350
|
+
```
|
|
2351
|
+
|
|
2198
2352
|
## Types
|
|
2199
2353
|
|
|
2200
2354
|
```ts
|
|
@@ -2349,6 +2503,36 @@ expect(onSave).toHaveBeenCalledOnce();
|
|
|
2349
2503
|
Refine's testing entry point contains only Refine-specific assertions and typed mount wrappers. Import Assay helpers
|
|
2350
2504
|
directly instead of routing generic DOM operations through another package.
|
|
2351
2505
|
|
|
2506
|
+
When a suite tests a form-associated custom element (one built on `useField()` from `@vielzeug/ore`), install the
|
|
2507
|
+
form-internals polyfill in the setup file so jsdom gains the `ElementInternals` API it lacks:
|
|
2508
|
+
|
|
2509
|
+
```ts
|
|
2510
|
+
// vitest.setup.ts
|
|
2511
|
+
import { installFormInternalsPolyfill } from '@vielzeug/assay';
|
|
2512
|
+
import { install } from '@vielzeug/ore/testing';
|
|
2513
|
+
import { afterAll, afterEach } from 'vitest';
|
|
2514
|
+
|
|
2515
|
+
install(afterEach);
|
|
2516
|
+
afterAll(installFormInternalsPolyfill());
|
|
2517
|
+
```
|
|
2518
|
+
|
|
2519
|
+
## Traverse slotted content in jsdom
|
|
2520
|
+
|
|
2521
|
+
Use `walkFlatTree()` when a test must visit descendants assigned through slots. It expands each slot into its assigned
|
|
2522
|
+
elements, including nested slots, instead of relying on jsdom's light-DOM-only `querySelectorAll()`.
|
|
2523
|
+
|
|
2524
|
+
```ts
|
|
2525
|
+
import { walkFlatTree } from '@vielzeug/assay';
|
|
2526
|
+
|
|
2527
|
+
const named: HTMLElement[] = [];
|
|
2528
|
+
walkFlatTree(form, (element) => {
|
|
2529
|
+
if (element.hasAttribute('name')) named.push(element);
|
|
2530
|
+
});
|
|
2531
|
+
```
|
|
2532
|
+
|
|
2533
|
+
The callback receives descendants in the flat tree, not the root element or slot elements themselves. Browsers already
|
|
2534
|
+
use the flat tree for DOM algorithms; this helper is for test code that needs equivalent traversal in jsdom.
|
|
2535
|
+
|
|
2352
2536
|
## Best Practices
|
|
2353
2537
|
|
|
2354
2538
|
- Scope multiple assertions with `within()` rather than repeatedly querying the document.
|
|
@@ -4523,6 +4707,7 @@ try {
|
|
|
4523
4707
|
- **`withBearerAuth()` / `withRequestId()` / `withLogging()`**: composable middleware configured at construction.
|
|
4524
4708
|
- **`tap()`**: signal-owned structured transport observation.
|
|
4525
4709
|
- **Error taxonomy**: HTTP, network, timeout, abort, parse, and schema failures are distinct, actionable classes.
|
|
4710
|
+
- **RFC 9457 problem details**: an `application/problem+json` error body parses onto `CourierHttpError.problem` while the HTTP status stays authoritative.
|
|
4526
4711
|
- **`cancelAll()` / `dispose()`**: abort active requests and tear down the transport.
|
|
4527
4712
|
|
|
4528
4713
|
## Documentation
|
|
@@ -4554,6 +4739,9 @@ try {
|
|
|
4554
4739
|
| `withRequestId()` | Adds request identifier middleware | Sync | Default generator uses `crypto.randomUUID()` |
|
|
4555
4740
|
| `withLogging()` | Logs request result metadata | Sync | Requires explicit logger; URLs may contain sensitive query values |
|
|
4556
4741
|
| `tap()` | Observe structured transport events | Sync | Observation only; optional signal owns lifetime |
|
|
4742
|
+
| `CourierHttpError.problem` | Parsed RFC 9457 problem details from an error body | Sync | Present only when the response is `application/problem+json` |
|
|
4743
|
+
| `ProblemDetail` | RFC 9457 problem members and extension members | Sync | Extension values are `unknown`; Courier does not validate their shapes |
|
|
4744
|
+
| `problemDetailError()` | Build an HTTP error that carries problem details | Sync | Intended for mocks and middleware, not for parsing a response |
|
|
4557
4745
|
|
|
4558
4746
|
## Package Entry Point
|
|
4559
4747
|
|
|
@@ -4799,7 +4987,7 @@ type PrefetchConfig = Omit,
|
|
|
4799
4987
|
| Error | Trigger | Notable properties |
|
|
4800
4988
|
| --- | --- | --- |
|
|
4801
4989
|
| `CourierError` | Base class for all Courier errors | Use `instanceof` to narrow |
|
|
4802
|
-
| `CourierHttpError` | Non-2xx HTTP response | `status`, `data`, `headers`, `method`, `url`; `CourierHttpError.is(e, status?)` narrows by status |
|
|
4990
|
+
| `CourierHttpError` | Non-2xx HTTP response | `status`, `data`, `headers`, `method`, `url`, `problem`; `CourierHttpError.is(e, status?)` narrows by status |
|
|
4803
4991
|
| `CourierNetworkError` | Request failure without response | `method`, `url`, `cause` |
|
|
4804
4992
|
| `CourierTimeoutError` | Timeout signal aborts request | `method`, `url`, `cause` |
|
|
4805
4993
|
| `CourierAbortError` | Caller or client cancellation | `method`, `url`, `cause` |
|
|
@@ -4810,6 +4998,72 @@ type PrefetchConfig = Omit,
|
|
|
4810
4998
|
|
|
4811
4999
|
Errors thrown directly by custom middleware are preserved rather than reclassified.
|
|
4812
5000
|
|
|
5001
|
+
### Problem details
|
|
5002
|
+
|
|
5003
|
+
```ts
|
|
5004
|
+
export type ProblemDetail = Readonly;
|
|
5005
|
+
|
|
5006
|
+
export const PROBLEM_JSON_MEDIA_TYPE = 'application/problem+json';
|
|
5007
|
+
|
|
5008
|
+
export function isProblemMediaType(contentType: string | null | undefined): boolean;
|
|
5009
|
+
|
|
5010
|
+
export function problemDetailError(
|
|
5011
|
+
problem: ProblemDetail,
|
|
5012
|
+
options?: { message?: string; method?: string; status?: number; url?: string },
|
|
5013
|
+
): CourierHttpError;
|
|
5014
|
+
```
|
|
5015
|
+
|
|
5016
|
+
When a non-2xx response answers with `application/problem+json` (RFC 9457), `CourierHttpError.problem`
|
|
5017
|
+
holds a shallow-frozen copy of the parsed JSON object and the error message prefers `detail`, then
|
|
5018
|
+
`title`, then the response status text (or `HTTP ` when status text is empty). Any other error
|
|
5019
|
+
body keeps the shape it already had on `data`, with `problem` left `undefined`. Courier checks that
|
|
5020
|
+
the parsed body is an object; it does not validate the types or contents of registered or extension
|
|
5021
|
+
members. RFC 9457 treats an omitted `type` as `about:blank`, but Courier does not insert that value
|
|
5022
|
+
into the object.
|
|
5023
|
+
|
|
5024
|
+
| Member | Purpose | Common gotcha |
|
|
5025
|
+
| --- | --- | --- |
|
|
5026
|
+
| `ProblemDetail` | The five registered RFC 9457 members plus extensions | Every member is optional; extension values have type `unknown` |
|
|
5027
|
+
| `CourierHttpError.problem` | Parsed problem document | For response errors, it is a frozen shallow copy and is `undefined` unless the media type matches and the body is an object |
|
|
5028
|
+
| `PROBLEM_JSON_MEDIA_TYPE` | Registered media type constant | Compare with `isProblemMediaType()`, not `===`, to allow parameters |
|
|
5029
|
+
| `isProblemMediaType()` | Match `application/problem+json` and its parameterised forms | Plain `application/json` never matches |
|
|
5030
|
+
| `problemDetailError()` | Build a problem-carrying `CourierHttpError` without a server | Status defaults to numeric `problem.status`, then `500`; a supplied `options.status` takes precedence |
|
|
5031
|
+
|
|
5032
|
+
```ts
|
|
5033
|
+
problemDetailError(problem, options?)
|
|
5034
|
+
```
|
|
5035
|
+
|
|
5036
|
+
`problemDetailError()` returns a `CourierHttpError` with the supplied problem object on both
|
|
5037
|
+
`error.problem` and `error.data`. Its defaults are `method: 'GET'`, `url: ''`, and status from
|
|
5038
|
+
`problem.status` when numeric or `500` otherwise. The error message uses `options.message`, then
|
|
5039
|
+
`problem.detail`, `problem.title`, and finally `HTTP `. Unlike a parsed response problem,
|
|
5040
|
+
the supplied object is not copied or frozen.
|
|
5041
|
+
|
|
5042
|
+
The HTTP status line stays authoritative: `error.status` is the response status even when
|
|
5043
|
+
`problem.status` disagrees. A JSON array or primitive leaves `data` as that parsed value; malformed
|
|
5044
|
+
JSON leaves `data` as its raw text. Neither produces a partial `problem`.
|
|
5045
|
+
|
|
5046
|
+
```ts
|
|
5047
|
+
import { createCourier, CourierHttpError, problemDetailError, type ProblemDetail } from '@vielzeug/courier';
|
|
5048
|
+
|
|
5049
|
+
const courier = createCourier({ baseUrl: '/api' });
|
|
5050
|
+
|
|
5051
|
+
try {
|
|
5052
|
+
await courier.post('/invoices');
|
|
5053
|
+
} catch (error) {
|
|
5054
|
+
if (CourierHttpError.is(error)) {
|
|
5055
|
+
const problem: ProblemDetail | undefined = error.problem;
|
|
5056
|
+
console.log(error.status, problem?.title, problem?.detail, problem?.type);
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
const mockedError = problemDetailError(
|
|
5061
|
+
{ title: 'Insufficient credit', detail: 'Choose a lower amount.', status: 402 },
|
|
5062
|
+
{ method: 'POST', url: '/invoices' },
|
|
5063
|
+
);
|
|
5064
|
+
console.log(mockedError.status, mockedError.problem?.detail);
|
|
5065
|
+
```
|
|
5066
|
+
|
|
4813
5067
|
### Usage Guide
|
|
4814
5068
|
|
|
4815
5069
|
## Basic Usage
|
|
@@ -4998,6 +5252,55 @@ async function loadUser(): Promise {
|
|
|
4998
5252
|
- Do not display cancellation as an application error during navigation.
|
|
4999
5253
|
- `CourierSchemaValidationError` carries the raw pre-validation `data`.
|
|
5000
5254
|
|
|
5255
|
+
## Problem Details
|
|
5256
|
+
|
|
5257
|
+
When a server answers a failure with `application/problem+json` (RFC 9457), Courier parses the
|
|
5258
|
+
JSON object onto `CourierHttpError.problem` and chooses the message from `detail`, then `title`,
|
|
5259
|
+
then the HTTP status text. The parsed object is shallow-frozen; extension members remain `unknown`
|
|
5260
|
+
and Courier does not validate their contents. Nothing is configured.
|
|
5261
|
+
|
|
5262
|
+
```ts
|
|
5263
|
+
import { CourierHttpError, createCourier, type ProblemDetail } from '@vielzeug/courier';
|
|
5264
|
+
|
|
5265
|
+
const courier = createCourier({ baseUrl: '/api' });
|
|
5266
|
+
|
|
5267
|
+
async function submitInvoice(body: unknown): Promise {
|
|
5268
|
+
try {
|
|
5269
|
+
await courier.post('/invoices', { body });
|
|
5270
|
+
return 'Saved.';
|
|
5271
|
+
} catch (error) {
|
|
5272
|
+
if (!CourierHttpError.is(error)) throw error;
|
|
5273
|
+
|
|
5274
|
+
const problem = error.problem;
|
|
5275
|
+
if (problem?.title === 'Insufficient credit') return `Blocked: ${problem.detail ?? 'no credit'}`;
|
|
5276
|
+
|
|
5277
|
+
// Extension members are ordinary properties on the same document.
|
|
5278
|
+
const field = problem?.['errors'] as Record | undefined;
|
|
5279
|
+
|
|
5280
|
+
return field ? Object.values(field).join(', ') : `Request failed (${error.status}).`;
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
```
|
|
5284
|
+
|
|
5285
|
+
`error.status` is always the HTTP status from the response, so a `problem.status` that disagrees
|
|
5286
|
+
never overrides it. `problem` stays `undefined` for any other content type, and a `problem+json`
|
|
5287
|
+
body that is a JSON array or primitive keeps that parsed value on `data`; malformed JSON keeps its
|
|
5288
|
+
raw text there. Neither case creates a partial `problem`. RFC 9457 treats an omitted `type` as
|
|
5289
|
+
`about:blank`, but Courier does not add a `type` property. Use `problemDetailError()` to raise the
|
|
5290
|
+
same error shape from a mock or middleware; it stores the supplied problem object directly on both
|
|
5291
|
+
`problem` and `data`.
|
|
5292
|
+
|
|
5293
|
+
```ts
|
|
5294
|
+
import { problemDetailError } from '@vielzeug/courier';
|
|
5295
|
+
|
|
5296
|
+
const mockFailure = problemDetailError(
|
|
5297
|
+
{ title: 'Insufficient credit', detail: 'Choose a lower amount.', status: 402 },
|
|
5298
|
+
{ method: 'POST', url: '/invoices' },
|
|
5299
|
+
);
|
|
5300
|
+
|
|
5301
|
+
console.log(mockFailure.status, mockFailure.problem?.detail);
|
|
5302
|
+
```
|
|
5303
|
+
|
|
5001
5304
|
## Composing with a State Layer
|
|
5002
5305
|
|
|
5003
5306
|
Courier owns HTTP transport and optional parsed GET caching. Observable query state, optimistic mutations, collection navigation, and stream state belong to the owning state layer (e.g. Sourcerer, TanStack Query, or consumer code). Compose Courier calls into that layer instead of nesting state machines.
|
|
@@ -5116,6 +5419,7 @@ There is no `use()` method. To change middleware, create a new client. This keep
|
|
|
5116
5419
|
- Cached Read and Prefetch (id: `cached-read`)
|
|
5117
5420
|
- createCourier - Transport Client (id: `create-courier`)
|
|
5118
5421
|
- middleware - Immutable Chain (id: `middleware`)
|
|
5422
|
+
- problem+json - RFC 9457 error bodies (id: `problem-details`)
|
|
5119
5423
|
|
|
5120
5424
|
---
|
|
5121
5425
|
|
|
@@ -15548,7 +15852,7 @@ define('my-counter', {
|
|
|
15548
15852
|
- Props via `prop.*` helpers (`prop.string`, `prop.number`, `prop.bool`, `prop.oneOf`, `prop.json`, `prop.data`) or raw `PropDef` objects
|
|
15549
15853
|
- `setup(props)` takes only props and returns an `HTMLResult` directly: `return html\`...\``
|
|
15550
15854
|
- Lifecycle hooks: `onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`: plain functions imported from `@vielzeug/ore`, called directly from `setup()` or any composable it calls
|
|
15551
|
-
- Directives: `each` (keyed
|
|
15855
|
+
- Directives: `each` (transactional keyed list reconciliation), `classMap`, `styleMap`, `when`, `live`, `model`, `unsafeHtml`
|
|
15552
15856
|
- Host bindings via `bind({ attr, class, style, on })`: pass `{ target: el }` to bind any off-host element
|
|
15553
15857
|
- Reactive ARIA sync via explicit `aria-*` keys in `bind({ attr }, { target })`, with automatic cleanup on disconnect
|
|
15554
15858
|
- Context via `provide(key, value)` / `inject(key)`; typed emit/slots via `useEmit()` / `useSlots()`
|
|
@@ -15596,6 +15900,7 @@ All browser-runtime symbols below are imported from `@vielzeug/ore`. Lifecycle/c
|
|
|
15596
15900
|
| `each()` | Keyed list rendering with DOM diffing | Sync | Duplicate keys report `ore:error`; plain `T[]` is a one-time static render |
|
|
15597
15901
|
| `when()` | Conditional branch rendering | Sync | Getter-fn computed disposed on cleanup; static bool skips subscription |
|
|
15598
15902
|
| `live(signal)` | One-way binding that skips stale writes during input | Sync | Use for controlled inputs alongside a manual `@input` handler |
|
|
15903
|
+
| `model(signal)` | Two-way form-control binding (value/checked + write-back) | Sync | Plain `value`/`checked` attribute only; pass `get` to keep numeric signals numeric |
|
|
15599
15904
|
| `onMounted(fn)` | DOM-ready callback | Setup only | Must be called synchronously during `setup()` |
|
|
15600
15905
|
| `onCleanup(fn)` | Register teardown | Setup only | Called on component disconnect |
|
|
15601
15906
|
| `onEvent(target, …)` | Scoped event listener with auto-cleanup | Setup only | No-ops on null target; removed on disconnect |
|
|
@@ -15710,12 +16015,13 @@ setup(_props) {
|
|
|
15710
16015
|
| `prop.string(defaultValue?)` | `PropDef` | Reflects by default |
|
|
15711
16016
|
| `prop.bool(defaultValue?)` | `PropDef` | Any non-null attribute value other than `"false"` parses as `true`; `"false"` or absent attribute is `false` |
|
|
15712
16017
|
| `prop.number(defaultValue?)` | `PropDef` | Returns default (not NaN) and warns in dev when attribute is not a valid number |
|
|
15713
|
-
| `prop.oneOf(allowed, defaultValue)` | `PropDef`
|
|
16018
|
+
| `prop.oneOf(allowed, defaultValue?)` | `PropDef` | Restricts to a fixed set of string or number members; `parse` falls back to `defaultValue` (or `undefined` when omitted) for anything outside the set. Numeric members match the attribute parsed as a number |
|
|
15714
16019
|
| `prop.json(defaultValue)` | `PropDef` | JSON.parse from attribute; `reflect: false` |
|
|
15715
16020
|
| `prop.data(defaultValue?)` | `PropDef` | JS-only: never reads/writes an attribute; use for objects, arrays, callbacks, or any non-serialisable value |
|
|
15716
16021
|
|
|
15717
16022
|
> **Choosing the right prop helper:**
|
|
15718
16023
|
>
|
|
16024
|
+
> - **`prop.oneOf`**: the honest way to type a union-valued prop: the restriction is enforced at runtime (`parse` falls back for anything outside the set), not just in a type parameter a raw attribute value could violate. Pass an `as const` array of the allowed string or number members.
|
|
15719
16025
|
> - **`prop.json`**: value can be declared in HTML (``); attribute string is `JSON.parse`d.
|
|
15720
16026
|
> - **`prop.data`**: value is always set from JavaScript (objects, arrays, callbacks, class instances); the attribute is never read. Use this for both data and function props.
|
|
15721
16027
|
|
|
@@ -15748,7 +16054,16 @@ define('data-grid', {
|
|
|
15748
16054
|
### `html`
|
|
15749
16055
|
|
|
15750
16056
|
Tagged template literal that returns an `HTMLResult`. Supports text interpolation, ordinary attributes (`attr=`),
|
|
15751
|
-
boolean attributes (`?attr=`),
|
|
16057
|
+
boolean attributes (`?attr=`), explicit property bindings (`.prop=`), events (`@event=` with optional
|
|
16058
|
+
`.stop`/`.prevent`/`.once`/`.capture` modifiers), refs (`ref=`), and nested templates.
|
|
16059
|
+
|
|
16060
|
+
`.prop=${value}` always writes the element property (never an attribute): the explicit channel for structured
|
|
16061
|
+
values (arrays, objects, callbacks). Prototype-polluting names (`__proto__`, `constructor`, `prototype`) are
|
|
16062
|
+
rejected at compile time.
|
|
16063
|
+
|
|
16064
|
+
`@event.stop`, `.prevent`, `.once`, and `.capture` are shorthand for the matching listener behavior
|
|
16065
|
+
(`stopPropagation`/`preventDefault` applied before the handler runs; `once`/`capture` passed as listener
|
|
16066
|
+
options). An unknown modifier throws at compile time.
|
|
15752
16067
|
|
|
15753
16068
|
### `css`
|
|
15754
16069
|
|
|
@@ -15763,9 +16078,28 @@ Tagged template literal that returns a `CSSResult` for use in `styles`.
|
|
|
15763
16078
|
| `classMap(record)` | Reactive class string from object map |
|
|
15764
16079
|
| `styleMap(record)` | Reactive inline style string from object map |
|
|
15765
16080
|
| `live(signal)` | One-way binding that skips stale writes during active user input; use with `@input` handler |
|
|
16081
|
+
| `model(signal, options?)` | Two-way form-control binding: writes the signal to `value`/`checked` and writes user input back on the control's change event |
|
|
15766
16082
|
| `unsafeHtml(value)` | HTML rendering sink; sanitize untrusted values before calling |
|
|
15767
16083
|
|
|
15768
|
-
`each()` treats number and string keys as distinct. A duplicate
|
|
16084
|
+
`each()` treats number and string keys as distinct. A reconciliation failure (a duplicate key, or a render function that throws) reports `ore:error` and preserves the last valid list: entries created during the failed pass are rolled back, so the previous list stays exactly on screen.
|
|
16085
|
+
|
|
16086
|
+
### `model(signal, options?)`
|
|
16087
|
+
|
|
16088
|
+
Two-way binding for a form control: `value=${model(signal)}`. It writes the signal to the control's `value`/`checked` property on every change and writes user input back into the signal on the control's change event (`'change'` for checkboxes, radios, and ``; `'input'` otherwise). This is the complete controlled-input loop that the `live()` + manual `@input` handler pair used to require.
|
|
16089
|
+
|
|
16090
|
+
```ts
|
|
16091
|
+
const text = signal('');
|
|
16092
|
+
const agree = signal(false);
|
|
16093
|
+
const count = signal(0);
|
|
16094
|
+
|
|
16095
|
+
html`
|
|
16096
|
+
|
|
16097
|
+
|
|
16098
|
+
Number(el.value) })} />
|
|
16099
|
+
`;
|
|
16100
|
+
```
|
|
16101
|
+
|
|
16102
|
+
`options` overrides the defaults for non-native controls: `event` sets the write-back event name and `get` reads the current value out of the element (default `el.checked` for checkboxes/radios, `el.value` otherwise). Because a native text input's `value` is always a string, pass a `get` like `(el) => Number(el.value)` to keep a numeric signal numeric. `model()` can only drive a plain `value`/`checked` attribute binding; using it on a boolean (`?checked=`) or other attribute throws.
|
|
15769
16103
|
|
|
15770
16104
|
### `unsafeHtml`
|
|
15771
16105
|
|
|
@@ -15865,6 +16199,8 @@ Import from `@vielzeug/ore`.
|
|
|
15865
16199
|
Wire a form-associated element to `ElementInternals`. Requires `formAssociated: true` on the component definition. The `disabled` state tracking via `internals.states` (CustomStateSet) is skipped with a dev warning if the API is unavailable in the current environment.
|
|
15866
16200
|
|
|
15867
16201
|
```ts
|
|
16202
|
+
type FormFieldValidity = { flags: ValidityStateFlags; message: string };
|
|
16203
|
+
|
|
15868
16204
|
type FormFieldOptions = {
|
|
15869
16205
|
disabled?: Readable;
|
|
15870
16206
|
/**
|
|
@@ -15877,8 +16213,13 @@ type FormFieldOptions = {
|
|
|
15877
16213
|
/** Called when the ancestor resets (see onFormReset): restore local field state here. */
|
|
15878
16214
|
onReset?: () => void;
|
|
15879
16215
|
toFormValue?: (value: T) => File | FormData | string | null;
|
|
15880
|
-
/**
|
|
15881
|
-
|
|
16216
|
+
/**
|
|
16217
|
+
* Recomputed reactively and passed straight to internals.setValidity().
|
|
16218
|
+
* null (or omitted) = always valid. Flags and message travel together because
|
|
16219
|
+
* the platform's setValidity() contract couples them (flags without a
|
|
16220
|
+
* non-empty message throw); a single { flags, message } | null input makes a
|
|
16221
|
+
* desynced pair unrepresentable.
|
|
16222
|
+
*/
|
|
15882
16223
|
validity?: Readable;
|
|
15883
16224
|
value: Signal | Readable;
|
|
15884
16225
|
};
|
|
@@ -15892,15 +16233,20 @@ type FormFieldHandle = {
|
|
|
15892
16233
|
};
|
|
15893
16234
|
```
|
|
15894
16235
|
|
|
15895
|
-
Pass `validity
|
|
15896
|
-
through `checkValidity()` and `reportValidity()
|
|
16236
|
+
Pass a single `validity` signal to make `required`-style constraints participate in native constraint validation
|
|
16237
|
+
through `checkValidity()` and `reportValidity()`. The `{ flags, message }` pair is the one validity input: the old
|
|
16238
|
+
split `validity` + `validationMessage` options are gone, since they could drift apart into a state the platform
|
|
16239
|
+
rejects.
|
|
15897
16240
|
|
|
15898
16241
|
```ts
|
|
15899
16242
|
const isBlank = (v: string) => v.trim() === '';
|
|
15900
16243
|
|
|
15901
16244
|
useField({
|
|
15902
|
-
|
|
15903
|
-
|
|
16245
|
+
validity: computed(() =>
|
|
16246
|
+
required.value && isBlank(value.value)
|
|
16247
|
+
? { flags: { valueMissing: true }, message: 'This field is required.' }
|
|
16248
|
+
: null,
|
|
16249
|
+
),
|
|
15904
16250
|
value,
|
|
15905
16251
|
});
|
|
15906
16252
|
```
|
|
@@ -15913,8 +16259,7 @@ Import from `@vielzeug/ore/testing`.
|
|
|
15913
16259
|
| ------------------------ | ------------------------------------------------------------------------------------------ |
|
|
15914
16260
|
| `mount(setup, options?)` | Mount a component and return a test fixture |
|
|
15915
16261
|
| `cleanup()` | Remove all mounted elements and reset test state |
|
|
15916
|
-
| `install(afterEach
|
|
15917
|
-
| `installFormInternalsPolyfill()` | Installs the form-internals polyfill directly (returns an `uninstall()` that restores every patched global). Usually called via `install(afterEach, { formInternals: true })` |
|
|
16262
|
+
| `install(afterEach)` | Register auto-cleanup after each test |
|
|
15918
16263
|
| `flush(options?)` | Drain reactive updates and animation frames |
|
|
15919
16264
|
| `renderHook(setup)` | Run lifecycle hooks in isolation; overload accepts `propDefs` as first arg for typed props |
|
|
15920
16265
|
| `OreTimeoutError` | Error thrown when `flush()` cannot settle tracked Ore work |
|
|
@@ -15924,7 +16269,16 @@ Import from `@vielzeug/ore/testing`.
|
|
|
15924
16269
|
Import `within`, named dispatchers such as `fireClick`, and waits such as `waitUntil` or `waitForEvent` from
|
|
15925
16270
|
`@vielzeug/assay`.
|
|
15926
16271
|
|
|
15927
|
-
> **Form-associated component testing:** jsdom implements none of the `ElementInternals` form-association API
|
|
16272
|
+
> **Form-associated component testing:** jsdom implements none of the `ElementInternals` form-association API.
|
|
16273
|
+
> `@vielzeug/assay` exports `installFormInternalsPolyfill()` for this: it polyfills
|
|
16274
|
+
> `setFormValue`/`setValidity`/`checkValidity`/`reportValidity`/`validationMessage`/`validity`/`states`, mixes
|
|
16275
|
+
> `checkValidity`/`reportValidity`/`validity`/`validationMessage` onto the host element itself (real browsers do
|
|
16276
|
+
> this for any `formAssociated: true` element), makes `FormData` collect a form-associated element's set value, and
|
|
16277
|
+
> makes `.reset()` invoke `formResetCallback()`. Every patch is a guarded no-op when its target already
|
|
16278
|
+
> exists, and the function returns an `uninstall()` that restores every patched global. Call it directly from a
|
|
16279
|
+
> test setup (alongside `install(afterEach)`) only when the suite tests form-associated components: the patches
|
|
16280
|
+
> are global, so suites without them shouldn't carry them. A downstream package (e.g. a component library built
|
|
16281
|
+
> on `ore`) should rely on it instead of hand-rolling its own copy.
|
|
15928
16282
|
|
|
15929
16283
|
#### `Fixture` interface
|
|
15930
16284
|
|
|
@@ -16093,18 +16447,26 @@ type CSSResult = {
|
|
|
16093
16447
|
|
|
16094
16448
|
type LiveBinding = { readonly source: Readable };
|
|
16095
16449
|
|
|
16450
|
+
type ModelOptions = {
|
|
16451
|
+
/** Write-back event (default: 'change' for checkbox/radio/select, 'input' otherwise). */
|
|
16452
|
+
event?: string;
|
|
16453
|
+
/** Value reader (default: el.checked for checkbox/radio, el.value otherwise). */
|
|
16454
|
+
get?: (el: HTMLElement) => T;
|
|
16455
|
+
};
|
|
16456
|
+
|
|
16096
16457
|
type EmitFn> = {
|
|
16097
16458
|
>(event: K): boolean;
|
|
16098
16459
|
>>(event: K, detail: T[K]): boolean;
|
|
16099
16460
|
};
|
|
16100
16461
|
// KeysWithoutDetail is an internal helper type, not exported.
|
|
16101
16462
|
|
|
16463
|
+
type FormFieldValidity = { flags: ValidityStateFlags; message: string };
|
|
16464
|
+
|
|
16102
16465
|
type FormFieldOptions = {
|
|
16103
16466
|
disabled?: Readable;
|
|
16104
16467
|
emptyStringForNull?: boolean;
|
|
16105
16468
|
onReset?: () => void;
|
|
16106
16469
|
toFormValue?: (value: T) => File | FormData | string | null;
|
|
16107
|
-
validationMessage?: Readable;
|
|
16108
16470
|
validity?: Readable;
|
|
16109
16471
|
value: Signal | Readable;
|
|
16110
16472
|
};
|
|
@@ -16258,11 +16620,13 @@ define('x-button', {
|
|
|
16258
16620
|
|
|
16259
16621
|
## template bindings
|
|
16260
16622
|
|
|
16261
|
-
`html` supports text, attributes, booleans, properties, events, refs, and nested templates.
|
|
16623
|
+
`html` supports text, attributes, booleans, properties, events, refs, and nested templates. Event bindings accept
|
|
16624
|
+
`.stop`, `.prevent`, `.once`, and `.capture`: the first two apply before the handler runs, while the latter two use
|
|
16625
|
+
native listener options. Unknown modifiers throw while Ore compiles the template.
|
|
16262
16626
|
|
|
16263
16627
|
```ts
|
|
16264
16628
|
import { computed, signal } from '@vielzeug/ripple';
|
|
16265
|
-
import { define, html, ref } from '@vielzeug/ore';
|
|
16629
|
+
import { define, html, model, ref } from '@vielzeug/ore';
|
|
16266
16630
|
|
|
16267
16631
|
define('profile-name', {
|
|
16268
16632
|
setup() {
|
|
@@ -16271,10 +16635,7 @@ define('profile-name', {
|
|
|
16271
16635
|
|
|
16272
16636
|
return html`
|
|
16273
16637
|
'Current: ' + name.value)}>Name
|
|
16274
|
-
'Current name ' + name.value}
|
|
16275
|
-
@input=${(event: Event) => {
|
|
16276
|
-
name.value = (event.target as HTMLInputElement).value;
|
|
16277
|
-
}} />
|
|
16638
|
+
'Current name ' + name.value} />
|
|
16278
16639
|
Hello ${name}
|
|
16279
16640
|
`;
|
|
16280
16641
|
},
|
|
@@ -16283,8 +16644,7 @@ define('profile-name', {
|
|
|
16283
16644
|
|
|
16284
16645
|
## directives
|
|
16285
16646
|
|
|
16286
|
-
Ore exports `each`, `classMap`, `styleMap`, `when`, `live`, and `unsafeHtml` from `@vielzeug/ore`.
|
|
16287
|
-
attribute bindings plus native event handlers for two-way input state; no special model directive is required.
|
|
16647
|
+
Ore exports `each`, `classMap`, `styleMap`, `when`, `live`, `model`, and `unsafeHtml` from `@vielzeug/ore`.
|
|
16288
16648
|
|
|
16289
16649
|
```ts
|
|
16290
16650
|
import { signal } from '@vielzeug/ripple';
|
|
@@ -16332,9 +16692,50 @@ each(
|
|
|
16332
16692
|
);
|
|
16333
16693
|
```
|
|
16334
16694
|
|
|
16335
|
-
|
|
16695
|
+
Each reactive reconciliation is transactional. Ore creates missing entries before committing updates or removing stale
|
|
16696
|
+
entries; if a render callback throws, entries created during that pass are rolled back and the last successful list
|
|
16697
|
+
remains on screen. Ore reports the failure through `ore:error` with phase `each-reconcile`.
|
|
16336
16698
|
|
|
16337
|
-
|
|
16699
|
+
## model: two-way form bindings
|
|
16700
|
+
|
|
16701
|
+
Use `model(signal)` for a controlled input: it writes the signal to the control's `value`/`checked` property and
|
|
16702
|
+
writes user input back on the control's change event (`'change'` for checkboxes, radios, and selects; `'input'`
|
|
16703
|
+
otherwise). This replaces the `live()` + manual `@input` handler pair with one binding.
|
|
16704
|
+
|
|
16705
|
+
```ts
|
|
16706
|
+
import { signal } from '@vielzeug/ripple';
|
|
16707
|
+
import { define, html, model } from '@vielzeug/ore';
|
|
16708
|
+
|
|
16709
|
+
define('live-search', {
|
|
16710
|
+
setup() {
|
|
16711
|
+
const query = signal('');
|
|
16712
|
+
const agree = signal(false);
|
|
16713
|
+
|
|
16714
|
+
return html`
|
|
16715
|
+
|
|
16716
|
+
|
|
16717
|
+
`;
|
|
16718
|
+
},
|
|
16719
|
+
});
|
|
16720
|
+
```
|
|
16721
|
+
|
|
16722
|
+
A native text input's `value` is always a string, so pass a `get` reader to keep a numeric signal numeric, and an
|
|
16723
|
+
`event` name for custom controls that emit their own change event:
|
|
16724
|
+
|
|
16725
|
+
```ts
|
|
16726
|
+
const count = signal(0);
|
|
16727
|
+
|
|
16728
|
+
html`
|
|
16729
|
+
Number(el.value) })} />
|
|
16730
|
+
(el as MyPicker).selection })} />
|
|
16731
|
+
`;
|
|
16732
|
+
```
|
|
16733
|
+
|
|
16734
|
+
## live: one-way bindings that preserve user edits
|
|
16735
|
+
|
|
16736
|
+
Use `live(signal)` when a binding must skip a stale app-state write while the user is mid-edit, without writing
|
|
16737
|
+
user input back. `model()` covers the common controlled-input case; `live()` is for one-way bindings that only
|
|
16738
|
+
need the stale-write guard.
|
|
16338
16739
|
|
|
16339
16740
|
```ts
|
|
16340
16741
|
import { signal } from '@vielzeug/ripple';
|
|
@@ -16698,7 +17099,7 @@ define('signup-form', {
|
|
|
16698
17099
|
- Bind host attributes and classes via `bind()` rather than mutating the element directly.
|
|
16699
17100
|
- Provide context at the nearest ancestor: avoid global context singletons.
|
|
16700
17101
|
- Call `onCleanup()` for every resource allocated in `setup()` (WebSockets, intervals, external subscriptions).
|
|
16701
|
-
- Use `
|
|
17102
|
+
- Use `model(signal)` for two-way form inputs; use `live(signal)` for a one-way binding that must not clobber user-in-progress edits.
|
|
16702
17103
|
- Extract composable helper functions freely: `onMounted`/`onCleanup`/`bind`/... resolve the active component through implicit context, so they work from any function called (transitively) during `setup()`, with no need to pass them in as parameters.
|
|
16703
17104
|
- Test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic DOM events, queries, and waits
|
|
16704
17105
|
from `@vielzeug/assay`.
|
|
@@ -20417,6 +20818,7 @@ Classification guides discovery and bundle selection. It is not a reason to remo
|
|
|
20417
20818
|
- **Framework agnostic**: works anywhere HTML can be rendered
|
|
20418
20819
|
- **Tree-shakeable**: import only the component entry points you register
|
|
20419
20820
|
- **Comprehensive surface**: inputs, content, disclosure, feedback, layout, and overlay primitives
|
|
20821
|
+
- **Native form integration**: form-associated controls submit values, validate supported required constraints, and reset with their form
|
|
20420
20822
|
- **Typed framework declarations**: DOM, React, and Vue tags derive from the component property contracts
|
|
20421
20823
|
|
|
20422
20824
|
### Prerequisites
|
|
@@ -20520,6 +20922,14 @@ The declarations cover every supported tag and derive component properties from
|
|
|
20520
20922
|
|
|
20521
20923
|
Form controls expose `.value` or `.checked` and dispatch standard `input` and `change` events. Read the property from `event.currentTarget`.
|
|
20522
20924
|
|
|
20925
|
+
Finite-choice properties use Ore's `prop.oneOf()` parsing: values outside a component's documented options resolve to
|
|
20926
|
+
the declared default, or `undefined` when no default is declared.
|
|
20927
|
+
|
|
20928
|
+
Form-associated controls participate in native form submission, validity checks, and reset. A named control's value
|
|
20929
|
+
is available through `new FormData(form)`, and supported `required` constraints affect `form.checkValidity()`,
|
|
20930
|
+
`form.reportValidity()`, and submit blocking. The `error` property renders and announces feedback; it does not by
|
|
20931
|
+
itself set native constraint validity.
|
|
20932
|
+
|
|
20523
20933
|
Stateful overlays expose controlled `open`, optional `default-open`, and an `open-change` custom event with `{ open, reason }` detail. Component pages define additional detail fields and reasons.
|
|
20524
20934
|
|
|
20525
20935
|
## Utilities
|
|
@@ -20625,6 +21035,20 @@ grid.rows = [
|
|
|
20625
21035
|
];
|
|
20626
21036
|
```
|
|
20627
21037
|
|
|
21038
|
+
Enumerated properties such as `variant`, `size`, and `color` accept the values listed on each component's reference
|
|
21039
|
+
page. An unsupported value resolves to the property's declared default (or `undefined` when it has no default).
|
|
21040
|
+
|
|
21041
|
+
In an Ore template, `model()` can bind a Refine field's `value` to a Ripple signal and write user input back:
|
|
21042
|
+
|
|
21043
|
+
```ts
|
|
21044
|
+
import { signal } from '@vielzeug/ripple';
|
|
21045
|
+
import { html, model } from '@vielzeug/ore';
|
|
21046
|
+
import '@vielzeug/refine/input';
|
|
21047
|
+
|
|
21048
|
+
const email = signal('');
|
|
21049
|
+
const field = html``;
|
|
21050
|
+
```
|
|
21051
|
+
|
|
20628
21052
|
## Handle Events
|
|
20629
21053
|
|
|
20630
21054
|
Form controls dispatch standard `input` and `change` events. Read their current property from `event.currentTarget`.
|
|
@@ -20648,6 +21072,37 @@ dialog?.addEventListener('open-change', (event: CustomEvent) => {
|
|
|
20648
21072
|
});
|
|
20649
21073
|
```
|
|
20650
21074
|
|
|
21075
|
+
## Native Form Validation
|
|
21076
|
+
|
|
21077
|
+
Refine's form-associated controls integrate with native form submission, `FormData`, validity checks, and reset.
|
|
21078
|
+
Use `required` for a required-value constraint; setting `error` displays and announces feedback but does not, by
|
|
21079
|
+
itself, invalidate the form.
|
|
21080
|
+
|
|
21081
|
+
```html
|
|
21082
|
+
|
|
21083
|
+
|
|
21084
|
+
|
|
21085
|
+
Choose a role…
|
|
21086
|
+
Editor
|
|
21087
|
+
Admin
|
|
21088
|
+
|
|
21089
|
+
Save
|
|
21090
|
+
|
|
21091
|
+
import '@vielzeug/refine/input';
|
|
21092
|
+
import '@vielzeug/refine/select';
|
|
21093
|
+
import '@vielzeug/refine/button';
|
|
21094
|
+
|
|
21095
|
+
const form = document.querySelector('#profile-form');
|
|
21096
|
+
form.addEventListener('submit', (event) => {
|
|
21097
|
+
event.preventDefault();
|
|
21098
|
+
console.log(new FormData(form).get('email'));
|
|
21099
|
+
});
|
|
21100
|
+
|
|
21101
|
+
```
|
|
21102
|
+
|
|
21103
|
+
The browser runs native validation before dispatching `submit`. Call `form.reportValidity()` to show validation
|
|
21104
|
+
feedback directly, or `form.checkValidity()` for a boolean check without the browser's validation UI.
|
|
21105
|
+
|
|
20651
21106
|
## Compose Slots
|
|
20652
21107
|
|
|
20653
21108
|
Default slots hold primary content. Named slots identify component-owned regions.
|
|
@@ -20842,16 +21297,16 @@ ripple.dispose();
|
|
|
20842
21297
|
|
|
20843
21298
|
## Features
|
|
20844
21299
|
|
|
20845
|
-
- `createRipple()` creates an isolated graph
|
|
21300
|
+
- `createRipple({ onError })` creates an isolated graph with one runtime failure callback.
|
|
20846
21301
|
- `signal()` stores writable values with configurable equality.
|
|
20847
21302
|
- `computed()` derives lazy read-only values.
|
|
20848
21303
|
- `effect()` reacts to dependency changes with cleanup support.
|
|
20849
21304
|
- `batch()` coalesces synchronous writes and notifications.
|
|
20850
21305
|
- `createScope()` groups owned reactive work.
|
|
20851
21306
|
- `watch()` observes one selected source transition.
|
|
20852
|
-
- `tap()` provides runtime observability for all graph events.
|
|
20853
21307
|
- `resource()` owns focused latest-request async state.
|
|
20854
21308
|
- `fromSubscribable()` creates a disposable bridge for external `{ getSnapshot, subscribe }` sources.
|
|
21309
|
+
- `RippleError`, `RippleCycleError`, and `RippleDisposedError` distinguish package-originated failures.
|
|
20855
21310
|
|
|
20856
21311
|
## Documentation
|
|
20857
21312
|
|
|
@@ -20880,15 +21335,18 @@ ripple.dispose();
|
|
|
20880
21335
|
| `createScope()` | Group owned reactive work | Sync | Call `run()` to activate it |
|
|
20881
21336
|
| `untrack()` | Read without tracking | Sync | Read still happens immediately |
|
|
20882
21337
|
| `watch()` | Observe selected output | Sync | Use `effect()` for broad reads |
|
|
20883
|
-
| `
|
|
21338
|
+
| `resource()` | Load from tracked reactive input | Sync factory / Async loader | `reload()` returns `void`; completion is observable state |
|
|
20884
21339
|
| `fromSubscribable()` | Bridge external source | Sync | Returned bridge must be disposed |
|
|
20885
21340
|
| `isReactive()` | Test Ripple's marked readable contract | Sync | Not a security or provenance check |
|
|
21341
|
+
| `RippleError` | Base package error | Sync | Runtime callback failures retain their original value |
|
|
21342
|
+
| `RippleCycleError` | Computed cycle or runaway flush | Sync | Flush failures reach `onError`, not the write site |
|
|
21343
|
+
| `RippleDisposedError` | Work after disposal | Sync | Cached reads remain available |
|
|
20886
21344
|
|
|
20887
21345
|
## Package Entry Point
|
|
20888
21346
|
|
|
20889
21347
|
| Import | Purpose |
|
|
20890
21348
|
| --- | --- |
|
|
20891
|
-
| `@vielzeug/ripple` | All primitives, types, and errors: signals, computed, effects, scopes, watch,
|
|
21349
|
+
| `@vielzeug/ripple` | All primitives, types, and errors: signals, computed, effects, scopes, watch, and the isolated graph factory |
|
|
20892
21350
|
|
|
20893
21351
|
## Graph Creation
|
|
20894
21352
|
|
|
@@ -20898,11 +21356,14 @@ ripple.dispose();
|
|
|
20898
21356
|
function createRipple(options?: RippleOptions): Ripple;
|
|
20899
21357
|
```
|
|
20900
21358
|
|
|
20901
|
-
Creates one isolated reactive graph. Factories on the returned object share scheduling, ownership, and error boundaries. `dispose()` is terminal: `ripple.disposed` becomes `true`, existing owned work is disposed, and creating more graph work throws `
|
|
21359
|
+
Creates one isolated reactive graph. Factories on the returned object share scheduling, ownership, and error boundaries. `dispose()` is terminal: `ripple.disposed` becomes `true`, existing owned work is disposed, and creating more graph work throws `RippleDisposedError`. Create a new graph for a new lifetime.
|
|
20902
21360
|
|
|
20903
21361
|
| Parameter | Type | Description |
|
|
20904
21362
|
| --- | --- | --- |
|
|
20905
|
-
| `options.
|
|
21363
|
+
| `options.onError` | `(error, context) => void` | Handles computed refresh, effect, cleanup, listener, and flush-loop failures. Default rethrows asynchronously via `queueMicrotask`; pass a no-op to silence. A failure in one node never interrupts sibling propagation. |
|
|
21364
|
+
|
|
21365
|
+
`onError` is the only runtime option. `RippleErrorContext.kind` is `cleanup`, `computed`,
|
|
21366
|
+
`effect`, `flush`, or `listener`; `name` is optional. There is no event stream or error-policy option.
|
|
20906
21367
|
|
|
20907
21368
|
**Returns:** `Ripple`.
|
|
20908
21369
|
|
|
@@ -20941,7 +21402,7 @@ console.log(isReactive(signal(0)));
|
|
|
20941
21402
|
|
|
20942
21403
|
## Default Graph Functions
|
|
20943
21404
|
|
|
20944
|
-
Root helpers (`signal`, `computed`, `effect`, `batch`, `createScope`, `untrack`, `watch`, `resource`, `fromSubscribable`) operate on a process-lifetime default graph.
|
|
21405
|
+
Root helpers (`signal`, `computed`, `effect`, `batch`, `createScope`, `untrack`, `watch`, `resource`, `fromSubscribable`) operate on a process-lifetime default graph. The graph itself has no public disposal handle; effects, scopes, resources, and bridges still require owner-managed disposal. For isolated, disposable graphs (tests, SSR, embedded features) use `createRipple()`.
|
|
20945
21406
|
|
|
20946
21407
|
### `signal(initial, options?)`
|
|
20947
21408
|
|
|
@@ -20973,7 +21434,7 @@ cart.update((state) => ({ ...state, items: state.items + 1 }));
|
|
|
20973
21434
|
function computed(derive: () => T, options?: ComputedOptions): Readable;
|
|
20974
21435
|
```
|
|
20975
21436
|
|
|
20976
|
-
Creates a lazy read-only value from reactive reads in `derive`. `peek()` may trigger derivation and throw directly. During dependency propagation, a computed refresh failure is reported
|
|
21437
|
+
Creates a lazy read-only value from reactive reads in `derive`. `peek()` may trigger derivation and throw directly. During dependency propagation, a computed refresh failure is reported to `onError` with context kind `computed` and does not prevent sibling dependents from updating. A disposed computed returns its last cached value from `peek()` without recomputing; a computed disposed before its first evaluation throws `RippleDisposedError` instead of returning a value.
|
|
20977
21438
|
|
|
20978
21439
|
**Returns:** `Readable`.
|
|
20979
21440
|
|
|
@@ -20997,6 +21458,12 @@ function effect(callback: () => Cleanup | undefined, options?: EffectOptions): E
|
|
|
20997
21458
|
|
|
20998
21459
|
Runs immediately and reruns when its tracked reads change. A returned cleanup runs before the next callback or disposal.
|
|
20999
21460
|
|
|
21461
|
+
| Parameter | Type | Description |
|
|
21462
|
+
| --- | --- | --- |
|
|
21463
|
+
| `options.name` | `string` | Optional label in error context |
|
|
21464
|
+
|
|
21465
|
+
Effects flush synchronously; `EffectOptions` has no scheduler option. Use `batch()` to coalesce writes.
|
|
21466
|
+
|
|
21000
21467
|
**Returns:** `EffectHandle`.
|
|
21001
21468
|
|
|
21002
21469
|
**Example:**
|
|
@@ -21080,16 +21547,31 @@ function resource(
|
|
|
21080
21547
|
|
|
21081
21548
|
Creates a disposable readable async state and starts loading immediately. Source changes and `reload()` abort stale work before starting the next loader. The last successful value is preserved as `previous`, including when that value is `undefined`.
|
|
21082
21549
|
|
|
21550
|
+
| Parameter | Type | Description |
|
|
21551
|
+
| --- | --- | --- |
|
|
21552
|
+
| `source` | `() => Source` | Tracks reactive input reads |
|
|
21553
|
+
| `loader` | `(source, context) => Promise` | Receives input and a request-owned cancellation signal |
|
|
21554
|
+
| `options.name` | `string` | Optional resource label |
|
|
21555
|
+
|
|
21556
|
+
**Returns:** `Resource`. Source and loader failures become error state, not `onError` reports.
|
|
21557
|
+
|
|
21558
|
+
**Example:**
|
|
21559
|
+
|
|
21083
21560
|
```ts
|
|
21561
|
+
import { resource, signal } from '@vielzeug/ripple';
|
|
21562
|
+
|
|
21563
|
+
const userId = signal('42');
|
|
21084
21564
|
const user = resource(
|
|
21085
21565
|
() => userId.value,
|
|
21086
21566
|
(id, { signal }) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),
|
|
21087
21567
|
);
|
|
21568
|
+
console.log(user.value);
|
|
21569
|
+
user.dispose();
|
|
21088
21570
|
```
|
|
21089
21571
|
|
|
21090
|
-
The state is `{ status: 'pending', previous? }`, `{ status: 'success', value }`, or `{ status: 'error', error, previous? }`. Reads remain available after disposal; `reload()` and new subscriptions throw `
|
|
21572
|
+
The state is `{ status: 'pending', previous? }`, `{ status: 'success', value }`, or `{ status: 'error', error, previous? }`. Reads remain available after disposal; `reload()` and new subscriptions throw `RippleDisposedError`.
|
|
21091
21573
|
|
|
21092
|
-
## Watch
|
|
21574
|
+
## Watch
|
|
21093
21575
|
|
|
21094
21576
|
### `watch(source, callback, options?)`
|
|
21095
21577
|
|
|
@@ -21117,41 +21599,6 @@ stop.dispose();
|
|
|
21117
21599
|
|
|
21118
21600
|
---
|
|
21119
21601
|
|
|
21120
|
-
### `ripple.tap(handler, options?)`
|
|
21121
|
-
|
|
21122
|
-
```ts
|
|
21123
|
-
function tap(
|
|
21124
|
-
handler: (event: RippleEvent) => void,
|
|
21125
|
-
options?: { signal?: AbortSignal },
|
|
21126
|
-
): Unsubscribe;
|
|
21127
|
-
```
|
|
21128
|
-
|
|
21129
|
-
Runtime observability for writes, computes, effects, disposals, and reported errors. Computed refresh errors are reported with context kind `computed` without interrupting sibling propagation. During graph disposal, taps remain active for owned cleanup errors and node disposal events; `dispose:graph` is emitted last. Handler errors are swallowed. The returned function unsubscribes, and an optional signal auto-detaches it.
|
|
21130
|
-
|
|
21131
|
-
The `Ripple.tap()` method is available on instances created with `createRipple()`. After runtime disposal it returns a no-op unsubscribe.
|
|
21132
|
-
|
|
21133
|
-
**Returns:** `Unsubscribe`.
|
|
21134
|
-
|
|
21135
|
-
**Example:**
|
|
21136
|
-
|
|
21137
|
-
```ts
|
|
21138
|
-
import { createRipple } from '@vielzeug/ripple';
|
|
21139
|
-
|
|
21140
|
-
const ripple = createRipple({ errorPolicy: 'swallow' });
|
|
21141
|
-
const stop = ripple.tap((event) => {
|
|
21142
|
-
if (event.type === 'error') console.error(event.context.kind, event.error);
|
|
21143
|
-
if (event.type === 'write') console.log(`${event.name}: ${event.previous} → ${event.next}`);
|
|
21144
|
-
});
|
|
21145
|
-
|
|
21146
|
-
const count = ripple.signal(0);
|
|
21147
|
-
count.value = 1;
|
|
21148
|
-
|
|
21149
|
-
stop();
|
|
21150
|
-
ripple.dispose();
|
|
21151
|
-
```
|
|
21152
|
-
|
|
21153
|
-
---
|
|
21154
|
-
|
|
21155
21602
|
### `fromSubscribable(source, options?)`
|
|
21156
21603
|
|
|
21157
21604
|
```ts
|
|
@@ -21167,12 +21614,20 @@ Bridges an external `{ getSnapshot, subscribe }` source into a disposable reacti
|
|
|
21167
21614
|
```ts
|
|
21168
21615
|
import { fromSubscribable } from '@vielzeug/ripple';
|
|
21169
21616
|
|
|
21170
|
-
|
|
21171
|
-
|
|
21172
|
-
|
|
21617
|
+
let current = 'idle';
|
|
21618
|
+
const listeners = new Set void>();
|
|
21619
|
+
const state = fromSubscribable({
|
|
21620
|
+
getSnapshot: () => current,
|
|
21621
|
+
subscribe: (listener) => {
|
|
21622
|
+
listeners.add(listener);
|
|
21623
|
+
return () => { listeners.delete(listener); };
|
|
21624
|
+
},
|
|
21173
21625
|
});
|
|
21174
21626
|
|
|
21175
|
-
|
|
21627
|
+
current = 'ready';
|
|
21628
|
+
for (const listener of listeners) listener();
|
|
21629
|
+
console.log(state.value);
|
|
21630
|
+
state.dispose();
|
|
21176
21631
|
```
|
|
21177
21632
|
|
|
21178
21633
|
## Types
|
|
@@ -21187,20 +21642,13 @@ type ComputedOptions = { equals?: Equality; name?: string };
|
|
|
21187
21642
|
type EffectOptions = { name?: string };
|
|
21188
21643
|
type WatchOptions = { equals?: Equality; immediate?: boolean; name?: string };
|
|
21189
21644
|
|
|
21190
|
-
type RippleErrorContext = { readonly kind: 'cleanup' | 'computed' | 'effect' | 'listener'; readonly name?: string };
|
|
21191
|
-
type
|
|
21192
|
-
type RippleOptions = { errorPolicy?: RippleErrorPolicy };
|
|
21193
|
-
|
|
21194
|
-
type RippleEvent =
|
|
21195
|
-
| { readonly type: 'compute'; readonly name?: string }
|
|
21196
|
-
| { readonly type: 'effect'; readonly name?: string }
|
|
21197
|
-
| { readonly type: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }
|
|
21198
|
-
| { readonly type: 'dispose'; readonly name?: string; readonly node: 'effect' | 'graph' | 'scope' }
|
|
21199
|
-
| { readonly type: 'error'; readonly error: unknown; readonly context: RippleErrorContext };
|
|
21645
|
+
type RippleErrorContext = { readonly kind: 'cleanup' | 'computed' | 'effect' | 'flush' | 'listener'; readonly name?: string };
|
|
21646
|
+
type RippleOptions = { onError?: (error: unknown, context: RippleErrorContext) => void };
|
|
21200
21647
|
|
|
21201
21648
|
interface Readable {
|
|
21202
21649
|
readonly name?: string;
|
|
21203
21650
|
peek(): T;
|
|
21651
|
+
/** Change-only: the listener is not called on subscribe, only after a tracked source changes. */
|
|
21204
21652
|
subscribe(listener: () => void): Unsubscribe;
|
|
21205
21653
|
readonly value: T;
|
|
21206
21654
|
}
|
|
@@ -21241,7 +21689,6 @@ interface Ripple extends Disposable {
|
|
|
21241
21689
|
options?: ResourceOptions,
|
|
21242
21690
|
): Resource;
|
|
21243
21691
|
signal(initial: T, options?: SignalOptions): Signal;
|
|
21244
|
-
tap(handler: (event: RippleEvent) => void, options?: { signal?: AbortSignal }): Unsubscribe;
|
|
21245
21692
|
untrack(fn: () => T): T;
|
|
21246
21693
|
watch(source: Readable | (() => T), callback: (value: T, previous: T | undefined) => void, options?: WatchOptions): EffectHandle;
|
|
21247
21694
|
}
|
|
@@ -21251,12 +21698,13 @@ interface Ripple extends Disposable {
|
|
|
21251
21698
|
|
|
21252
21699
|
| Error | Trigger | Notable properties |
|
|
21253
21700
|
| --- | --- | --- |
|
|
21254
|
-
| `RippleError` | Base Ripple error | Use `instanceof RippleError` to narrow unknown values. |
|
|
21255
|
-
| `
|
|
21256
|
-
| `
|
|
21257
|
-
|
|
21258
|
-
|
|
21259
|
-
|
|
21701
|
+
| `RippleError` | Base Ripple error; a new subscription to a disposed external-source bridge | Use `instanceof RippleError` to narrow unknown values. |
|
|
21702
|
+
| `RippleCycleError` | A computed reads itself, or a flush exceeds the iteration limit | Extends `RippleError`. A flush-loop breach is reported to `onError` (context kind `flush`) instead of throwing from the write site. |
|
|
21703
|
+
| `RippleDisposedError` | New graph work, writes, or subscriptions on a disposed runtime; `run()` on a disposed scope; `reload()` or new subscriptions on a disposed resource; reading a never-evaluated disposed computed | Extends `RippleError`. Cached reads and repeated disposal remain valid. |
|
|
21704
|
+
|
|
21705
|
+
These are the three exported error classes. User callback failures are reported as their original
|
|
21706
|
+
values through `onError`; they are not wrapped in `RippleError`. A direct computed read can throw
|
|
21707
|
+
its derivation error to the caller.
|
|
21260
21708
|
|
|
21261
21709
|
### Usage Guide
|
|
21262
21710
|
|
|
@@ -21282,10 +21730,8 @@ Use `createRipple()` for tests, SSR requests, embedded applications, or independ
|
|
|
21282
21730
|
```ts
|
|
21283
21731
|
import { createRipple } from '@vielzeug/ripple';
|
|
21284
21732
|
|
|
21285
|
-
const ripple = createRipple({
|
|
21286
|
-
|
|
21287
|
-
ripple.tap((event) => {
|
|
21288
|
-
if (event.type === 'error') console.log(event.context.kind, event.error);
|
|
21733
|
+
const ripple = createRipple({
|
|
21734
|
+
onError: (error, context) => console.log(context.kind, error),
|
|
21289
21735
|
});
|
|
21290
21736
|
|
|
21291
21737
|
const count = ripple.signal(0);
|
|
@@ -21371,26 +21817,28 @@ const stopWatch = ripple.watch(
|
|
|
21371
21817
|
stopWatch.dispose();
|
|
21372
21818
|
```
|
|
21373
21819
|
|
|
21374
|
-
##
|
|
21820
|
+
## Error Handling
|
|
21375
21821
|
|
|
21376
|
-
|
|
21822
|
+
Failures inside effect callbacks, computed derivations, cleanups, and listeners never corrupt the graph: the failing node is reported and siblings keep propagating. The `onError` option decides what reporting means. By default failures rethrow asynchronously (via `queueMicrotask`), surfacing as unhandled errors; pass a handler to route them to a logger or error boundary instead:
|
|
21377
21823
|
|
|
21378
21824
|
```ts
|
|
21379
|
-
const ripple = createRipple({
|
|
21380
|
-
|
|
21381
|
-
|
|
21382
|
-
|
|
21383
|
-
|
|
21384
|
-
console.log(`${event.name}: ${event.previous} → ${event.next}`);
|
|
21385
|
-
break;
|
|
21386
|
-
case 'error':
|
|
21387
|
-
console.error(`${event.context.kind}:`, event.error);
|
|
21388
|
-
break;
|
|
21389
|
-
}
|
|
21825
|
+
const ripple = createRipple({
|
|
21826
|
+
onError: (error, context) => {
|
|
21827
|
+
// context.kind: 'cleanup' | 'computed' | 'effect' | 'flush' | 'listener'
|
|
21828
|
+
console.error(`${context.kind}:`, error);
|
|
21829
|
+
},
|
|
21390
21830
|
});
|
|
21391
21831
|
```
|
|
21392
21832
|
|
|
21393
|
-
`
|
|
21833
|
+
`onError` is the only runtime option. Effect options accept only a `name`; effects do not have a
|
|
21834
|
+
microtask scheduler. Use `batch()` to coalesce related writes.
|
|
21835
|
+
|
|
21836
|
+
A direct computed read can throw its derivation error to the caller. `onError` handles computed
|
|
21837
|
+
refresh failures during propagation, not every possible synchronous exception. The exported
|
|
21838
|
+
hierarchy is `RippleError`, `RippleCycleError`, and `RippleDisposedError`; callback failures retain
|
|
21839
|
+
their original values.
|
|
21840
|
+
|
|
21841
|
+
A runaway effect (one that keeps re-triggering itself) is caught by the flush iteration limit and reported with context kind `flush`; the flush is abandoned and the graph stays usable for later writes.
|
|
21394
21842
|
|
|
21395
21843
|
## Bridging External Sources
|
|
21396
21844
|
|
|
@@ -21426,15 +21874,19 @@ const user = ripple.resource(
|
|
|
21426
21874
|
|
|
21427
21875
|
const stop = ripple.effect(() => {
|
|
21428
21876
|
const state = user.value;
|
|
21429
|
-
if (state.status === 'success')
|
|
21430
|
-
if (state.status === 'error')
|
|
21877
|
+
if (state.status === 'success') console.log(state.value);
|
|
21878
|
+
if (state.status === 'error') console.error(state.error);
|
|
21431
21879
|
});
|
|
21432
21880
|
|
|
21433
21881
|
stop.dispose();
|
|
21434
21882
|
user.dispose();
|
|
21435
21883
|
```
|
|
21436
21884
|
|
|
21437
|
-
|
|
21885
|
+
Source and loader failures become resource error state rather than `onError` reports.
|
|
21886
|
+
`reload()` returns `void`; observe state to know when loading finishes. Disposed resources retain
|
|
21887
|
+
readable state but throw `RippleDisposedError` from `reload()` and new subscriptions. Use Sourcerer
|
|
21888
|
+
for paginated collection state and a dedicated server-state cache for shared keyed reads, retries,
|
|
21889
|
+
and invalidation.
|
|
21438
21890
|
|
|
21439
21891
|
## Object State
|
|
21440
21892
|
|
|
@@ -21554,13 +22006,21 @@ ripple.dispose();
|
|
|
21554
22006
|
|
|
21555
22007
|
## Gotchas
|
|
21556
22008
|
|
|
22009
|
+
### `subscribe()` is change-only
|
|
22010
|
+
|
|
22011
|
+
`Readable.subscribe(listener)` never calls the listener on subscribe: it fires only after a tracked source changes. Read the current value at the call site when you need an initial state.
|
|
22012
|
+
|
|
21557
22013
|
### `subscribe()` forces computed evaluation
|
|
21558
22014
|
|
|
21559
22015
|
`Readable.subscribe()` calls `peek()` before registering the listener. For signals this is a no-op, but for computeds it forces `refresh()`: the derivation runs immediately even if no one reads `.value`. This ensures `equals` comparison works on the first dependency change. Avoid subscribing to expensive computeds unless you need their value.
|
|
21560
22016
|
|
|
22017
|
+
### Diamonds recompute each path once per write
|
|
22018
|
+
|
|
22019
|
+
A computed with dependents refreshes eagerly during propagation, so effects never re-run for unchanged derived values. The tradeoff: in a diamond (`a → b, c → d`), one write to `a` recomputes both `b` and `c` and then `d` once per changed path. Values a consumer observes are always consistent: a dependent reading a stale intermediate pulls the fresh value on read, so no glitched state is ever visible. Prefer keeping diamond depth shallow when a derivation is expensive.
|
|
22020
|
+
|
|
21561
22021
|
### Computed first-run failure is recoverable
|
|
21562
22022
|
|
|
21563
|
-
If a computed's `derive` throws on its first run (e.g., a source is `null`), the computed commits the partial dependencies it tracked before the throw. When a dependency changes and the derivation can succeed, the computed refreshes and notifies its dependents. Effects that read a failing computed report the error through `
|
|
22023
|
+
If a computed's `derive` throws on its first run (e.g., a source is `null`), the computed commits the partial dependencies it tracked before the throw. When a dependency changes and the derivation can succeed, the computed refreshes and notifies its dependents. Effects that read a failing computed report the error through `onError` and re-run when the computed recovers.
|
|
21564
22024
|
|
|
21565
22025
|
## Best Practices
|
|
21566
22026
|
|
|
@@ -21571,8 +22031,7 @@ If a computed's `derive` throws on its first run (e.g., a source is `null`), the
|
|
|
21571
22031
|
- Batch related synchronous writes.
|
|
21572
22032
|
- Use `watch()` only for selected source transitions; callback-only reads are untracked.
|
|
21573
22033
|
- Dispose external-source bridges when their owner ends.
|
|
21574
|
-
-
|
|
21575
|
-
- Use `resource()` for focused async state and Sourcerer for paginated collection workflows.
|
|
22034
|
+
- Route runtime failures with `onError` and observe async failures through resource state.
|
|
21576
22035
|
|
|
21577
22036
|
### Examples
|
|
21578
22037
|
|
|
@@ -21591,7 +22050,7 @@ If a computed's `derive` throws on its first run (e.g., a source is `null`), the
|
|
|
21591
22050
|
- Async Resource (id: `async-resource`)
|
|
21592
22051
|
- Create Graph, Signal, Computed & Effect (id: `basic-signal`)
|
|
21593
22052
|
- Batch & Untrack (id: `batch-untrack`)
|
|
21594
|
-
-
|
|
22053
|
+
- Named Effect and onError (id: `effect-options`)
|
|
21595
22054
|
- Nested Effect Ownership (id: `scope-ownership`)
|
|
21596
22055
|
- Immutable State (id: `store-basics`)
|
|
21597
22056
|
- Watch Selected Value (id: `watch-selected-value`)
|
|
@@ -28350,7 +28809,9 @@ try {
|
|
|
28350
28809
|
- `createPageSource()`: loads numbered pages and exposes direct navigation commands
|
|
28351
28810
|
- `createCursorSource()`: follows opaque cursors returned by the loader
|
|
28352
28811
|
- `createInfiniteSource()`: appends pages while preserving loaded items
|
|
28812
|
+
- `createResource()`: starts loading one value immediately, or awaits `reload()` with `start: false`
|
|
28353
28813
|
- `setParams()`: replaces consumer-owned loader parameters and resets pagination
|
|
28814
|
+
- `PageSource.apply()`: reconciles page and structurally compared params in at most one request
|
|
28354
28815
|
- `subscribe()`: publishes complete immutable state replacements
|
|
28355
28816
|
|
|
28356
28817
|
## Documentation
|
|
@@ -28377,7 +28838,10 @@ try {
|
|
|
28377
28838
|
| `createPageSource()` | Load numbered pages | Async | Construction is inert |
|
|
28378
28839
|
| `createCursorSource()` | Load server-issued cursor pages | Async | `after` and `before` cannot coexist initially |
|
|
28379
28840
|
| `createInfiniteSource()` | Append numbered pages | Async | `loadMore()` is a no-op while loading or exhausted |
|
|
28841
|
+
| `createResource()` | Load one reactive value | Async | Starts loading during construction; `start: false` opts out |
|
|
28380
28842
|
| `SourcererError` | Base package error | Sync | Loader errors retain their original type |
|
|
28843
|
+
| `SourcererConfigurationError` | Invalid pagination configuration | Sync | Pages and page sizes must be positive integers |
|
|
28844
|
+
| `SourcererDisposedError` | Command after source disposal | Sync | In-flight work instead settles without committing |
|
|
28381
28845
|
|
|
28382
28846
|
## Package Entry Point
|
|
28383
28847
|
|
|
@@ -28462,6 +28926,7 @@ await source.reload();
|
|
|
28462
28926
|
|
|
28463
28927
|
| Method | Returns | Description |
|
|
28464
28928
|
| --- | --- | --- |
|
|
28929
|
+
| `apply(update)` | `Promise` | Reconcile `{ params, page }` in at most one load; params compare structurally |
|
|
28465
28930
|
| `first()` | `Promise` | Load page one |
|
|
28466
28931
|
| `goTo(page)` | `Promise` | Load a positive page |
|
|
28467
28932
|
| `last()` | `Promise` | Load the last known page |
|
|
@@ -28473,6 +28938,14 @@ await source.reload();
|
|
|
28473
28938
|
| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |
|
|
28474
28939
|
| `dispose()` | `void` | Abort work and stop notifications |
|
|
28475
28940
|
|
|
28941
|
+
`apply(update: PageSourceUpdate): Promise` compares params structurally with
|
|
28942
|
+
`isEqual`. Changed params use the supplied positive page without clamping, or page one when omitted.
|
|
28943
|
+
Unchanged params with a page use `goTo()` and clamp after the first successful load.
|
|
28944
|
+
An unchanged request does not refetch a committed result or duplicate an active request; it loads
|
|
28945
|
+
an inert source or retries an uncommitted request after failure. Omitted or `undefined` params
|
|
28946
|
+
preserve the requested value; use `setParams(undefined)` when `undefined` is a valid replacement.
|
|
28947
|
+
The update does not include page size; use `setPageSize()` separately.
|
|
28948
|
+
|
|
28476
28949
|
---
|
|
28477
28950
|
|
|
28478
28951
|
### `createCursorSource()`
|
|
@@ -28550,6 +29023,54 @@ await source.loadMore();
|
|
|
28550
29023
|
| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |
|
|
28551
29024
|
| `dispose()` | `void` | Abort work and stop notifications |
|
|
28552
29025
|
|
|
29026
|
+
---
|
|
29027
|
+
|
|
29028
|
+
### `createResource()`
|
|
29029
|
+
|
|
29030
|
+
```ts
|
|
29031
|
+
function createResource(config: ResourceConfig): Resource
|
|
29032
|
+
```
|
|
29033
|
+
|
|
29034
|
+
Returns observable state for one asynchronously loaded value. It does not suspend rendering,
|
|
29035
|
+
cache by key, or track external reactive inputs.
|
|
29036
|
+
|
|
29037
|
+
| Parameter | Type | Description |
|
|
29038
|
+
| --- | --- | --- |
|
|
29039
|
+
| `config.load` | `(context: ResourceLoadContext) => Promise` | Loader; receives a signal aborted on disposal and on the next `reload()` |
|
|
29040
|
+
| `config.start` | `boolean` | Start the initial load during construction; defaults to `true` |
|
|
29041
|
+
|
|
29042
|
+
**Returns:** `Resource`.
|
|
29043
|
+
|
|
29044
|
+
```ts
|
|
29045
|
+
import { createResource } from '@vielzeug/sourcerer';
|
|
29046
|
+
|
|
29047
|
+
const profile = createResource({
|
|
29048
|
+
load: ({ signal }) => fetch('/me', { signal }).then((response) => response.json()),
|
|
29049
|
+
start: false,
|
|
29050
|
+
});
|
|
29051
|
+
|
|
29052
|
+
const stop = profile.subscribe((state) => console.log(state.data, state.loading, state.error));
|
|
29053
|
+
console.log(profile.state);
|
|
29054
|
+
try {
|
|
29055
|
+
await profile.reload();
|
|
29056
|
+
} finally {
|
|
29057
|
+
stop();
|
|
29058
|
+
profile.dispose();
|
|
29059
|
+
}
|
|
29060
|
+
```
|
|
29061
|
+
|
|
29062
|
+
| Method | Returns | Description |
|
|
29063
|
+
| --- | --- | --- |
|
|
29064
|
+
| `reload()` | `Promise` | Refetch with a fresh signal; rejects so callers can await it |
|
|
29065
|
+
| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |
|
|
29066
|
+
| `dispose()` | `void` | Abort the in-flight load and stop notifications |
|
|
29067
|
+
|
|
29068
|
+
Construction starts the initial load unless `start: false`. Its failure surfaces through
|
|
29069
|
+
`state.error`; a later `reload()` rejects and retains the previous `data` on failure.
|
|
29070
|
+
A superseded or disposed request settles without touching state. Subscriptions are change-only:
|
|
29071
|
+
read `state` for the current snapshot, including the initial loading state. `dispose()` is idempotent,
|
|
29072
|
+
aborts `disposalSignal`, and leaves the last snapshot readable; subscriptions after disposal are inert.
|
|
29073
|
+
|
|
28553
29074
|
## Types
|
|
28554
29075
|
|
|
28555
29076
|
### Page source types
|
|
@@ -28568,8 +29089,11 @@ type PageSourceConfig = Readonly): Promise>;
|
|
|
28568
29089
|
} & (undefined extends TParams ? { params?: TParams } : { params: TParams })
|
|
28569
29090
|
>;
|
|
28570
29091
|
|
|
29092
|
+
type PageSourceUpdate = Readonly;
|
|
29093
|
+
|
|
28571
29094
|
type PageSource = {
|
|
28572
29095
|
[Symbol.dispose](): void;
|
|
29096
|
+
apply(update: PageSourceUpdate): Promise;
|
|
28573
29097
|
readonly disposalSignal: AbortSignal;
|
|
28574
29098
|
dispose(): void;
|
|
28575
29099
|
readonly disposed: boolean;
|
|
@@ -28588,6 +29112,28 @@ type PageSource = {
|
|
|
28588
29112
|
|
|
28589
29113
|
The conditional config field makes `params` optional only when `TParams` includes `undefined`.
|
|
28590
29114
|
|
|
29115
|
+
### Resource types
|
|
29116
|
+
|
|
29117
|
+
```ts
|
|
29118
|
+
type ResourceLoadContext = Readonly;
|
|
29119
|
+
|
|
29120
|
+
type ResourceState = Readonly;
|
|
29121
|
+
|
|
29122
|
+
type ResourceConfig = Readonly;
|
|
29123
|
+
start?: boolean;
|
|
29124
|
+
}>;
|
|
29125
|
+
|
|
29126
|
+
type Resource = {
|
|
29127
|
+
[Symbol.dispose](): void;
|
|
29128
|
+
readonly disposalSignal: AbortSignal;
|
|
29129
|
+
dispose(): void;
|
|
29130
|
+
readonly disposed: boolean;
|
|
29131
|
+
reload(): Promise;
|
|
29132
|
+
readonly state: ResourceState;
|
|
29133
|
+
subscribe(listener: (state: ResourceState) => void): () => void;
|
|
29134
|
+
};
|
|
29135
|
+
```
|
|
29136
|
+
|
|
28591
29137
|
### Local source types
|
|
28592
29138
|
|
|
28593
29139
|
```ts
|
|
@@ -28736,7 +29282,7 @@ console.log(source.state.params);
|
|
|
28736
29282
|
source.dispose();
|
|
28737
29283
|
```
|
|
28738
29284
|
|
|
28739
|
-
A non-optional parameter type requires an initial `params` value.
|
|
29285
|
+
A non-optional parameter type requires an initial `params` value. `setParams()` compares values with `Object.is`; `apply()` compares structurally, avoiding redundant requests from a router or reactive effect. Both can load an inert source or retry an uncommitted request. See [Apply Page and Filter Together](#apply-page-and-filter-together).
|
|
28740
29286
|
|
|
28741
29287
|
While replacement parameters load, `state.params` continues to describe the committed items and `state.pendingParams` contains the requested value. Page-only navigation does not set `pendingParams`.
|
|
28742
29288
|
|
|
@@ -28756,6 +29302,47 @@ await source.setPageSize(50);
|
|
|
28756
29302
|
|
|
28757
29303
|
After the first successful load, navigation is clamped to the known `pageCount`. Before that load, `goTo()` accepts any positive page because the total is unknown.
|
|
28758
29304
|
|
|
29305
|
+
## Apply Page and Filter Together
|
|
29306
|
+
|
|
29307
|
+
A URL query, a filter form, or a router state usually carries the page and the filters at once. `apply()` accepts both in one call and issues at most one request.
|
|
29308
|
+
|
|
29309
|
+
```ts
|
|
29310
|
+
import { createPageSource } from '@vielzeug/sourcerer';
|
|
29311
|
+
|
|
29312
|
+
const names = ['Ada', 'Grace', 'Linus', 'Ada Byron'];
|
|
29313
|
+
const source = createPageSource({
|
|
29314
|
+
load: async ({ page, pageSize, params }) => {
|
|
29315
|
+
const matching = names.filter((name) => name.includes(params.search));
|
|
29316
|
+
const start = (page - 1) * pageSize;
|
|
29317
|
+
return { items: matching.slice(start, start + pageSize), totalItems: matching.length };
|
|
29318
|
+
},
|
|
29319
|
+
pageSize: 1,
|
|
29320
|
+
params: { search: '' },
|
|
29321
|
+
});
|
|
29322
|
+
|
|
29323
|
+
try {
|
|
29324
|
+
await source.apply({ page: 2, params: { search: 'Ada' } });
|
|
29325
|
+
console.log(source.state.items); // ['Ada Byron']
|
|
29326
|
+
await source.apply({ params: { search: 'Ada' } });
|
|
29327
|
+
console.log(source.state.pagination.page); // 2; no extra request
|
|
29328
|
+
await source.apply({ params: { search: 'Grace' } });
|
|
29329
|
+
console.log(source.state.pagination.page); // 1
|
|
29330
|
+
} finally {
|
|
29331
|
+
source.dispose();
|
|
29332
|
+
}
|
|
29333
|
+
```
|
|
29334
|
+
|
|
29335
|
+
Parameters are compared structurally, so a freshly built object with the same contents does not
|
|
29336
|
+
refetch a committed result or duplicate active work. An inert source still loads, and an unchanged
|
|
29337
|
+
request can retry after failure. When parameters change,
|
|
29338
|
+
`page` is validated but never clamped: the page count still describes the previous filter set, so
|
|
29339
|
+
clamping against it would silently move the reader elsewhere. Omit `page` in that case and the
|
|
29340
|
+
source returns to page one. When parameters are unchanged, `apply()` routes through `goTo()` and
|
|
29341
|
+
clamps to the known page count.
|
|
29342
|
+
|
|
29343
|
+
Omitted or `undefined` params keep the requested value. Use `setParams(undefined)` when
|
|
29344
|
+
your parameter type allows `undefined` and you want to replace the value with it.
|
|
29345
|
+
|
|
28759
29346
|
## Filter Local Collections
|
|
28760
29347
|
|
|
28761
29348
|
Use a local source when data is already in memory. Its commands are synchronous.
|
|
@@ -28831,6 +29418,44 @@ source.dispose();
|
|
|
28831
29418
|
|
|
28832
29419
|
`loadMore()` is a no-op while another request is active or after `pagination.hasMore` becomes false. `setParams()` and `setPageSize()` replace the feed from page one.
|
|
28833
29420
|
|
|
29421
|
+
## Load a Single Value
|
|
29422
|
+
|
|
29423
|
+
Use a resource for one reactive value: a profile, a settings document, a pricing summary. It loads once and refetches on demand.
|
|
29424
|
+
|
|
29425
|
+
```ts
|
|
29426
|
+
import { createResource } from '@vielzeug/sourcerer';
|
|
29427
|
+
|
|
29428
|
+
type Profile = { name: string };
|
|
29429
|
+
const profile = createResource({
|
|
29430
|
+
load: async ({ signal }) => {
|
|
29431
|
+
const response = await fetch('/me', { signal });
|
|
29432
|
+
if (!response.ok) throw new Error(`Profile request failed: ${response.status}`);
|
|
29433
|
+
return response.json();
|
|
29434
|
+
},
|
|
29435
|
+
start: false,
|
|
29436
|
+
});
|
|
29437
|
+
|
|
29438
|
+
const stop = profile.subscribe((state) => console.log(state.data, state.loading, state.error));
|
|
29439
|
+
console.log(profile.state);
|
|
29440
|
+
try {
|
|
29441
|
+
await profile.reload();
|
|
29442
|
+
} catch (error) {
|
|
29443
|
+
console.error(error);
|
|
29444
|
+
} finally {
|
|
29445
|
+
stop();
|
|
29446
|
+
profile.dispose();
|
|
29447
|
+
}
|
|
29448
|
+
```
|
|
29449
|
+
|
|
29450
|
+
A resource starts loading during construction by default; the example uses `start: false` to await
|
|
29451
|
+
the first load explicitly. Automatic initial-load failures land in `state.error` without an
|
|
29452
|
+
unhandled rejection. Explicit `reload()` calls reject current failures and preserve previous `data`.
|
|
29453
|
+
Reloading aborts older work, whose promise settles without committing.
|
|
29454
|
+
|
|
29455
|
+
Read `state` before subscribing when you need the initial snapshot: subscriptions notify only
|
|
29456
|
+
changes. A resource does not track Ripple signals or cache values by key; call `reload()` when
|
|
29457
|
+
external inputs change. Disposal aborts work and retains the last readable snapshot.
|
|
29458
|
+
|
|
28834
29459
|
## Handle Requests and Errors
|
|
28835
29460
|
|
|
28836
29461
|
A new remote command aborts older work. Superseded or disposed work settles without committing. A current loader failure preserves committed items, writes the error to state, and rejects the command.
|
|
@@ -28963,6 +29588,7 @@ Use Scout to compute indexed local matches, Ripple to project state into reactiv
|
|
|
28963
29588
|
- [Page Params with URL State](./examples/remote-search-with-url-state.md)
|
|
28964
29589
|
- [Cursor-Based Pagination](./examples/cursor-based-pagination.md)
|
|
28965
29590
|
- [Infinite Scroll](./examples/infinite-scroll.md)
|
|
29591
|
+
- [Load a Single Value](./examples/single-value-resource.md)
|
|
28966
29592
|
- [Framework Integration](./examples/framework-integration.md)
|
|
28967
29593
|
- [Remote Data with Courier](./examples/sourcerer-with-courier.md)
|
|
28968
29594
|
- [Reactive Controls with Ripple](./examples/sourcerer-with-ripple.md)
|
|
@@ -28970,12 +29596,14 @@ Use Scout to compute indexed local matches, Ripple to project state into reactiv
|
|
|
28970
29596
|
|
|
28971
29597
|
### REPL Examples
|
|
28972
29598
|
|
|
29599
|
+
- apply - Page and params in one call (id: `apply`)
|
|
28973
29600
|
- Cursor Source (id: `cursor-source`)
|
|
28974
29601
|
- Error Handling (id: `error-handling`)
|
|
28975
29602
|
- Infinite Source (id: `infinite-source`)
|
|
28976
29603
|
- Source Lifecycle (id: `lifecycle`)
|
|
28977
29604
|
- Local Source (id: `local-source`)
|
|
28978
29605
|
- Page Source (id: `page-source`)
|
|
29606
|
+
- createResource - One reactive value (id: `resource`)
|
|
28979
29607
|
|
|
28980
29608
|
---
|
|
28981
29609
|
|