@vielzeug/codex 26.10.6 → 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 +780 -150
- package/data/llms.txt +3 -3
- package/data/manifest.json +1 -1
- package/data/packages/archive.json +6 -6
- 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 +3711 -3751
- 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
|
|
@@ -116,7 +116,8 @@ console.log(parsed.entries);
|
|
|
116
116
|
- [Usage Guide](./usage.md) — stand up a collection, deploy the Worker, wire the client.
|
|
117
117
|
- [API Reference](./api.md) — every export, all three entry points, the full route table.
|
|
118
118
|
- [Examples](./examples.md) — recipes from first deployment to GDPR erasure.
|
|
119
|
-
- [
|
|
119
|
+
- [Migration](./migration.md) — breaking changes after the first release and
|
|
120
|
+
fresh-database scope.
|
|
120
121
|
|
|
121
122
|
## See Also
|
|
122
123
|
|
|
@@ -146,8 +147,8 @@ console.log(parsed.entries);
|
|
|
146
147
|
| Import | Purpose |
|
|
147
148
|
| --- | --- |
|
|
148
149
|
| `@vielzeug/archive` | Browser-safe collections, constants, schemas and errors |
|
|
149
|
-
| `@vielzeug/archive/worker` | Catalog factory, issuer-session service binding and event types |
|
|
150
|
-
| `@vielzeug/archive/auth` | Issuer factory
|
|
150
|
+
| `@vielzeug/archive/worker` | Catalog factory, issuer signing-key/session service binding and event types |
|
|
151
|
+
| `@vielzeug/archive/auth` | Issuer factory and binding types |
|
|
151
152
|
|
|
152
153
|
## Collections
|
|
153
154
|
|
|
@@ -192,7 +193,7 @@ console.log(derived.contentHash);
|
|
|
192
193
|
| `derive(raw)` | `Promise` |
|
|
193
194
|
| `contract.entry` | Public entry schema |
|
|
194
195
|
| `contract.ownedEntry`, `ownedEntries` | Owner view with `clientKey` and `hidden` |
|
|
195
|
-
| `contract.page` | Entries and forward
|
|
196
|
+
| `contract.page` | Entries and the forward cursor |
|
|
196
197
|
| `contract.publishInput`, `updateInput` | Publish and revision-guarded update request schemas |
|
|
197
198
|
| `filters`, `tags` | Frozen vocabulary snapshots |
|
|
198
199
|
|
|
@@ -202,7 +203,7 @@ submitted content. Invalid index-result shapes also throw.
|
|
|
202
203
|
|
|
203
204
|
Const generics preserve the content-version literal and infer filter/tag unions from vocabulary arrays without `as const`. `NoInfer` prevents `index` from widening those unions; omitted arrays infer `never`, permitting only `filter: null` and empty tags.
|
|
204
205
|
|
|
205
|
-
Objects serialize with recursively sorted keys; arrays preserve order unless your canonicalizer changes it. SHA-256 hashes the core-owned `{contentVersion, payload}` envelope.
|
|
206
|
+
Objects serialize with recursively sorted keys; arrays preserve order unless your canonicalizer changes it. SHA-256 hashes the core-owned `{contentVersion, payload}` envelope. Stored payloads are canonical by construction, so the emitted entry schemas embed the plain payload schema.
|
|
206
207
|
|
|
207
208
|
`filter`, tags and searchable title are persisted projections, not computed during reads. Changing `index` or its vocabulary does not update stored rows. The active definition and persisted dataset must remain consistent; Archive has no projection-update or historical-schema API.
|
|
208
209
|
|
|
@@ -242,8 +243,13 @@ export default createArchiveWorker([notes]);
|
|
|
242
243
|
| `tap(handler, { signal? }?)` | Returns unsubscribe; a pre-aborted signal registers nothing |
|
|
243
244
|
|
|
244
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.
|
|
245
250
|
The catalog verifies the signed JWT locally, then asks the issuer to validate its
|
|
246
|
-
current account session version.
|
|
251
|
+
current account session version. JOSE caches signing keys; the catalog does not
|
|
252
|
+
cache successful issuer session verdicts.
|
|
247
253
|
If the service exchange fails or exceeds its fixed 5000 ms timeout (body decoding
|
|
248
254
|
included), the catalog returns `503 auth-unavailable`; the timeout does not cancel
|
|
249
255
|
work already started by the issuer.
|
|
@@ -317,8 +323,8 @@ time. The sweep deletes expired links and closed per-user send windows, then
|
|
|
317
323
|
unverified accounts with neither remaining links nor send-budget references.
|
|
318
324
|
Verified accounts are retained until explicit account deletion.
|
|
319
325
|
|
|
320
|
-
|
|
321
|
-
the factory and issuer binding types.
|
|
326
|
+
`OPERATOR_SESSION_MAX_AGE_SECONDS = 900` is part of the browser-safe root
|
|
327
|
+
contract; `/auth` exports the factory and issuer binding types.
|
|
322
328
|
|
|
323
329
|
`AUTH_JWT_PREVIOUS_PUBLIC_JWKS` optionally publishes retired public RS256 keys and verifies sessions minted before rotation; private members reject configuration. The package has no demo sign-in routes.
|
|
324
330
|
|
|
@@ -345,7 +351,7 @@ Every route below is mounted under `/api/v1`. Mutation guards require an exact a
|
|
|
345
351
|
| `GET /health` | Public catalog readiness; 200 `ok` or 503 `degraded` |
|
|
346
352
|
| `POST /sign-out` | Catalog-local sign-out; `{ ok: true }` and expired session cookie, without issuer access |
|
|
347
353
|
| `GET /collections/:collection` | Collection metadata |
|
|
348
|
-
| `GET /collections/:collection/entries` | `author`, `filter`, `tag`, `search`, `sort`, `cursor`, `limit`;
|
|
354
|
+
| `GET /collections/:collection/entries` | `author`, `filter`, `tag`, `search`, `sort`, `cursor`, `limit`; forward entry page |
|
|
349
355
|
| `POST /collections/:collection/entries` | `{ clientKey, payload }`; 201 create or 200 replay |
|
|
350
356
|
| `GET /collections/:collection/entries/:id` | Public entry; always `no-store` |
|
|
351
357
|
| `PUT /collections/:collection/entries/:id` | Owner-only `{ expectedRevision, payload }` |
|
|
@@ -403,8 +409,10 @@ clear the cookie. This revokes all sessions, including the caller's, after a
|
|
|
403
409
|
successful response. Requests authorized before revocation may finish; there is
|
|
404
410
|
no cross-database transactional or in-flight cancellation guarantee.
|
|
405
411
|
|
|
406
|
-
The catalog requires `AUTH_ISSUER_SERVICE`, a service binding to the issuer
|
|
407
|
-
|
|
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
|
|
408
416
|
`GET /api/v1/auth/session` through that binding on every private request. Only a
|
|
409
417
|
strictly validated 401 `unauthenticated` remains a 401. Malformed, unexpected,
|
|
410
418
|
provider and 5xx responses become 503 `auth-unavailable`; there is no authority
|
|
@@ -434,7 +442,7 @@ Effective hide, unhide, operator removal and publisher erasure append audit rows
|
|
|
434
442
|
| `ARCHIVE_OPERATOR_ROLE` | `'archive-operator'` |
|
|
435
443
|
| `SESSION_COOKIE_NAME`, `SESSION_COOKIE_PATH` | `'archive_session'`, `'/api/v1'` |
|
|
436
444
|
| `SESSION_MAX_AGE_SECONDS` | `2592000` |
|
|
437
|
-
| `OPERATOR_SESSION_MAX_AGE_SECONDS` | `900`; shared root contract
|
|
445
|
+
| `OPERATOR_SESSION_MAX_AGE_SECONDS` | `900`; shared root contract |
|
|
438
446
|
| `ACCOUNT_HEADER` | `'X-Archive-Account'`; required issuer-account UUID on coordinated erasure |
|
|
439
447
|
| `PUBLISHER_HEADER`, `REQUEST_MARKER_HEADER` | `'x-archive-publisher'`, `'x-archive-request'` |
|
|
440
448
|
| `ILL_FORMED_TEXT`, `isWellFormedText(value: string): boolean` | Reject control characters and lone UTF-16 surrogates |
|
|
@@ -579,10 +587,9 @@ interface ArchiveCollection {
|
|
|
579
587
|
interface ArchivePage {
|
|
580
588
|
cursor: string | null;
|
|
581
589
|
entries: readonly T[];
|
|
582
|
-
previous: string | null;
|
|
583
590
|
}
|
|
584
591
|
type ArchiveSort = 'liked' | 'recent';
|
|
585
|
-
type ArchiveDerivationStage = '
|
|
592
|
+
type ArchiveDerivationStage = 'canonicalize' | 'hash' | 'index' | 'schema';
|
|
586
593
|
type ArchiveIdentity = { id: string; name: string; operator: boolean } | null;
|
|
587
594
|
type ArchiveAccount = {
|
|
588
595
|
id: string; email: string; createdAt: string; lastSignInAt: string | null;
|
|
@@ -770,7 +777,7 @@ window resets atomically on a later sign-in request.
|
|
|
770
777
|
| --- | --- | --- |
|
|
771
778
|
| `ArchiveError` | Base for public package errors | `name`, `message`, optional `cause` |
|
|
772
779
|
| `ArchiveDefinitionError` | Invalid collection definition or duplicate registered collection names | Extends `ArchiveError` |
|
|
773
|
-
| `ArchiveDerivationError` | Unexpected schema/canonicalization/index/hash
|
|
780
|
+
| `ArchiveDerivationError` | Unexpected schema/canonicalization/index/hash fault or violated collection invariant | Extends `ArchiveError`; readonly `stage: ArchiveDerivationStage`; optional `cause` |
|
|
774
781
|
|
|
775
782
|
`ArchiveError` and `ArchiveDefinitionError` accept `(message: string, opts?: ErrorOptions)`.
|
|
776
783
|
`ArchiveDerivationError` accepts `(stage: ArchiveDerivationStage, message: string, opts?: ErrorOptions)`.
|
|
@@ -779,8 +786,9 @@ failures use the wire envelope, not these thrown classes. Internal `ApiError` an
|
|
|
779
786
|
`AuthOperationError` are not exported.
|
|
780
787
|
|
|
781
788
|
Stored entries that fail the current wire schema return 503 `record-unavailable`.
|
|
782
|
-
|
|
783
|
-
|
|
789
|
+
Stored payloads are canonical by construction, so the emitted entry schemas embed
|
|
790
|
+
the plain payload schema: parsing a response re-runs that schema, and an
|
|
791
|
+
exception from one of its callbacks becomes a sanitized 500.
|
|
784
792
|
|
|
785
793
|
The complete public error signatures are:
|
|
786
794
|
|
|
@@ -884,11 +892,12 @@ Lossy or non-JSON schema output and invalid index-result shapes also throw.
|
|
|
884
892
|
|
|
885
893
|
`contentVersion` participates in content identity. Request handling validates
|
|
886
894
|
only the current collection definition; it does not interpret historical schemas.
|
|
887
|
-
Archive
|
|
895
|
+
Archive provides no content-upgrade APIs: a new definition never reinterprets
|
|
896
|
+
stored rows.
|
|
888
897
|
|
|
889
898
|
Filters, tags and searchable titles are persisted data derived at publish/update
|
|
890
899
|
time. Editing `index` or its vocabularies does not recompute existing projections.
|
|
891
|
-
Keep the active definition and dataset consistent. For
|
|
900
|
+
Keep the active definition and dataset consistent. For a development
|
|
892
901
|
deployment, deliberately reset and reseed a fresh database when you change payload
|
|
893
902
|
or projection semantics; the package does not supply a migration framework.
|
|
894
903
|
Resetting destroys existing data. Perform it only as an explicit, deliberate
|
|
@@ -925,8 +934,8 @@ well-formed, or the route answers 503 without naming what is missing:
|
|
|
925
934
|
| ------------------------ | ---------------------------------------------------------------- |
|
|
926
935
|
| `APP_ENV` | `local` or `production` (transport rules and the pepper policy) |
|
|
927
936
|
| `ALLOWED_ORIGINS` | Exact canonical origins; never wildcards |
|
|
928
|
-
| `JWT_ISSUER` / `JWT_AUDIENCE` / `JWT_JWKS_URL` |
|
|
929
|
-
| `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 |
|
|
930
939
|
| `LIKES_ENABLED` / `PUBLISHING_ENABLED` | Feature flags |
|
|
931
940
|
| `RATE_LIMIT_KEY_SECRET` | HMAC pepper. Required ≥ 32 chars in production; local may fall back |
|
|
932
941
|
| `DB` / `READ_LIMIT` / `WRITE_LIMIT` | D1 + rate-limit bindings |
|
|
@@ -939,6 +948,14 @@ make the header lie.
|
|
|
939
948
|
Generated Env types (wrangler's `worker-configuration.d.ts`) satisfy the package's
|
|
940
949
|
structural `ArchiveWorkerEnv` — no `@cloudflare/workers-types` dependency, no casts.
|
|
941
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
|
+
|
|
942
959
|
## The auth issuer
|
|
943
960
|
|
|
944
961
|
`createAuthWorker()` from `@vielzeug/archive/auth` is the reference passwordless
|
|
@@ -1063,8 +1080,7 @@ The issuer points to
|
|
|
1063
1080
|
fresh-database initialization definitions, not upgrades. The issuer DDL initializes
|
|
1064
1081
|
`auth_users.session_version` to `0` and stamps `auth_schema_meta.applied` as
|
|
1065
1082
|
`AUTH_SCHEMA_VERSION` (`1`). Neither file automatically deletes or resets data.
|
|
1066
|
-
|
|
1067
|
-
canonical initial schemas are edited directly. Reset and reseed an unreleased
|
|
1083
|
+
Archive's canonical initial schemas are edited directly. Reset and reseed a
|
|
1068
1084
|
development database deliberately when its schema or collection semantics no
|
|
1069
1085
|
longer match; reapplying the initial file does not update an existing dataset.
|
|
1070
1086
|
The initialization file also stamps the storage schema
|
|
@@ -1115,10 +1131,11 @@ POST /api/v1/auth/verify (guarded {token} rede
|
|
|
1115
1131
|
Publishing is bounded by `maxPublications` live entries per `(collection, owner)`.
|
|
1116
1132
|
The quota check runs inside the guarded insert.
|
|
1117
1133
|
|
|
1118
|
-
`/me/likes` without `ids`, `/me/reports`, operator
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1134
|
+
Every list — entry pages, `/me/likes` without `ids`, `/me/reports`, operator
|
|
1135
|
+
reports and `/moderation-log` — pages forward-only: follow `cursor` until `null`,
|
|
1136
|
+
preserving query context; a cursor minted for another query is a 400. `limit`
|
|
1137
|
+
defaults to `pageSize` and is at most `maxPageSize`; there is no total export
|
|
1138
|
+
bound. `/me/likes?ids=…` remains a subset lookup returning `{ ids }`, not a
|
|
1122
1139
|
page, and cannot combine `ids` with `cursor` or `limit`.
|
|
1123
1140
|
|
|
1124
1141
|
Shared wire names live in the package — `archive_session` cookie,
|
|
@@ -1281,7 +1298,7 @@ export function useRecipes() {
|
|
|
1281
1298
|
have them; a value outside it rejects loudly.
|
|
1282
1299
|
- Keep `canonicalize` pure and schema-true: reordered lists in, canonical form out.
|
|
1283
1300
|
- Initialize fresh databases with the shipped DDL and deliberately reset/reseed
|
|
1284
|
-
|
|
1301
|
+
development data when collection or storage semantics change.
|
|
1285
1302
|
- Treat `ARCHIVE_ERROR_CODES` and `archiveErrorResponseSchema` as one tested contract.
|
|
1286
1303
|
- Pin the expected-publisher header from the identity you captured at sign-in, never from storage alone.
|
|
1287
1304
|
- Grant the operator role claim only through your issuer, and only to accounts that
|
|
@@ -1375,6 +1392,8 @@ console.log(byRole, health, profile);
|
|
|
1375
1392
|
- **`tryParseJson`**: preserve JSON syntax failures from `/object`
|
|
1376
1393
|
- **`allocate`**: lossless proportional distribution from `/math`
|
|
1377
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
|
|
1378
1397
|
- **`range`, `average`, `median`**: common numeric operations from `/math`
|
|
1379
1398
|
- **`assert`, `tap`**: typed control-flow helpers from `/function`
|
|
1380
1399
|
- **`isEqual`**: structural equality from package root
|
|
@@ -1402,6 +1421,8 @@ console.log(byRole, health, profile);
|
|
|
1402
1421
|
| --- | --- | --- | --- |
|
|
1403
1422
|
| `groupBy` | Group values by key (prototype-pollution guarded) | Sync | Root export |
|
|
1404
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 |
|
|
1405
1426
|
| `range` / `average` / `median` | Numeric generation and aggregation | Sync | Available from `/math` |
|
|
1406
1427
|
| `assert` | Assert and narrow a condition | Sync | Available from `/function` |
|
|
1407
1428
|
| `retry` | Retry async work | Async | Rethrows final error |
|
|
@@ -1422,7 +1443,7 @@ console.log(byRole, health, profile);
|
|
|
1422
1443
|
| Import | Purpose |
|
|
1423
1444
|
| --- | --- |
|
|
1424
1445
|
| `@vielzeug/arsenal` | Curated common utilities |
|
|
1425
|
-
| `@vielzeug/arsenal/array` | Immutable transforms, set operations, sorting, fuzzy search |
|
|
1446
|
+
| `@vielzeug/arsenal/array` | Immutable transforms, pagination, set operations, sorting, fuzzy search |
|
|
1426
1447
|
| `@vielzeug/arsenal/async` | Retry, cancellation, task pool, timing |
|
|
1427
1448
|
| `@vielzeug/arsenal/cache` | In-memory cache and memoization |
|
|
1428
1449
|
| `@vielzeug/arsenal/encoding` | Base64url and native byte compression |
|
|
@@ -1450,6 +1471,35 @@ unzip(rows): tupleOfArrays
|
|
|
1450
1471
|
|
|
1451
1472
|
`take`, `takeLast`, `drop`, and `dropLast` return new arrays and normalize negative counts to zero. Set operations accept optional selectors for object identity.
|
|
1452
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
|
+
|
|
1453
1503
|
### fuzzyFilter / fuzzyScore
|
|
1454
1504
|
|
|
1455
1505
|
```ts
|
|
@@ -1715,6 +1765,16 @@ type FuzzySelection = FuzzyOptions & {
|
|
|
1715
1765
|
|
|
1716
1766
|
type ScoredResult = { item: T; score: number };
|
|
1717
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
|
+
|
|
1718
1778
|
type CacheOptions = {
|
|
1719
1779
|
capacity?: number;
|
|
1720
1780
|
now?: () => number;
|
|
@@ -1789,6 +1849,45 @@ const byPrice = sort(products, (product) => product.price);
|
|
|
1789
1849
|
console.log(publishedLabels, byId, byPrice);
|
|
1790
1850
|
```
|
|
1791
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
|
+
|
|
1792
1891
|
## Search Explicit Fields
|
|
1793
1892
|
|
|
1794
1893
|
Search string arrays directly. Object collections require `select`, so callers define exactly what can match.
|
|
@@ -1970,8 +2069,10 @@ const settings = parsed.ok ? Settings.parse(parsed.value) : { theme: 'system' };
|
|
|
1970
2069
|
- filterMap - Filter and map array elements (id: `array-filter`)
|
|
1971
2070
|
- groupBy - Group array by key (id: `array-group`)
|
|
1972
2071
|
- filterMap - Transform array elements (id: `array-map`)
|
|
2072
|
+
- paginate - Page an array (id: `array-paginate`)
|
|
1973
2073
|
- fuzzyFilter - Explicit searchable fields (id: `array-search`)
|
|
1974
2074
|
- fuzzyFilter - Unicode normalization (id: `array-search-normalize`)
|
|
2075
|
+
- sort - Selector, selectors, or grid criteria (id: `array-sort-criteria`)
|
|
1975
2076
|
- uniq - Remove duplicates (id: `array-uniq`)
|
|
1976
2077
|
- attempt - Safe async execution with isFail/isOk helpers (id: `async-attempt`)
|
|
1977
2078
|
- parallel - Controlled parallel execution (id: `async-parallel`)
|
|
@@ -2070,6 +2171,8 @@ await waitUntil(() => view.queryByText('Saved') !== null);
|
|
|
2070
2171
|
- `fireClick`, `fireInput`, `fireKeyDown`, and peers dispatch exact synchronous events.
|
|
2071
2172
|
- `waitUntil`, `eventually`, and `waitForEvent` provide bounded, abortable async waiting.
|
|
2072
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.
|
|
2073
2176
|
- `AssayError`, `AssayQueryError`, and `AssayTimeoutError` provide typed failures.
|
|
2074
2177
|
|
|
2075
2178
|
## Documentation
|
|
@@ -2096,12 +2199,14 @@ await waitUntil(() => view.queryByText('Saved') !== null);
|
|
|
2096
2199
|
| `fire*` / `dispatch` | Dispatches platform event instances | Sync | Does not reproduce browser default behavior |
|
|
2097
2200
|
| `waitUntil` / `eventually` / `waitForEvent` | Waits for conditions, assertions, or events | Async | Use a signal or timeout for bounded waits |
|
|
2098
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 |
|
|
2099
2204
|
|
|
2100
2205
|
## Package Entry Point
|
|
2101
2206
|
|
|
2102
2207
|
| Import | Purpose |
|
|
2103
2208
|
| ----------------- | ---------------------------------------------------- |
|
|
2104
|
-
| `@vielzeug/assay` | DOM queries, events, wait helpers, errors, and types |
|
|
2209
|
+
| `@vielzeug/assay` | DOM queries, events, wait helpers, test-environment helpers, errors, and types |
|
|
2105
2210
|
|
|
2106
2211
|
## Queries
|
|
2107
2212
|
|
|
@@ -2193,6 +2298,57 @@ await delay(100, { signal });
|
|
|
2193
2298
|
|
|
2194
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.
|
|
2195
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
|
+
|
|
2196
2352
|
## Types
|
|
2197
2353
|
|
|
2198
2354
|
```ts
|
|
@@ -2347,6 +2503,36 @@ expect(onSave).toHaveBeenCalledOnce();
|
|
|
2347
2503
|
Refine's testing entry point contains only Refine-specific assertions and typed mount wrappers. Import Assay helpers
|
|
2348
2504
|
directly instead of routing generic DOM operations through another package.
|
|
2349
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
|
+
|
|
2350
2536
|
## Best Practices
|
|
2351
2537
|
|
|
2352
2538
|
- Scope multiple assertions with `within()` rather than repeatedly querying the document.
|
|
@@ -4521,6 +4707,7 @@ try {
|
|
|
4521
4707
|
- **`withBearerAuth()` / `withRequestId()` / `withLogging()`**: composable middleware configured at construction.
|
|
4522
4708
|
- **`tap()`**: signal-owned structured transport observation.
|
|
4523
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.
|
|
4524
4711
|
- **`cancelAll()` / `dispose()`**: abort active requests and tear down the transport.
|
|
4525
4712
|
|
|
4526
4713
|
## Documentation
|
|
@@ -4552,6 +4739,9 @@ try {
|
|
|
4552
4739
|
| `withRequestId()` | Adds request identifier middleware | Sync | Default generator uses `crypto.randomUUID()` |
|
|
4553
4740
|
| `withLogging()` | Logs request result metadata | Sync | Requires explicit logger; URLs may contain sensitive query values |
|
|
4554
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 |
|
|
4555
4745
|
|
|
4556
4746
|
## Package Entry Point
|
|
4557
4747
|
|
|
@@ -4797,7 +4987,7 @@ type PrefetchConfig = Omit,
|
|
|
4797
4987
|
| Error | Trigger | Notable properties |
|
|
4798
4988
|
| --- | --- | --- |
|
|
4799
4989
|
| `CourierError` | Base class for all Courier errors | Use `instanceof` to narrow |
|
|
4800
|
-
| `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 |
|
|
4801
4991
|
| `CourierNetworkError` | Request failure without response | `method`, `url`, `cause` |
|
|
4802
4992
|
| `CourierTimeoutError` | Timeout signal aborts request | `method`, `url`, `cause` |
|
|
4803
4993
|
| `CourierAbortError` | Caller or client cancellation | `method`, `url`, `cause` |
|
|
@@ -4808,6 +4998,72 @@ type PrefetchConfig = Omit,
|
|
|
4808
4998
|
|
|
4809
4999
|
Errors thrown directly by custom middleware are preserved rather than reclassified.
|
|
4810
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
|
+
|
|
4811
5067
|
### Usage Guide
|
|
4812
5068
|
|
|
4813
5069
|
## Basic Usage
|
|
@@ -4996,6 +5252,55 @@ async function loadUser(): Promise {
|
|
|
4996
5252
|
- Do not display cancellation as an application error during navigation.
|
|
4997
5253
|
- `CourierSchemaValidationError` carries the raw pre-validation `data`.
|
|
4998
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
|
+
|
|
4999
5304
|
## Composing with a State Layer
|
|
5000
5305
|
|
|
5001
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.
|
|
@@ -5114,6 +5419,7 @@ There is no `use()` method. To change middleware, create a new client. This keep
|
|
|
5114
5419
|
- Cached Read and Prefetch (id: `cached-read`)
|
|
5115
5420
|
- createCourier - Transport Client (id: `create-courier`)
|
|
5116
5421
|
- middleware - Immutable Chain (id: `middleware`)
|
|
5422
|
+
- problem+json - RFC 9457 error bodies (id: `problem-details`)
|
|
5117
5423
|
|
|
5118
5424
|
---
|
|
5119
5425
|
|
|
@@ -15546,7 +15852,7 @@ define('my-counter', {
|
|
|
15546
15852
|
- Props via `prop.*` helpers (`prop.string`, `prop.number`, `prop.bool`, `prop.oneOf`, `prop.json`, `prop.data`) or raw `PropDef` objects
|
|
15547
15853
|
- `setup(props)` takes only props and returns an `HTMLResult` directly: `return html\`...\``
|
|
15548
15854
|
- Lifecycle hooks: `onMounted`, `onCleanup`, `onEvent`, `onElement`, `watchEffect`: plain functions imported from `@vielzeug/ore`, called directly from `setup()` or any composable it calls
|
|
15549
|
-
- Directives: `each` (keyed
|
|
15855
|
+
- Directives: `each` (transactional keyed list reconciliation), `classMap`, `styleMap`, `when`, `live`, `model`, `unsafeHtml`
|
|
15550
15856
|
- Host bindings via `bind({ attr, class, style, on })`: pass `{ target: el }` to bind any off-host element
|
|
15551
15857
|
- Reactive ARIA sync via explicit `aria-*` keys in `bind({ attr }, { target })`, with automatic cleanup on disconnect
|
|
15552
15858
|
- Context via `provide(key, value)` / `inject(key)`; typed emit/slots via `useEmit()` / `useSlots()`
|
|
@@ -15594,6 +15900,7 @@ All browser-runtime symbols below are imported from `@vielzeug/ore`. Lifecycle/c
|
|
|
15594
15900
|
| `each()` | Keyed list rendering with DOM diffing | Sync | Duplicate keys report `ore:error`; plain `T[]` is a one-time static render |
|
|
15595
15901
|
| `when()` | Conditional branch rendering | Sync | Getter-fn computed disposed on cleanup; static bool skips subscription |
|
|
15596
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 |
|
|
15597
15904
|
| `onMounted(fn)` | DOM-ready callback | Setup only | Must be called synchronously during `setup()` |
|
|
15598
15905
|
| `onCleanup(fn)` | Register teardown | Setup only | Called on component disconnect |
|
|
15599
15906
|
| `onEvent(target, …)` | Scoped event listener with auto-cleanup | Setup only | No-ops on null target; removed on disconnect |
|
|
@@ -15708,12 +16015,13 @@ setup(_props) {
|
|
|
15708
16015
|
| `prop.string(defaultValue?)` | `PropDef` | Reflects by default |
|
|
15709
16016
|
| `prop.bool(defaultValue?)` | `PropDef` | Any non-null attribute value other than `"false"` parses as `true`; `"false"` or absent attribute is `false` |
|
|
15710
16017
|
| `prop.number(defaultValue?)` | `PropDef` | Returns default (not NaN) and warns in dev when attribute is not a valid number |
|
|
15711
|
-
| `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 |
|
|
15712
16019
|
| `prop.json(defaultValue)` | `PropDef` | JSON.parse from attribute; `reflect: false` |
|
|
15713
16020
|
| `prop.data(defaultValue?)` | `PropDef` | JS-only: never reads/writes an attribute; use for objects, arrays, callbacks, or any non-serialisable value |
|
|
15714
16021
|
|
|
15715
16022
|
> **Choosing the right prop helper:**
|
|
15716
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.
|
|
15717
16025
|
> - **`prop.json`**: value can be declared in HTML (``); attribute string is `JSON.parse`d.
|
|
15718
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.
|
|
15719
16027
|
|
|
@@ -15746,7 +16054,16 @@ define('data-grid', {
|
|
|
15746
16054
|
### `html`
|
|
15747
16055
|
|
|
15748
16056
|
Tagged template literal that returns an `HTMLResult`. Supports text interpolation, ordinary attributes (`attr=`),
|
|
15749
|
-
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.
|
|
15750
16067
|
|
|
15751
16068
|
### `css`
|
|
15752
16069
|
|
|
@@ -15761,9 +16078,28 @@ Tagged template literal that returns a `CSSResult` for use in `styles`.
|
|
|
15761
16078
|
| `classMap(record)` | Reactive class string from object map |
|
|
15762
16079
|
| `styleMap(record)` | Reactive inline style string from object map |
|
|
15763
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 |
|
|
15764
16082
|
| `unsafeHtml(value)` | HTML rendering sink; sanitize untrusted values before calling |
|
|
15765
16083
|
|
|
15766
|
-
`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.
|
|
15767
16103
|
|
|
15768
16104
|
### `unsafeHtml`
|
|
15769
16105
|
|
|
@@ -15863,6 +16199,8 @@ Import from `@vielzeug/ore`.
|
|
|
15863
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.
|
|
15864
16200
|
|
|
15865
16201
|
```ts
|
|
16202
|
+
type FormFieldValidity = { flags: ValidityStateFlags; message: string };
|
|
16203
|
+
|
|
15866
16204
|
type FormFieldOptions = {
|
|
15867
16205
|
disabled?: Readable;
|
|
15868
16206
|
/**
|
|
@@ -15875,8 +16213,13 @@ type FormFieldOptions = {
|
|
|
15875
16213
|
/** Called when the ancestor resets (see onFormReset): restore local field state here. */
|
|
15876
16214
|
onReset?: () => void;
|
|
15877
16215
|
toFormValue?: (value: T) => File | FormData | string | null;
|
|
15878
|
-
/**
|
|
15879
|
-
|
|
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
|
+
*/
|
|
15880
16223
|
validity?: Readable;
|
|
15881
16224
|
value: Signal | Readable;
|
|
15882
16225
|
};
|
|
@@ -15890,15 +16233,20 @@ type FormFieldHandle = {
|
|
|
15890
16233
|
};
|
|
15891
16234
|
```
|
|
15892
16235
|
|
|
15893
|
-
Pass `validity
|
|
15894
|
-
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.
|
|
15895
16240
|
|
|
15896
16241
|
```ts
|
|
15897
16242
|
const isBlank = (v: string) => v.trim() === '';
|
|
15898
16243
|
|
|
15899
16244
|
useField({
|
|
15900
|
-
|
|
15901
|
-
|
|
16245
|
+
validity: computed(() =>
|
|
16246
|
+
required.value && isBlank(value.value)
|
|
16247
|
+
? { flags: { valueMissing: true }, message: 'This field is required.' }
|
|
16248
|
+
: null,
|
|
16249
|
+
),
|
|
15902
16250
|
value,
|
|
15903
16251
|
});
|
|
15904
16252
|
```
|
|
@@ -15911,8 +16259,7 @@ Import from `@vielzeug/ore/testing`.
|
|
|
15911
16259
|
| ------------------------ | ------------------------------------------------------------------------------------------ |
|
|
15912
16260
|
| `mount(setup, options?)` | Mount a component and return a test fixture |
|
|
15913
16261
|
| `cleanup()` | Remove all mounted elements and reset test state |
|
|
15914
|
-
| `install(afterEach
|
|
15915
|
-
| `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 |
|
|
15916
16263
|
| `flush(options?)` | Drain reactive updates and animation frames |
|
|
15917
16264
|
| `renderHook(setup)` | Run lifecycle hooks in isolation; overload accepts `propDefs` as first arg for typed props |
|
|
15918
16265
|
| `OreTimeoutError` | Error thrown when `flush()` cannot settle tracked Ore work |
|
|
@@ -15922,7 +16269,16 @@ Import from `@vielzeug/ore/testing`.
|
|
|
15922
16269
|
Import `within`, named dispatchers such as `fireClick`, and waits such as `waitUntil` or `waitForEvent` from
|
|
15923
16270
|
`@vielzeug/assay`.
|
|
15924
16271
|
|
|
15925
|
-
> **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.
|
|
15926
16282
|
|
|
15927
16283
|
#### `Fixture` interface
|
|
15928
16284
|
|
|
@@ -16091,18 +16447,26 @@ type CSSResult = {
|
|
|
16091
16447
|
|
|
16092
16448
|
type LiveBinding = { readonly source: Readable };
|
|
16093
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
|
+
|
|
16094
16457
|
type EmitFn> = {
|
|
16095
16458
|
>(event: K): boolean;
|
|
16096
16459
|
>>(event: K, detail: T[K]): boolean;
|
|
16097
16460
|
};
|
|
16098
16461
|
// KeysWithoutDetail is an internal helper type, not exported.
|
|
16099
16462
|
|
|
16463
|
+
type FormFieldValidity = { flags: ValidityStateFlags; message: string };
|
|
16464
|
+
|
|
16100
16465
|
type FormFieldOptions = {
|
|
16101
16466
|
disabled?: Readable;
|
|
16102
16467
|
emptyStringForNull?: boolean;
|
|
16103
16468
|
onReset?: () => void;
|
|
16104
16469
|
toFormValue?: (value: T) => File | FormData | string | null;
|
|
16105
|
-
validationMessage?: Readable;
|
|
16106
16470
|
validity?: Readable;
|
|
16107
16471
|
value: Signal | Readable;
|
|
16108
16472
|
};
|
|
@@ -16256,11 +16620,13 @@ define('x-button', {
|
|
|
16256
16620
|
|
|
16257
16621
|
## template bindings
|
|
16258
16622
|
|
|
16259
|
-
`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.
|
|
16260
16626
|
|
|
16261
16627
|
```ts
|
|
16262
16628
|
import { computed, signal } from '@vielzeug/ripple';
|
|
16263
|
-
import { define, html, ref } from '@vielzeug/ore';
|
|
16629
|
+
import { define, html, model, ref } from '@vielzeug/ore';
|
|
16264
16630
|
|
|
16265
16631
|
define('profile-name', {
|
|
16266
16632
|
setup() {
|
|
@@ -16269,10 +16635,7 @@ define('profile-name', {
|
|
|
16269
16635
|
|
|
16270
16636
|
return html`
|
|
16271
16637
|
'Current: ' + name.value)}>Name
|
|
16272
|
-
'Current name ' + name.value}
|
|
16273
|
-
@input=${(event: Event) => {
|
|
16274
|
-
name.value = (event.target as HTMLInputElement).value;
|
|
16275
|
-
}} />
|
|
16638
|
+
'Current name ' + name.value} />
|
|
16276
16639
|
Hello ${name}
|
|
16277
16640
|
`;
|
|
16278
16641
|
},
|
|
@@ -16281,8 +16644,7 @@ define('profile-name', {
|
|
|
16281
16644
|
|
|
16282
16645
|
## directives
|
|
16283
16646
|
|
|
16284
|
-
Ore exports `each`, `classMap`, `styleMap`, `when`, `live`, and `unsafeHtml` from `@vielzeug/ore`.
|
|
16285
|
-
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`.
|
|
16286
16648
|
|
|
16287
16649
|
```ts
|
|
16288
16650
|
import { signal } from '@vielzeug/ripple';
|
|
@@ -16330,9 +16692,50 @@ each(
|
|
|
16330
16692
|
);
|
|
16331
16693
|
```
|
|
16332
16694
|
|
|
16333
|
-
|
|
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`.
|
|
16334
16698
|
|
|
16335
|
-
|
|
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.
|
|
16336
16739
|
|
|
16337
16740
|
```ts
|
|
16338
16741
|
import { signal } from '@vielzeug/ripple';
|
|
@@ -16696,7 +17099,7 @@ define('signup-form', {
|
|
|
16696
17099
|
- Bind host attributes and classes via `bind()` rather than mutating the element directly.
|
|
16697
17100
|
- Provide context at the nearest ancestor: avoid global context singletons.
|
|
16698
17101
|
- Call `onCleanup()` for every resource allocated in `setup()` (WebSockets, intervals, external subscriptions).
|
|
16699
|
-
- 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.
|
|
16700
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.
|
|
16701
17104
|
- Test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic DOM events, queries, and waits
|
|
16702
17105
|
from `@vielzeug/assay`.
|
|
@@ -20415,6 +20818,7 @@ Classification guides discovery and bundle selection. It is not a reason to remo
|
|
|
20415
20818
|
- **Framework agnostic**: works anywhere HTML can be rendered
|
|
20416
20819
|
- **Tree-shakeable**: import only the component entry points you register
|
|
20417
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
|
|
20418
20822
|
- **Typed framework declarations**: DOM, React, and Vue tags derive from the component property contracts
|
|
20419
20823
|
|
|
20420
20824
|
### Prerequisites
|
|
@@ -20518,6 +20922,14 @@ The declarations cover every supported tag and derive component properties from
|
|
|
20518
20922
|
|
|
20519
20923
|
Form controls expose `.value` or `.checked` and dispatch standard `input` and `change` events. Read the property from `event.currentTarget`.
|
|
20520
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
|
+
|
|
20521
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.
|
|
20522
20934
|
|
|
20523
20935
|
## Utilities
|
|
@@ -20623,6 +21035,20 @@ grid.rows = [
|
|
|
20623
21035
|
];
|
|
20624
21036
|
```
|
|
20625
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
|
+
|
|
20626
21052
|
## Handle Events
|
|
20627
21053
|
|
|
20628
21054
|
Form controls dispatch standard `input` and `change` events. Read their current property from `event.currentTarget`.
|
|
@@ -20646,6 +21072,37 @@ dialog?.addEventListener('open-change', (event: CustomEvent) => {
|
|
|
20646
21072
|
});
|
|
20647
21073
|
```
|
|
20648
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
|
+
|
|
20649
21106
|
## Compose Slots
|
|
20650
21107
|
|
|
20651
21108
|
Default slots hold primary content. Named slots identify component-owned regions.
|
|
@@ -20840,16 +21297,16 @@ ripple.dispose();
|
|
|
20840
21297
|
|
|
20841
21298
|
## Features
|
|
20842
21299
|
|
|
20843
|
-
- `createRipple()` creates an isolated graph
|
|
21300
|
+
- `createRipple({ onError })` creates an isolated graph with one runtime failure callback.
|
|
20844
21301
|
- `signal()` stores writable values with configurable equality.
|
|
20845
21302
|
- `computed()` derives lazy read-only values.
|
|
20846
21303
|
- `effect()` reacts to dependency changes with cleanup support.
|
|
20847
21304
|
- `batch()` coalesces synchronous writes and notifications.
|
|
20848
21305
|
- `createScope()` groups owned reactive work.
|
|
20849
21306
|
- `watch()` observes one selected source transition.
|
|
20850
|
-
- `tap()` provides runtime observability for all graph events.
|
|
20851
21307
|
- `resource()` owns focused latest-request async state.
|
|
20852
21308
|
- `fromSubscribable()` creates a disposable bridge for external `{ getSnapshot, subscribe }` sources.
|
|
21309
|
+
- `RippleError`, `RippleCycleError`, and `RippleDisposedError` distinguish package-originated failures.
|
|
20853
21310
|
|
|
20854
21311
|
## Documentation
|
|
20855
21312
|
|
|
@@ -20878,15 +21335,18 @@ ripple.dispose();
|
|
|
20878
21335
|
| `createScope()` | Group owned reactive work | Sync | Call `run()` to activate it |
|
|
20879
21336
|
| `untrack()` | Read without tracking | Sync | Read still happens immediately |
|
|
20880
21337
|
| `watch()` | Observe selected output | Sync | Use `effect()` for broad reads |
|
|
20881
|
-
| `
|
|
21338
|
+
| `resource()` | Load from tracked reactive input | Sync factory / Async loader | `reload()` returns `void`; completion is observable state |
|
|
20882
21339
|
| `fromSubscribable()` | Bridge external source | Sync | Returned bridge must be disposed |
|
|
20883
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 |
|
|
20884
21344
|
|
|
20885
21345
|
## Package Entry Point
|
|
20886
21346
|
|
|
20887
21347
|
| Import | Purpose |
|
|
20888
21348
|
| --- | --- |
|
|
20889
|
-
| `@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 |
|
|
20890
21350
|
|
|
20891
21351
|
## Graph Creation
|
|
20892
21352
|
|
|
@@ -20896,11 +21356,14 @@ ripple.dispose();
|
|
|
20896
21356
|
function createRipple(options?: RippleOptions): Ripple;
|
|
20897
21357
|
```
|
|
20898
21358
|
|
|
20899
|
-
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.
|
|
20900
21360
|
|
|
20901
21361
|
| Parameter | Type | Description |
|
|
20902
21362
|
| --- | --- | --- |
|
|
20903
|
-
| `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.
|
|
20904
21367
|
|
|
20905
21368
|
**Returns:** `Ripple`.
|
|
20906
21369
|
|
|
@@ -20939,7 +21402,7 @@ console.log(isReactive(signal(0)));
|
|
|
20939
21402
|
|
|
20940
21403
|
## Default Graph Functions
|
|
20941
21404
|
|
|
20942
|
-
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()`.
|
|
20943
21406
|
|
|
20944
21407
|
### `signal(initial, options?)`
|
|
20945
21408
|
|
|
@@ -20971,7 +21434,7 @@ cart.update((state) => ({ ...state, items: state.items + 1 }));
|
|
|
20971
21434
|
function computed(derive: () => T, options?: ComputedOptions): Readable;
|
|
20972
21435
|
```
|
|
20973
21436
|
|
|
20974
|
-
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.
|
|
20975
21438
|
|
|
20976
21439
|
**Returns:** `Readable`.
|
|
20977
21440
|
|
|
@@ -20995,6 +21458,12 @@ function effect(callback: () => Cleanup | undefined, options?: EffectOptions): E
|
|
|
20995
21458
|
|
|
20996
21459
|
Runs immediately and reruns when its tracked reads change. A returned cleanup runs before the next callback or disposal.
|
|
20997
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
|
+
|
|
20998
21467
|
**Returns:** `EffectHandle`.
|
|
20999
21468
|
|
|
21000
21469
|
**Example:**
|
|
@@ -21078,16 +21547,31 @@ function resource(
|
|
|
21078
21547
|
|
|
21079
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`.
|
|
21080
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
|
+
|
|
21081
21560
|
```ts
|
|
21561
|
+
import { resource, signal } from '@vielzeug/ripple';
|
|
21562
|
+
|
|
21563
|
+
const userId = signal('42');
|
|
21082
21564
|
const user = resource(
|
|
21083
21565
|
() => userId.value,
|
|
21084
21566
|
(id, { signal }) => fetch(`/users/${id}`, { signal }).then((response) => response.json()),
|
|
21085
21567
|
);
|
|
21568
|
+
console.log(user.value);
|
|
21569
|
+
user.dispose();
|
|
21086
21570
|
```
|
|
21087
21571
|
|
|
21088
|
-
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`.
|
|
21089
21573
|
|
|
21090
|
-
## Watch
|
|
21574
|
+
## Watch
|
|
21091
21575
|
|
|
21092
21576
|
### `watch(source, callback, options?)`
|
|
21093
21577
|
|
|
@@ -21115,41 +21599,6 @@ stop.dispose();
|
|
|
21115
21599
|
|
|
21116
21600
|
---
|
|
21117
21601
|
|
|
21118
|
-
### `ripple.tap(handler, options?)`
|
|
21119
|
-
|
|
21120
|
-
```ts
|
|
21121
|
-
function tap(
|
|
21122
|
-
handler: (event: RippleEvent) => void,
|
|
21123
|
-
options?: { signal?: AbortSignal },
|
|
21124
|
-
): Unsubscribe;
|
|
21125
|
-
```
|
|
21126
|
-
|
|
21127
|
-
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.
|
|
21128
|
-
|
|
21129
|
-
The `Ripple.tap()` method is available on instances created with `createRipple()`. After runtime disposal it returns a no-op unsubscribe.
|
|
21130
|
-
|
|
21131
|
-
**Returns:** `Unsubscribe`.
|
|
21132
|
-
|
|
21133
|
-
**Example:**
|
|
21134
|
-
|
|
21135
|
-
```ts
|
|
21136
|
-
import { createRipple } from '@vielzeug/ripple';
|
|
21137
|
-
|
|
21138
|
-
const ripple = createRipple({ errorPolicy: 'swallow' });
|
|
21139
|
-
const stop = ripple.tap((event) => {
|
|
21140
|
-
if (event.type === 'error') console.error(event.context.kind, event.error);
|
|
21141
|
-
if (event.type === 'write') console.log(`${event.name}: ${event.previous} → ${event.next}`);
|
|
21142
|
-
});
|
|
21143
|
-
|
|
21144
|
-
const count = ripple.signal(0);
|
|
21145
|
-
count.value = 1;
|
|
21146
|
-
|
|
21147
|
-
stop();
|
|
21148
|
-
ripple.dispose();
|
|
21149
|
-
```
|
|
21150
|
-
|
|
21151
|
-
---
|
|
21152
|
-
|
|
21153
21602
|
### `fromSubscribable(source, options?)`
|
|
21154
21603
|
|
|
21155
21604
|
```ts
|
|
@@ -21165,12 +21614,20 @@ Bridges an external `{ getSnapshot, subscribe }` source into a disposable reacti
|
|
|
21165
21614
|
```ts
|
|
21166
21615
|
import { fromSubscribable } from '@vielzeug/ripple';
|
|
21167
21616
|
|
|
21168
|
-
|
|
21169
|
-
|
|
21170
|
-
|
|
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
|
+
},
|
|
21171
21625
|
});
|
|
21172
21626
|
|
|
21173
|
-
|
|
21627
|
+
current = 'ready';
|
|
21628
|
+
for (const listener of listeners) listener();
|
|
21629
|
+
console.log(state.value);
|
|
21630
|
+
state.dispose();
|
|
21174
21631
|
```
|
|
21175
21632
|
|
|
21176
21633
|
## Types
|
|
@@ -21185,20 +21642,13 @@ type ComputedOptions = { equals?: Equality; name?: string };
|
|
|
21185
21642
|
type EffectOptions = { name?: string };
|
|
21186
21643
|
type WatchOptions = { equals?: Equality; immediate?: boolean; name?: string };
|
|
21187
21644
|
|
|
21188
|
-
type RippleErrorContext = { readonly kind: 'cleanup' | 'computed' | 'effect' | 'listener'; readonly name?: string };
|
|
21189
|
-
type
|
|
21190
|
-
type RippleOptions = { errorPolicy?: RippleErrorPolicy };
|
|
21191
|
-
|
|
21192
|
-
type RippleEvent =
|
|
21193
|
-
| { readonly type: 'compute'; readonly name?: string }
|
|
21194
|
-
| { readonly type: 'effect'; readonly name?: string }
|
|
21195
|
-
| { readonly type: 'write'; readonly name?: string; readonly next: unknown; readonly previous: unknown }
|
|
21196
|
-
| { readonly type: 'dispose'; readonly name?: string; readonly node: 'effect' | 'graph' | 'scope' }
|
|
21197
|
-
| { 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 };
|
|
21198
21647
|
|
|
21199
21648
|
interface Readable {
|
|
21200
21649
|
readonly name?: string;
|
|
21201
21650
|
peek(): T;
|
|
21651
|
+
/** Change-only: the listener is not called on subscribe, only after a tracked source changes. */
|
|
21202
21652
|
subscribe(listener: () => void): Unsubscribe;
|
|
21203
21653
|
readonly value: T;
|
|
21204
21654
|
}
|
|
@@ -21239,7 +21689,6 @@ interface Ripple extends Disposable {
|
|
|
21239
21689
|
options?: ResourceOptions,
|
|
21240
21690
|
): Resource;
|
|
21241
21691
|
signal(initial: T, options?: SignalOptions): Signal;
|
|
21242
|
-
tap(handler: (event: RippleEvent) => void, options?: { signal?: AbortSignal }): Unsubscribe;
|
|
21243
21692
|
untrack(fn: () => T): T;
|
|
21244
21693
|
watch(source: Readable | (() => T), callback: (value: T, previous: T | undefined) => void, options?: WatchOptions): EffectHandle;
|
|
21245
21694
|
}
|
|
@@ -21249,12 +21698,13 @@ interface Ripple extends Disposable {
|
|
|
21249
21698
|
|
|
21250
21699
|
| Error | Trigger | Notable properties |
|
|
21251
21700
|
| --- | --- | --- |
|
|
21252
|
-
| `RippleError` | Base Ripple error | Use `instanceof RippleError` to narrow unknown values. |
|
|
21253
|
-
| `
|
|
21254
|
-
| `
|
|
21255
|
-
|
|
21256
|
-
|
|
21257
|
-
|
|
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.
|
|
21258
21708
|
|
|
21259
21709
|
### Usage Guide
|
|
21260
21710
|
|
|
@@ -21280,10 +21730,8 @@ Use `createRipple()` for tests, SSR requests, embedded applications, or independ
|
|
|
21280
21730
|
```ts
|
|
21281
21731
|
import { createRipple } from '@vielzeug/ripple';
|
|
21282
21732
|
|
|
21283
|
-
const ripple = createRipple({
|
|
21284
|
-
|
|
21285
|
-
ripple.tap((event) => {
|
|
21286
|
-
if (event.type === 'error') console.log(event.context.kind, event.error);
|
|
21733
|
+
const ripple = createRipple({
|
|
21734
|
+
onError: (error, context) => console.log(context.kind, error),
|
|
21287
21735
|
});
|
|
21288
21736
|
|
|
21289
21737
|
const count = ripple.signal(0);
|
|
@@ -21369,26 +21817,28 @@ const stopWatch = ripple.watch(
|
|
|
21369
21817
|
stopWatch.dispose();
|
|
21370
21818
|
```
|
|
21371
21819
|
|
|
21372
|
-
##
|
|
21820
|
+
## Error Handling
|
|
21373
21821
|
|
|
21374
|
-
|
|
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:
|
|
21375
21823
|
|
|
21376
21824
|
```ts
|
|
21377
|
-
const ripple = createRipple({
|
|
21378
|
-
|
|
21379
|
-
|
|
21380
|
-
|
|
21381
|
-
|
|
21382
|
-
console.log(`${event.name}: ${event.previous} → ${event.next}`);
|
|
21383
|
-
break;
|
|
21384
|
-
case 'error':
|
|
21385
|
-
console.error(`${event.context.kind}:`, event.error);
|
|
21386
|
-
break;
|
|
21387
|
-
}
|
|
21825
|
+
const ripple = createRipple({
|
|
21826
|
+
onError: (error, context) => {
|
|
21827
|
+
// context.kind: 'cleanup' | 'computed' | 'effect' | 'flush' | 'listener'
|
|
21828
|
+
console.error(`${context.kind}:`, error);
|
|
21829
|
+
},
|
|
21388
21830
|
});
|
|
21389
21831
|
```
|
|
21390
21832
|
|
|
21391
|
-
`
|
|
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.
|
|
21392
21842
|
|
|
21393
21843
|
## Bridging External Sources
|
|
21394
21844
|
|
|
@@ -21424,15 +21874,19 @@ const user = ripple.resource(
|
|
|
21424
21874
|
|
|
21425
21875
|
const stop = ripple.effect(() => {
|
|
21426
21876
|
const state = user.value;
|
|
21427
|
-
if (state.status === 'success')
|
|
21428
|
-
if (state.status === 'error')
|
|
21877
|
+
if (state.status === 'success') console.log(state.value);
|
|
21878
|
+
if (state.status === 'error') console.error(state.error);
|
|
21429
21879
|
});
|
|
21430
21880
|
|
|
21431
21881
|
stop.dispose();
|
|
21432
21882
|
user.dispose();
|
|
21433
21883
|
```
|
|
21434
21884
|
|
|
21435
|
-
|
|
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.
|
|
21436
21890
|
|
|
21437
21891
|
## Object State
|
|
21438
21892
|
|
|
@@ -21552,13 +22006,21 @@ ripple.dispose();
|
|
|
21552
22006
|
|
|
21553
22007
|
## Gotchas
|
|
21554
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
|
+
|
|
21555
22013
|
### `subscribe()` forces computed evaluation
|
|
21556
22014
|
|
|
21557
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.
|
|
21558
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
|
+
|
|
21559
22021
|
### Computed first-run failure is recoverable
|
|
21560
22022
|
|
|
21561
|
-
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.
|
|
21562
22024
|
|
|
21563
22025
|
## Best Practices
|
|
21564
22026
|
|
|
@@ -21569,8 +22031,7 @@ If a computed's `derive` throws on its first run (e.g., a source is `null`), the
|
|
|
21569
22031
|
- Batch related synchronous writes.
|
|
21570
22032
|
- Use `watch()` only for selected source transitions; callback-only reads are untracked.
|
|
21571
22033
|
- Dispose external-source bridges when their owner ends.
|
|
21572
|
-
-
|
|
21573
|
-
- 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.
|
|
21574
22035
|
|
|
21575
22036
|
### Examples
|
|
21576
22037
|
|
|
@@ -21589,7 +22050,7 @@ If a computed's `derive` throws on its first run (e.g., a source is `null`), the
|
|
|
21589
22050
|
- Async Resource (id: `async-resource`)
|
|
21590
22051
|
- Create Graph, Signal, Computed & Effect (id: `basic-signal`)
|
|
21591
22052
|
- Batch & Untrack (id: `batch-untrack`)
|
|
21592
|
-
-
|
|
22053
|
+
- Named Effect and onError (id: `effect-options`)
|
|
21593
22054
|
- Nested Effect Ownership (id: `scope-ownership`)
|
|
21594
22055
|
- Immutable State (id: `store-basics`)
|
|
21595
22056
|
- Watch Selected Value (id: `watch-selected-value`)
|
|
@@ -28348,7 +28809,9 @@ try {
|
|
|
28348
28809
|
- `createPageSource()`: loads numbered pages and exposes direct navigation commands
|
|
28349
28810
|
- `createCursorSource()`: follows opaque cursors returned by the loader
|
|
28350
28811
|
- `createInfiniteSource()`: appends pages while preserving loaded items
|
|
28812
|
+
- `createResource()`: starts loading one value immediately, or awaits `reload()` with `start: false`
|
|
28351
28813
|
- `setParams()`: replaces consumer-owned loader parameters and resets pagination
|
|
28814
|
+
- `PageSource.apply()`: reconciles page and structurally compared params in at most one request
|
|
28352
28815
|
- `subscribe()`: publishes complete immutable state replacements
|
|
28353
28816
|
|
|
28354
28817
|
## Documentation
|
|
@@ -28375,7 +28838,10 @@ try {
|
|
|
28375
28838
|
| `createPageSource()` | Load numbered pages | Async | Construction is inert |
|
|
28376
28839
|
| `createCursorSource()` | Load server-issued cursor pages | Async | `after` and `before` cannot coexist initially |
|
|
28377
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 |
|
|
28378
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 |
|
|
28379
28845
|
|
|
28380
28846
|
## Package Entry Point
|
|
28381
28847
|
|
|
@@ -28460,6 +28926,7 @@ await source.reload();
|
|
|
28460
28926
|
|
|
28461
28927
|
| Method | Returns | Description |
|
|
28462
28928
|
| --- | --- | --- |
|
|
28929
|
+
| `apply(update)` | `Promise` | Reconcile `{ params, page }` in at most one load; params compare structurally |
|
|
28463
28930
|
| `first()` | `Promise` | Load page one |
|
|
28464
28931
|
| `goTo(page)` | `Promise` | Load a positive page |
|
|
28465
28932
|
| `last()` | `Promise` | Load the last known page |
|
|
@@ -28471,6 +28938,14 @@ await source.reload();
|
|
|
28471
28938
|
| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |
|
|
28472
28939
|
| `dispose()` | `void` | Abort work and stop notifications |
|
|
28473
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
|
+
|
|
28474
28949
|
---
|
|
28475
28950
|
|
|
28476
28951
|
### `createCursorSource()`
|
|
@@ -28548,6 +29023,54 @@ await source.loadMore();
|
|
|
28548
29023
|
| `subscribe(listener)` | `() => void` | Register a state listener and return its unsubscribe function |
|
|
28549
29024
|
| `dispose()` | `void` | Abort work and stop notifications |
|
|
28550
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
|
+
|
|
28551
29074
|
## Types
|
|
28552
29075
|
|
|
28553
29076
|
### Page source types
|
|
@@ -28566,8 +29089,11 @@ type PageSourceConfig = Readonly): Promise>;
|
|
|
28566
29089
|
} & (undefined extends TParams ? { params?: TParams } : { params: TParams })
|
|
28567
29090
|
>;
|
|
28568
29091
|
|
|
29092
|
+
type PageSourceUpdate = Readonly;
|
|
29093
|
+
|
|
28569
29094
|
type PageSource = {
|
|
28570
29095
|
[Symbol.dispose](): void;
|
|
29096
|
+
apply(update: PageSourceUpdate): Promise;
|
|
28571
29097
|
readonly disposalSignal: AbortSignal;
|
|
28572
29098
|
dispose(): void;
|
|
28573
29099
|
readonly disposed: boolean;
|
|
@@ -28586,6 +29112,28 @@ type PageSource = {
|
|
|
28586
29112
|
|
|
28587
29113
|
The conditional config field makes `params` optional only when `TParams` includes `undefined`.
|
|
28588
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
|
+
|
|
28589
29137
|
### Local source types
|
|
28590
29138
|
|
|
28591
29139
|
```ts
|
|
@@ -28734,7 +29282,7 @@ console.log(source.state.params);
|
|
|
28734
29282
|
source.dispose();
|
|
28735
29283
|
```
|
|
28736
29284
|
|
|
28737
|
-
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).
|
|
28738
29286
|
|
|
28739
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`.
|
|
28740
29288
|
|
|
@@ -28754,6 +29302,47 @@ await source.setPageSize(50);
|
|
|
28754
29302
|
|
|
28755
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.
|
|
28756
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
|
+
|
|
28757
29346
|
## Filter Local Collections
|
|
28758
29347
|
|
|
28759
29348
|
Use a local source when data is already in memory. Its commands are synchronous.
|
|
@@ -28829,6 +29418,44 @@ source.dispose();
|
|
|
28829
29418
|
|
|
28830
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.
|
|
28831
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
|
+
|
|
28832
29459
|
## Handle Requests and Errors
|
|
28833
29460
|
|
|
28834
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.
|
|
@@ -28961,6 +29588,7 @@ Use Scout to compute indexed local matches, Ripple to project state into reactiv
|
|
|
28961
29588
|
- [Page Params with URL State](./examples/remote-search-with-url-state.md)
|
|
28962
29589
|
- [Cursor-Based Pagination](./examples/cursor-based-pagination.md)
|
|
28963
29590
|
- [Infinite Scroll](./examples/infinite-scroll.md)
|
|
29591
|
+
- [Load a Single Value](./examples/single-value-resource.md)
|
|
28964
29592
|
- [Framework Integration](./examples/framework-integration.md)
|
|
28965
29593
|
- [Remote Data with Courier](./examples/sourcerer-with-courier.md)
|
|
28966
29594
|
- [Reactive Controls with Ripple](./examples/sourcerer-with-ripple.md)
|
|
@@ -28968,12 +29596,14 @@ Use Scout to compute indexed local matches, Ripple to project state into reactiv
|
|
|
28968
29596
|
|
|
28969
29597
|
### REPL Examples
|
|
28970
29598
|
|
|
29599
|
+
- apply - Page and params in one call (id: `apply`)
|
|
28971
29600
|
- Cursor Source (id: `cursor-source`)
|
|
28972
29601
|
- Error Handling (id: `error-handling`)
|
|
28973
29602
|
- Infinite Source (id: `infinite-source`)
|
|
28974
29603
|
- Source Lifecycle (id: `lifecycle`)
|
|
28975
29604
|
- Local Source (id: `local-source`)
|
|
28976
29605
|
- Page Source (id: `page-source`)
|
|
29606
|
+
- createResource - One reactive value (id: `resource`)
|
|
28977
29607
|
|
|
28978
29608
|
---
|
|
28979
29609
|
|