@vielzeug/codex 2.2.9 → 2.3.0
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 -30
- package/data/llms-full.txt +1472 -370
- package/data/llms.txt +2 -1
- package/data/manifest.json +1 -1
- package/data/packages/clockwork.json +2 -2
- package/data/packages/conduit.json +1 -1
- package/data/packages/courier.json +7 -6
- package/data/packages/dnd.json +1 -1
- package/data/packages/familiar.json +1 -1
- package/data/packages/forge.json +1 -1
- package/data/packages/gesture.json +1 -1
- package/data/packages/herald.json +18 -18
- package/data/packages/keymap.json +2 -2
- package/data/packages/lingua.json +1 -1
- package/data/packages/necromancer.json +1 -1
- package/data/packages/ore.json +1 -1
- package/data/packages/postmaster.json +45 -0
- package/data/packages/pulse.json +31 -30
- package/data/packages/scout.json +13 -12
- package/data/packages/scroll.json +1 -1
- package/data/packages/sentinel.json +1 -1
- package/data/packages/spell.json +1 -1
- package/data/packages/vault.json +22 -28
- package/data/packages/ward.json +28 -28
- package/data/packages/wayfinder.json +5 -5
- package/data/refine.json +3926 -3926
- package/data/search.json +76 -54
- package/package.json +2 -1
package/data/llms-full.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vielzeug — Full Documentation
|
|
2
2
|
|
|
3
|
-
> Complete documentation for
|
|
3
|
+
> Complete documentation for 37 packages. Version: 2.2.9
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -977,7 +977,7 @@ actor.send({ type: 'INC' });
|
|
|
977
977
|
| `Machine.transition()` | Resolve a pure next snapshot | Sync | Does not run effects, invokes, or timers |
|
|
978
978
|
| `Machine.createActor()` | Create a runtime owner | Sync | Fresh and restored actors have different entry behavior |
|
|
979
979
|
| `Actor.send()` | Dispatch an event | Sync | Returns `void`; re-entrant events queue internally |
|
|
980
|
-
| `
|
|
980
|
+
| `Actor.subscribe()` | Observe committed snapshots | Sync | Observes only; it does not trace sends or errors |
|
|
981
981
|
| `ClockworkError` | Report definition and snapshot validation failures | Sync | Use `code`, not message text |
|
|
982
982
|
|
|
983
983
|
## Package Entry Points
|
|
@@ -985,7 +985,6 @@ actor.send({ type: 'INC' });
|
|
|
985
985
|
| Import | Purpose |
|
|
986
986
|
| --- | --- |
|
|
987
987
|
| `@vielzeug/clockwork` | Machine compiler, actor runtime, errors, and types |
|
|
988
|
-
| `@vielzeug/clockwork/devtools` | Opt-in snapshot observation through `debugActor()` |
|
|
989
988
|
|
|
990
989
|
## Core Functions
|
|
991
990
|
|
|
@@ -1018,24 +1017,18 @@ Throws `ClockworkError` when a definition has an invalid context, initial state,
|
|
|
1018
1017
|
|
|
1019
1018
|
---
|
|
1020
1019
|
|
|
1021
|
-
### `
|
|
1020
|
+
### `Actor.subscribe()`
|
|
1022
1021
|
|
|
1023
1022
|
```ts
|
|
1024
|
-
|
|
1025
|
-
actor: Actor,
|
|
1026
|
-
options?: DebugActorOptions,
|
|
1027
|
-
): () => void;
|
|
1023
|
+
subscribe(listener: (snapshot: ActorSnapshot) => void): () => void;
|
|
1028
1024
|
```
|
|
1029
1025
|
|
|
1030
|
-
Subscribes to committed actor snapshots
|
|
1031
|
-
|
|
1032
|
-
**Returns:** An unsubscribe cleanup function.
|
|
1026
|
+
Subscribes to committed actor snapshots. Returns an unsubscribe function. The listener receives the current snapshot immediately on subscribe, then on every committed transition. It does not observe dispatched events or runtime errors.
|
|
1033
1027
|
|
|
1034
1028
|
**Example:**
|
|
1035
1029
|
|
|
1036
1030
|
```ts
|
|
1037
1031
|
import { defineMachine } from '@vielzeug/clockwork';
|
|
1038
|
-
import { debugActor } from '@vielzeug/clockwork/devtools';
|
|
1039
1032
|
|
|
1040
1033
|
const machine = defineMachine, { type: 'NEXT' }>()({
|
|
1041
1034
|
initial: 'idle',
|
|
@@ -1043,9 +1036,9 @@ const machine = defineMachine, { type: 'NEXT' }>()({
|
|
|
1043
1036
|
});
|
|
1044
1037
|
|
|
1045
1038
|
const actor = machine.createActor();
|
|
1046
|
-
const
|
|
1039
|
+
const stop = actor.subscribe((snapshot) => console.debug(snapshot));
|
|
1047
1040
|
actor.send({ type: 'NEXT' });
|
|
1048
|
-
|
|
1041
|
+
stop();
|
|
1049
1042
|
actor.dispose();
|
|
1050
1043
|
```
|
|
1051
1044
|
|
|
@@ -1364,16 +1357,6 @@ type Machine, Event extends MachineEvent> = {
|
|
|
1364
1357
|
|
|
1365
1358
|
A compiled, reusable machine. Its transition lookup is map-based, so unknown or poison event names such as `__proto__` are safely ignored when no transition exists.
|
|
1366
1359
|
|
|
1367
|
-
### `DebugActorOptions`
|
|
1368
|
-
|
|
1369
|
-
```ts
|
|
1370
|
-
type DebugActorOptions> = {
|
|
1371
|
-
readonly logger?: (snapshot: MachineSnapshot) => void;
|
|
1372
|
-
};
|
|
1373
|
-
```
|
|
1374
|
-
|
|
1375
|
-
Optional logger for `debugActor()`. Logger failures are ignored so observation cannot affect the actor's error policy.
|
|
1376
|
-
|
|
1377
1360
|
## Errors
|
|
1378
1361
|
|
|
1379
1362
|
### `ClockworkErrorCode`
|
|
@@ -1622,19 +1605,17 @@ Without `onError`, an actor disposes silently. Return `'dispose'` explicitly whe
|
|
|
1622
1605
|
|
|
1623
1606
|
## Debugging
|
|
1624
1607
|
|
|
1625
|
-
Use
|
|
1608
|
+
Use `actor.subscribe()` to observe committed snapshots during development. It observes snapshots only; it does not trace dispatched events or runtime errors.
|
|
1626
1609
|
|
|
1627
1610
|
```ts
|
|
1628
|
-
import { debugActor } from '@vielzeug/clockwork/devtools';
|
|
1629
|
-
|
|
1630
1611
|
const actor = machine.createActor();
|
|
1631
|
-
const
|
|
1612
|
+
const stop = actor.subscribe((snapshot) => console.debug(snapshot));
|
|
1632
1613
|
actor.send({ type: 'NEXT' });
|
|
1633
|
-
|
|
1614
|
+
stop();
|
|
1634
1615
|
actor.dispose();
|
|
1635
1616
|
```
|
|
1636
1617
|
|
|
1637
|
-
For richer inspection,
|
|
1618
|
+
For richer inspection, route snapshots to application devtools. Clockwork intentionally has no internal trace buffer.
|
|
1638
1619
|
|
|
1639
1620
|
## Flat state maps
|
|
1640
1621
|
|
|
@@ -2804,7 +2785,7 @@ interface Container {
|
|
|
2804
2785
|
|
|
2805
2786
|
## Errors
|
|
2806
2787
|
|
|
2807
|
-
- `ConduitError` — base class; `ConduitError
|
|
2788
|
+
- `ConduitError` — base class; use `instanceof ConduitError` to narrow package errors.
|
|
2808
2789
|
- `ConduitProviderNotFoundError` — dependency has no registration.
|
|
2809
2790
|
- `ConduitCircularDependencyError` — static factory tuple graph contains a cycle.
|
|
2810
2791
|
- `ConduitDuplicateRegistrationError` — token registered twice in one container.
|
|
@@ -3035,6 +3016,7 @@ try {
|
|
|
3035
3016
|
- **`queries.fetch()`** — key-based cached reads, subscriptions, invalidation with refetch, and automatic garbage collection.
|
|
3036
3017
|
- **`mutate()`** — direct write operation with `invalidateKeys` for one-step cache refetch, without hidden retries or a second state store.
|
|
3037
3018
|
- **`events()` / `read()`** — abortable SSE, text, and NDJSON iteration with normalized request errors.
|
|
3019
|
+
- **`tap()`** — runtime observability for request lifecycle events (start, success, error).
|
|
3038
3020
|
- **`withBearerAuth()` / `withRequestId()` / `withLogging()`** — composable transport policies.
|
|
3039
3021
|
|
|
3040
3022
|
## Documentation
|
|
@@ -3195,6 +3177,42 @@ fields; it does not retain event IDs or reconnect.
|
|
|
3195
3177
|
|
|
3196
3178
|
---
|
|
3197
3179
|
|
|
3180
|
+
## Observability
|
|
3181
|
+
|
|
3182
|
+
### `tap()`
|
|
3183
|
+
|
|
3184
|
+
```ts
|
|
3185
|
+
tap(handler: (event: CourierEvent) => void, options?: { signal?: AbortSignal }): () => void;
|
|
3186
|
+
```
|
|
3187
|
+
|
|
3188
|
+
Observe request lifecycle events without affecting courier behavior. Handler errors are swallowed. Returns an unsubscribe function.
|
|
3189
|
+
|
|
3190
|
+
```ts
|
|
3191
|
+
type CourierEvent =
|
|
3192
|
+
| { type: 'request-start'; method: string; url: string }
|
|
3193
|
+
| { type: 'request-success'; method: string; url: string; status: number; duration: number }
|
|
3194
|
+
| { type: 'request-error'; method: string; url: string; error: unknown }
|
|
3195
|
+
| { type: 'dispose' };
|
|
3196
|
+
```
|
|
3197
|
+
|
|
3198
|
+
**Example:**
|
|
3199
|
+
|
|
3200
|
+
```ts
|
|
3201
|
+
const courier = createCourier({ baseUrl: '/api' });
|
|
3202
|
+
courier.tap((event) => {
|
|
3203
|
+
if (event.type === 'request-error') console.error(event.method, event.url, event.error);
|
|
3204
|
+
if (event.type === 'request-success') console.debug(event.method, event.url, event.duration);
|
|
3205
|
+
});
|
|
3206
|
+
```
|
|
3207
|
+
|
|
3208
|
+
For structured logging, route tap events to rune:
|
|
3209
|
+
|
|
3210
|
+
```ts
|
|
3211
|
+
import { createLogger } from '@vielzeug/rune';
|
|
3212
|
+
const log = createLogger({ name: 'courier' });
|
|
3213
|
+
courier.tap((event) => log.debug(event, `courier:${event.type}`));
|
|
3214
|
+
```
|
|
3215
|
+
|
|
3198
3216
|
## Interceptors
|
|
3199
3217
|
|
|
3200
3218
|
### Interceptor helpers
|
|
@@ -3775,7 +3793,7 @@ using sortable = createSortable({
|
|
|
3775
3793
|
| `DropZoneOptions.accept` | Filter file types before processing | Sync | Mismatch between MIME and extension can reject files unexpectedly |
|
|
3776
3794
|
| `DropZoneOptions.maxFiles` | Cap accepted files per drop | Sync | Excess accepted files become rejected; `onDropRejected` is called |
|
|
3777
3795
|
| `matchesAccept()` | Test a single `File` against an accept list | Sync | Extension patterns are case-insensitive; empty list accepts all |
|
|
3778
|
-
| `DndError` | Base class for Dnd errors | Sync | Use `DndError
|
|
3796
|
+
| `DndError` | Base class for Dnd errors | Sync | Use `instanceof DndError` to narrow unknown errors |
|
|
3779
3797
|
|
|
3780
3798
|
## Package Entry Point
|
|
3781
3799
|
|
|
@@ -4250,7 +4268,7 @@ const next = applyReorder(items, orderedIds, (item) => item.id);
|
|
|
4250
4268
|
|
|
4251
4269
|
| Error | Trigger | Notable property |
|
|
4252
4270
|
| --- | --- | --- |
|
|
4253
|
-
| `DndError` | Base class for package errors | `DndError
|
|
4271
|
+
| `DndError` | Base class for package errors | Use `instanceof DndError` to narrow |
|
|
4254
4272
|
| `DndScopeError` | A sortable receives a scope not created by `createSortableScope()` | — |
|
|
4255
4273
|
|
|
4256
4274
|
### Usage Guide
|
|
@@ -5257,7 +5275,7 @@ type StreamHandler = (input: TInput) => AsyncIterable | Promise>;
|
|
|
5257
5275
|
|
|
5258
5276
|
| Error | Trigger | Notable property |
|
|
5259
5277
|
| --- | --- | --- |
|
|
5260
|
-
| `FamiliarError` | Base class for all Familiar errors | `FamiliarError
|
|
5278
|
+
| `FamiliarError` | Base class for all Familiar errors | Use `instanceof FamiliarError` to narrow |
|
|
5261
5279
|
| `FamiliarInvalidOptionsError` | Invalid factory or test options | — |
|
|
5262
5280
|
| `FamiliarQueueFullError` | Queue limit reached with `onFull: 'reject'` | `maxQueue` |
|
|
5263
5281
|
| `FamiliarTaskError` | Worker handler throws or payload cannot clone | `cause` |
|
|
@@ -7211,7 +7229,7 @@ type FormDraftCodec, S extends AnySchema, K extends keyof S & string> = Readonly
|
|
|
7211
7229
|
|
|
7212
7230
|
| Error | Trigger | Notable properties |
|
|
7213
7231
|
| --- | --- | --- |
|
|
7214
|
-
| `ForgeError` | Base Forge error | `ForgeError
|
|
7232
|
+
| `ForgeError` | Base Forge error | Use `instanceof ForgeError` to narrow unknown values. |
|
|
7215
7233
|
| `ForgeConfigError` | Unsafe key or unsupported form value | Extends `ForgeError`. |
|
|
7216
7234
|
| `ForgeDisposedError` | Operation or subscription after disposal | Message names the attempted operation. |
|
|
7217
7235
|
| `ForgeSubmitError` | Concurrent `submit()` call | Extends `ForgeError`. |
|
|
@@ -7832,7 +7850,7 @@ Gesture tracks a constrained pointer pan. Dnd owns draggable items, sortable lis
|
|
|
7832
7850
|
|
|
7833
7851
|
## Examples
|
|
7834
7852
|
|
|
7835
|
-
- [Carousel
|
|
7853
|
+
- [Carousel Pan Navigation](./examples/carousel-swipe-navigation.md)
|
|
7836
7854
|
- [Swipe-to-Dismiss Notifications](./examples/swipe-dismiss-notifications.md)
|
|
7837
7855
|
|
|
7838
7856
|
### REPL Examples
|
|
@@ -7920,12 +7938,12 @@ bus.dispose();
|
|
|
7920
7938
|
|
|
7921
7939
|
- `on()` / `once()` — typed subscriptions with explicit teardown
|
|
7922
7940
|
- `onAny()` — cross-cutting event observation
|
|
7941
|
+
- `tap()` — observe bus activity for logging and diagnostics
|
|
7923
7942
|
- `wait()` / `waitAny()` — one-shot async coordination
|
|
7924
7943
|
- `events()` — bounded async event streams
|
|
7925
7944
|
- `pipeEvents()` — compatible cross-bus forwarding
|
|
7926
7945
|
- `AbortSignal` — cancellation and disposal ownership
|
|
7927
7946
|
- `createTestBus()` — emitted-payload recording for tests
|
|
7928
|
-
- `debugBus()` — development logging from `/devtools`
|
|
7929
7947
|
|
|
7930
7948
|
## Documentation
|
|
7931
7949
|
|
|
@@ -7950,7 +7968,6 @@ bus.dispose();
|
|
|
7950
7968
|
| `pipeEvents()` | Forward compatible source events | Sync | Payloads must be assignable to target event |
|
|
7951
7969
|
| `combineSignals()` | Abort when any input aborts | Sync | Public composition has no manual teardown |
|
|
7952
7970
|
| `createTestBus()` | Record dispatched test events | Sync | Available from `/testing` only |
|
|
7953
|
-
| `debugBus()` | Create console-debug instrumented bus | Sync | Available from `/devtools` only |
|
|
7954
7971
|
|
|
7955
7972
|
## Package Entry Point
|
|
7956
7973
|
|
|
@@ -7958,7 +7975,6 @@ bus.dispose();
|
|
|
7958
7975
|
| --- | --- |
|
|
7959
7976
|
| `@vielzeug/herald` | Runtime bus, pipes, public types, and errors |
|
|
7960
7977
|
| `@vielzeug/herald/testing` | `createTestBus()` and `TestBus` |
|
|
7961
|
-
| `@vielzeug/herald/devtools` | `debugBus()` |
|
|
7962
7978
|
|
|
7963
7979
|
## Core Functions
|
|
7964
7980
|
|
|
@@ -7974,7 +7990,7 @@ Creates a synchronous bus for future event delivery.
|
|
|
7974
7990
|
|
|
7975
7991
|
| Parameter | Type | Description |
|
|
7976
7992
|
| --- | --- | --- |
|
|
7977
|
-
| `options` | `BusOptions` | Optional middleware, validation, error handling,
|
|
7993
|
+
| `options` | `BusOptions` | Optional middleware, validation, error handling, and listener threshold configuration. |
|
|
7978
7994
|
|
|
7979
7995
|
**Returns:** `Bus`.
|
|
7980
7996
|
|
|
@@ -8073,7 +8089,6 @@ type EventKey = Extract;
|
|
|
8073
8089
|
|
|
8074
8090
|
```ts
|
|
8075
8091
|
type BusOptions = {
|
|
8076
|
-
logger?: BusLogger;
|
|
8077
8092
|
maxListeners?: number;
|
|
8078
8093
|
middleware?: readonly Middleware[];
|
|
8079
8094
|
name?: string;
|
|
@@ -8084,10 +8099,9 @@ type BusOptions = {
|
|
|
8084
8099
|
|
|
8085
8100
|
| Field | Description |
|
|
8086
8101
|
| --- | --- |
|
|
8087
|
-
| `logger` | Optional debug and warning output. |
|
|
8088
8102
|
| `maxListeners` | Warn when one event exceeds this active-listener count. |
|
|
8089
8103
|
| `middleware` | Synchronous dispatch middleware. |
|
|
8090
|
-
| `name` | Display name in
|
|
8104
|
+
| `name` | Display name in disposal errors. |
|
|
8091
8105
|
| `onError` | Handles listener and validation errors instead of rethrowing. |
|
|
8092
8106
|
| `validatePayload` | Runs before middleware and listeners. |
|
|
8093
8107
|
|
|
@@ -8108,6 +8122,7 @@ type Bus = {
|
|
|
8108
8122
|
on>(event: K, listener: Listener, opts?: SubscribeOptions): Unsubscribe;
|
|
8109
8123
|
onAny(listener: (event: EventKey, payload: unknown) => void, opts?: SubscribeOptions): Unsubscribe;
|
|
8110
8124
|
once>(event: K, listener: Listener, opts?: { signal?: AbortSignal }): Unsubscribe;
|
|
8125
|
+
tap(handler: (event: HeraldEvent) => void, options?: { signal?: AbortSignal }): Unsubscribe;
|
|
8111
8126
|
wait>(event: K, opts?: { signal?: AbortSignal }): Promise;
|
|
8112
8127
|
waitAny, EventKey, ...EventKey[]]>(
|
|
8113
8128
|
events: K,
|
|
@@ -8119,16 +8134,20 @@ type Bus = {
|
|
|
8119
8134
|
|
|
8120
8135
|
`emit()` returns listener count or `0` after disposal, blocked middleware, or handled validation rejection.
|
|
8121
8136
|
|
|
8137
|
+
`tap()` receives every `emit`, `subscribe`, `unsubscribe`, `listener-error`, and `dispose` event as a `HeraldEvent`. It is the supported way to observe bus activity for logging and diagnostics. The returned `Unsubscribe` stops the tap; pass `{ signal }` to bind its lifetime to an `AbortSignal`.
|
|
8138
|
+
|
|
8139
|
+
```ts
|
|
8140
|
+
import { createBus } from '@vielzeug/herald';
|
|
8141
|
+
|
|
8142
|
+
const bus = createBus();
|
|
8143
|
+
const stop = bus.tap((event) => console.debug(`herald:${event.type}`, event));
|
|
8144
|
+
```
|
|
8145
|
+
|
|
8122
8146
|
---
|
|
8123
8147
|
|
|
8124
|
-
### `
|
|
8148
|
+
### `Listener`, `SubscribeOptions`, and `Unsubscribe`
|
|
8125
8149
|
|
|
8126
8150
|
```ts
|
|
8127
|
-
type BusLogger = {
|
|
8128
|
-
debug?: (message: string) => void;
|
|
8129
|
-
warn?: (message: string) => void;
|
|
8130
|
-
};
|
|
8131
|
-
|
|
8132
8151
|
type Listener = (payload: T) => void;
|
|
8133
8152
|
type SubscribeOptions = { once?: boolean; signal?: AbortSignal };
|
|
8134
8153
|
type Unsubscribe = () => void;
|
|
@@ -8136,6 +8155,21 @@ type Unsubscribe = () => void;
|
|
|
8136
8155
|
|
|
8137
8156
|
---
|
|
8138
8157
|
|
|
8158
|
+
### `HeraldEvent`
|
|
8159
|
+
|
|
8160
|
+
```ts
|
|
8161
|
+
type HeraldEvent =
|
|
8162
|
+
| { type: 'emit'; event: EventKey; payload: unknown; timestamp: number }
|
|
8163
|
+
| { type: 'subscribe'; event: EventKey; timestamp: number }
|
|
8164
|
+
| { type: 'unsubscribe'; event: EventKey; timestamp: number }
|
|
8165
|
+
| { type: 'listener-error'; event: EventKey; err: unknown; timestamp: number }
|
|
8166
|
+
| { type: 'dispose'; timestamp: number };
|
|
8167
|
+
```
|
|
8168
|
+
|
|
8169
|
+
Discriminated union delivered to `tap()` handlers. Narrow on `event.type` to access type-specific fields.
|
|
8170
|
+
|
|
8171
|
+
---
|
|
8172
|
+
|
|
8139
8173
|
### `EmissionErrorContext` and `Middleware`
|
|
8140
8174
|
|
|
8141
8175
|
```ts
|
|
@@ -8187,7 +8221,7 @@ type PipeEntry =
|
|
|
8187
8221
|
| RenamedPipeEntry;
|
|
8188
8222
|
```
|
|
8189
8223
|
|
|
8190
|
-
## Testing
|
|
8224
|
+
## Testing
|
|
8191
8225
|
|
|
8192
8226
|
### `createTestBus()`
|
|
8193
8227
|
|
|
@@ -8212,16 +8246,6 @@ type TestBus = Bus & {
|
|
|
8212
8246
|
};
|
|
8213
8247
|
```
|
|
8214
8248
|
|
|
8215
|
-
### `debugBus()`
|
|
8216
|
-
|
|
8217
|
-
```ts
|
|
8218
|
-
function debugBus(
|
|
8219
|
-
options?: Omit, 'logger'> & { logger?: { warn?: BusLogger['warn'] } },
|
|
8220
|
-
): Bus;
|
|
8221
|
-
```
|
|
8222
|
-
|
|
8223
|
-
Creates a bus with `console.debug` logging. Import from `@vielzeug/herald/devtools`.
|
|
8224
|
-
|
|
8225
8249
|
## Errors
|
|
8226
8250
|
|
|
8227
8251
|
| Error | Trigger | Notable properties |
|
|
@@ -8285,10 +8309,22 @@ bus.dispose();
|
|
|
8285
8309
|
|
|
8286
8310
|
## Debugging
|
|
8287
8311
|
|
|
8312
|
+
`tap()` observes every bus activity as a `HeraldEvent` — use it for logging and diagnostics.
|
|
8313
|
+
|
|
8288
8314
|
```ts
|
|
8289
|
-
import {
|
|
8315
|
+
import { createBus } from '@vielzeug/herald';
|
|
8316
|
+
|
|
8317
|
+
const bus = createBus();
|
|
8318
|
+
bus.tap((event) => console.debug(`herald:${event.type}`, event));
|
|
8319
|
+
```
|
|
8320
|
+
|
|
8321
|
+
Integrate with the Rune logger:
|
|
8322
|
+
|
|
8323
|
+
```ts
|
|
8324
|
+
import { createLogger } from '@vielzeug/rune';
|
|
8290
8325
|
|
|
8291
|
-
const
|
|
8326
|
+
const log = createLogger({ name: 'herald' });
|
|
8327
|
+
bus.tap((event) => log.debug(event, `herald:${event.type}`));
|
|
8292
8328
|
```
|
|
8293
8329
|
|
|
8294
8330
|
## Working with Other Vielzeug Libraries
|
|
@@ -10042,7 +10078,7 @@ const map = createKeymap(
|
|
|
10042
10078
|
|
|
10043
10079
|
| Error | Trigger | Notable properties |
|
|
10044
10080
|
| --- | --- | --- |
|
|
10045
|
-
| `KeymapError` | Lifecycle operation after disposal | `KeymapError
|
|
10081
|
+
| `KeymapError` | Lifecycle operation after disposal | Use `instanceof KeymapError` to narrow Keymap errors. |
|
|
10046
10082
|
| `KeymapParseError` | Strict shortcut parser receives invalid input | Extends `KeymapError`. |
|
|
10047
10083
|
|
|
10048
10084
|
### Usage Guide
|
|
@@ -11084,7 +11120,7 @@ try {
|
|
|
11084
11120
|
| `createFormatter()` | Format Intl values from `/format` | Sync | Import from subpath |
|
|
11085
11121
|
| `validateCatalog()` | Check explicit plural forms from `/validate` | Sync | Import from subpath |
|
|
11086
11122
|
| `compareCatalogs()` | Compare key parity across locales from `/validate` | Sync | First locale is the base; import from subpath |
|
|
11087
|
-
| `LinguaError` | Base class for Lingua errors | Sync | Use `LinguaError
|
|
11123
|
+
| `LinguaError` | Base class for Lingua errors | Sync | Use `instanceof LinguaError` for broad narrowing |
|
|
11088
11124
|
|
|
11089
11125
|
## Package Entry Point
|
|
11090
11126
|
|
|
@@ -11922,7 +11958,7 @@ Necromancer owns explicit WAAPI keyframes. It does not generate CSS keyframes, o
|
|
|
11922
11958
|
| `animate()` | Animate one element | Sync | Defaults to a visible `180ms` duration |
|
|
11923
11959
|
| `animateEach()` | Animate a unique element group | Sync | Non-zero `stagger` needs numeric `delay` |
|
|
11924
11960
|
| `captureLayout()` | Capture positions and create a one-shot FLIP transition | Sync | Capture before changing layout |
|
|
11925
|
-
| `NecromancerError` | Base package error | Sync | Use `NecromancerError
|
|
11961
|
+
| `NecromancerError` | Base package error | Sync | Use `instanceof NecromancerError` to narrow unknown errors |
|
|
11926
11962
|
|
|
11927
11963
|
## Package Entry Point
|
|
11928
11964
|
|
|
@@ -13695,7 +13731,6 @@ type OreErrorPhase = 'each-reconcile' | 'form-reset' | 'mounted' | 'setup';
|
|
|
13695
13731
|
## Errors
|
|
13696
13732
|
|
|
13697
13733
|
`OreError` is the base class for every Ore error class — `err instanceof OreError` catches all of them.
|
|
13698
|
-
`OreError.is(err)` is the equivalent static type-guard.
|
|
13699
13734
|
|
|
13700
13735
|
- **`OreApiError`** — thrown when the `ore` API itself is misused: calling `define()` with a duplicate tag, calling a lifecycle hook (`inject`, `onMounted`, `onCleanup`, `onEvent`, …) outside of `setup()`, or passing an invalid prop definition to `define()`.
|
|
13701
13736
|
- **`OreInternalError`** — thrown when an Ore invariant fails, indicating a package bug rather than invalid application code.
|
|
@@ -14192,7 +14227,996 @@ function App() {
|
|
|
14192
14227
|
import './x-toggle'; // wherever define('x-toggle', { ... }) is called
|
|
14193
14228
|
import { ref } from 'vue';
|
|
14194
14229
|
|
|
14195
|
-
const open = ref(false);
|
|
14230
|
+
const open = ref(false);
|
|
14231
|
+
|
|
14232
|
+
|
|
14233
|
+
|
|
14234
|
+
```
|
|
14235
|
+
|
|
14236
|
+
```svelte [Svelte]
|
|
14237
|
+
|
|
14238
|
+
import './x-toggle'; // wherever define('x-toggle', { ... }) is called
|
|
14239
|
+
|
|
14240
|
+
function handleClick() {
|
|
14241
|
+
console.log('toggled');
|
|
14242
|
+
}
|
|
14243
|
+
|
|
14244
|
+
```
|
|
14245
|
+
|
|
14246
|
+
## Working with Other Vielzeug Libraries
|
|
14247
|
+
|
|
14248
|
+
### With Ripple
|
|
14249
|
+
|
|
14250
|
+
Import ripple primitives directly from `@vielzeug/ripple` for standalone reactive state outside components.
|
|
14251
|
+
|
|
14252
|
+
```ts
|
|
14253
|
+
import { signal, computed } from '@vielzeug/ripple';
|
|
14254
|
+
import { define, html } from '@vielzeug/ore';
|
|
14255
|
+
|
|
14256
|
+
// Shared state created outside any component
|
|
14257
|
+
const theme = signal('light');
|
|
14258
|
+
const isDark = computed(() => theme.value === 'dark');
|
|
14259
|
+
|
|
14260
|
+
define('theme-toggle', {
|
|
14261
|
+
setup() {
|
|
14262
|
+
return html`
|
|
14263
|
+
(theme.value = isDark.value ? 'light' : 'dark')}>
|
|
14264
|
+
${() =>
|
|
14265
|
+
isDark.value ? '' : ''}
|
|
14266
|
+
|
|
14267
|
+
`;
|
|
14268
|
+
},
|
|
14269
|
+
});
|
|
14270
|
+
```
|
|
14271
|
+
|
|
14272
|
+
### With Forge
|
|
14273
|
+
|
|
14274
|
+
Use `@vielzeug/forge` for typed form state. `useField()` remains intentionally narrow: it connects a form-associated
|
|
14275
|
+
custom element to native `ElementInternals` without imposing submission, validation, or dirty-state policy.
|
|
14276
|
+
|
|
14277
|
+
```ts
|
|
14278
|
+
import { createForm } from '@vielzeug/forge';
|
|
14279
|
+
import { define, html } from '@vielzeug/ore';
|
|
14280
|
+
|
|
14281
|
+
define('signup-form', {
|
|
14282
|
+
setup(_props) {
|
|
14283
|
+
const form = createForm({ initialValues: { email: '' } });
|
|
14284
|
+
|
|
14285
|
+
return html`
|
|
14286
|
+
{
|
|
14287
|
+
event.preventDefault();
|
|
14288
|
+
void form.submit(async (values) => {
|
|
14289
|
+
console.log(values);
|
|
14290
|
+
});
|
|
14291
|
+
}}>
|
|
14292
|
+
|
|
14293
|
+
|
|
14294
|
+
`;
|
|
14295
|
+
},
|
|
14296
|
+
});
|
|
14297
|
+
```
|
|
14298
|
+
|
|
14299
|
+
## Best Practices
|
|
14300
|
+
|
|
14301
|
+
- Setup returns `html\`...\`` directly — not a function wrapping the template.
|
|
14302
|
+
- Use `watchEffect()` for reactive subscriptions tied to component lifetime — it auto-registers cleanup on disconnect.
|
|
14303
|
+
- Use `onElement(ref, cb)` instead of `onMounted` when the work is tied to a single DOM node.
|
|
14304
|
+
- Bind host attributes and classes via `bind()` rather than mutating the element directly.
|
|
14305
|
+
- Provide context at the nearest ancestor — avoid global context singletons.
|
|
14306
|
+
- Call `onCleanup()` for every resource allocated in `setup()` (WebSockets, intervals, external subscriptions).
|
|
14307
|
+
- Use `live(signal)` for form inputs to prevent clobbering user-in-progress edits.
|
|
14308
|
+
- 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.
|
|
14309
|
+
- Test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic DOM events, queries, and waits
|
|
14310
|
+
from `@vielzeug/assay`.
|
|
14311
|
+
|
|
14312
|
+
### Examples
|
|
14313
|
+
|
|
14314
|
+
## Examples
|
|
14315
|
+
|
|
14316
|
+
- [Counter Component](./examples/counter-component.md)
|
|
14317
|
+
- [Typed Props And Emits](./examples/typed-props-and-emits.md)
|
|
14318
|
+
- [Observers In onMounted()](./examples/observers-in-onmount.md)
|
|
14319
|
+
- [Search List With Directives](./examples/search-list-with-directives.md)
|
|
14320
|
+
- [Context Provider And Consumer](./examples/context-provider-and-consumer.md)
|
|
14321
|
+
- [Prop Helpers And Raw PropDef](./examples/propsof-builder-api.md)
|
|
14322
|
+
- [Form Associated Rating Input](./examples/form-associated-rating-input.md)
|
|
14323
|
+
- [Test Example With @vielzeug/ore/testing](./examples/test-example-at-vielzeug-ore-testing.md)
|
|
14324
|
+
|
|
14325
|
+
---
|
|
14326
|
+
|
|
14327
|
+
## @vielzeug/postmaster
|
|
14328
|
+
|
|
14329
|
+
**Category:** Async
|
|
14330
|
+
|
|
14331
|
+
### Overview
|
|
14332
|
+
|
|
14333
|
+
## Why Postmaster?
|
|
14334
|
+
|
|
14335
|
+
Application jobs that touch a remote service — posting a form, syncing state, sending analytics — must survive page reloads, resume later, retry according to an explicit policy, and retain terminal failures for recovery. Postmaster coordinates that delivery with typed job definitions, leased processing, and a dead-letter queue, all backed by IndexedDB.
|
|
14336
|
+
|
|
14337
|
+
```ts
|
|
14338
|
+
// Before
|
|
14339
|
+
async function createTodo(payload: { id: string; title: string }) {
|
|
14340
|
+
// Lost on reload. No retry. No recovery. Silent failure.
|
|
14341
|
+
await fetch('/api/todos', { method: 'POST', body: JSON.stringify(payload) });
|
|
14342
|
+
}
|
|
14343
|
+
|
|
14344
|
+
// After
|
|
14345
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
14346
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
14347
|
+
import { s } from '@vielzeug/spell';
|
|
14348
|
+
|
|
14349
|
+
const jobs = defineJobs({
|
|
14350
|
+
createTodo: {
|
|
14351
|
+
version: 1,
|
|
14352
|
+
validate: s.object({ id: s.string(), title: s.string() }),
|
|
14353
|
+
key: (p) => p.id,
|
|
14354
|
+
execute: async (payload, { key, signal }) => {
|
|
14355
|
+
await fetch('/api/todos', {
|
|
14356
|
+
method: 'POST',
|
|
14357
|
+
body: JSON.stringify(payload),
|
|
14358
|
+
headers: { 'Idempotency-Key': key },
|
|
14359
|
+
signal,
|
|
14360
|
+
});
|
|
14361
|
+
},
|
|
14362
|
+
},
|
|
14363
|
+
});
|
|
14364
|
+
|
|
14365
|
+
const store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });
|
|
14366
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
14367
|
+
|
|
14368
|
+
await postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });
|
|
14369
|
+
await postmaster.start();
|
|
14370
|
+
```
|
|
14371
|
+
|
|
14372
|
+
| Feature | Postmaster | Ad hoc outbox | Familiar |
|
|
14373
|
+
| --- | --- | --- | --- |
|
|
14374
|
+
| Bundle size | | Application-defined | |
|
|
14375
|
+
| Zero dependencies | | | |
|
|
14376
|
+
| Survives page reload | | | |
|
|
14377
|
+
| Leased cross-tab processing | | | |
|
|
14378
|
+
| Dead-letter recovery | | | |
|
|
14379
|
+
| Typed job payloads | | | |
|
|
14380
|
+
|
|
14381
|
+
**Use Postmaster when** application jobs must survive reloads, retry explicitly, and remain recoverable after terminal failure.
|
|
14382
|
+
|
|
14383
|
+
**Consider Familiar when** jobs are CPU-bound, in-memory only, and never need to survive a page reload.
|
|
14384
|
+
|
|
14385
|
+
## Installation
|
|
14386
|
+
|
|
14387
|
+
```sh [pnpm]
|
|
14388
|
+
pnpm add @vielzeug/postmaster
|
|
14389
|
+
```
|
|
14390
|
+
|
|
14391
|
+
```sh [npm]
|
|
14392
|
+
npm install @vielzeug/postmaster
|
|
14393
|
+
```
|
|
14394
|
+
|
|
14395
|
+
```sh [yarn]
|
|
14396
|
+
yarn add @vielzeug/postmaster
|
|
14397
|
+
```
|
|
14398
|
+
|
|
14399
|
+
For browser persistence, also install `@vielzeug/vault` (a workspace peer of the IndexedDB adapter):
|
|
14400
|
+
|
|
14401
|
+
```sh [pnpm]
|
|
14402
|
+
pnpm add @vielzeug/postmaster @vielzeug/vault
|
|
14403
|
+
```
|
|
14404
|
+
|
|
14405
|
+
```sh [npm]
|
|
14406
|
+
npm install @vielzeug/postmaster @vielzeug/vault
|
|
14407
|
+
```
|
|
14408
|
+
|
|
14409
|
+
```sh [yarn]
|
|
14410
|
+
yarn add @vielzeug/postmaster @vielzeug/vault
|
|
14411
|
+
```
|
|
14412
|
+
|
|
14413
|
+
## Quick Start
|
|
14414
|
+
|
|
14415
|
+
Define typed jobs, create a durable store, enqueue work, and start the processor. Dispose both the processor and the store when the page lifetime ends.
|
|
14416
|
+
|
|
14417
|
+
```ts
|
|
14418
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
14419
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
14420
|
+
|
|
14421
|
+
const jobs = defineJobs({
|
|
14422
|
+
createTodo: {
|
|
14423
|
+
version: 1,
|
|
14424
|
+
validate: (v: unknown) => v as { id: string; title: string },
|
|
14425
|
+
key: (p) => p.id,
|
|
14426
|
+
execute: async (payload, { key, signal }) => {
|
|
14427
|
+
await fetch('/api/todos', {
|
|
14428
|
+
method: 'POST',
|
|
14429
|
+
body: JSON.stringify(payload),
|
|
14430
|
+
headers: { 'Idempotency-Key': key },
|
|
14431
|
+
signal,
|
|
14432
|
+
});
|
|
14433
|
+
},
|
|
14434
|
+
retry: { maxAttempts: 5, shouldRetry: () => true },
|
|
14435
|
+
},
|
|
14436
|
+
});
|
|
14437
|
+
|
|
14438
|
+
const store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });
|
|
14439
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
14440
|
+
|
|
14441
|
+
await postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });
|
|
14442
|
+
await postmaster.start();
|
|
14443
|
+
|
|
14444
|
+
// On page unload:
|
|
14445
|
+
await postmaster.dispose();
|
|
14446
|
+
await store.dispose();
|
|
14447
|
+
```
|
|
14448
|
+
|
|
14449
|
+
## Features
|
|
14450
|
+
|
|
14451
|
+
- `defineJobs()` — Typed job registry with payload inference and validation.
|
|
14452
|
+
- `createPostmaster()` — Processor with leased claims, heartbeat renewal, and crash recovery.
|
|
14453
|
+
- `enqueue()` — Persist a job and wake the processor.
|
|
14454
|
+
- `flush()` — Process every available job until the queue is empty.
|
|
14455
|
+
- `retry()` / `remove()` — Recover or discard dead-letter jobs.
|
|
14456
|
+
- `tap()` — Typed runtime events for enqueued, started, completed, retry-scheduled, dead-lettered, removed, lease-lost, and processor-error.
|
|
14457
|
+
- `createIndexedDbPostmasterStore()` — Durable browser store backed by Vault IndexedDB.
|
|
14458
|
+
- `createMemoryPostmasterStore()` — Deterministic in-memory store for tests.
|
|
14459
|
+
|
|
14460
|
+
## Documentation
|
|
14461
|
+
|
|
14462
|
+
- [**Usage Guide**](./usage.md)
|
|
14463
|
+
- [**API Reference**](./api.md)
|
|
14464
|
+
- [**Examples**](./examples.md)
|
|
14465
|
+
|
|
14466
|
+
## See Also
|
|
14467
|
+
|
|
14468
|
+
- [@vielzeug/courier](../courier/) — Perform the HTTP requests Postmaster jobs coordinate.
|
|
14469
|
+
- [@vielzeug/vault](../vault/) — IndexedDB storage primitive backing the durable store.
|
|
14470
|
+
- [@vielzeug/sentinel](../sentinel/) — Flush the outbox when the network returns.
|
|
14471
|
+
- [@vielzeug/familiar](../familiar/) — In-memory Web Worker pool for CPU-bound tasks.
|
|
14472
|
+
|
|
14473
|
+
### API Reference
|
|
14474
|
+
|
|
14475
|
+
## API Overview
|
|
14476
|
+
|
|
14477
|
+
| Symbol | Purpose | Execution mode | Common gotcha |
|
|
14478
|
+
| --- | --- | --- | --- |
|
|
14479
|
+
| `defineJobs()` | Typed job registry with validation | Sync | Throws on invalid version, missing fields, or bad retry config |
|
|
14480
|
+
| `createPostmaster()` | Processor with leased claims and retry | Sync | Store is borrowed, not disposed with the processor |
|
|
14481
|
+
| `createIndexedDbPostmasterStore()` | Durable browser store | Sync | Requires `@vielzeug/vault` as a workspace peer |
|
|
14482
|
+
| `createMemoryPostmasterStore()` | Deterministic in-memory store | Sync | Use for tests only |
|
|
14483
|
+
| `PostmasterError` | Base class for package errors | Sync | Catch a subtype when recovery is specific |
|
|
14484
|
+
|
|
14485
|
+
## Package Entry Point
|
|
14486
|
+
|
|
14487
|
+
| Import | Purpose |
|
|
14488
|
+
| --- | --- |
|
|
14489
|
+
| `@vielzeug/postmaster` | Job definitions, processor, store contract, events, errors |
|
|
14490
|
+
| `@vielzeug/postmaster/indexeddb` | Durable browser store backed by Vault IndexedDB |
|
|
14491
|
+
| `@vielzeug/postmaster/testing` | Deterministic in-memory store and test helpers |
|
|
14492
|
+
|
|
14493
|
+
## Factories
|
|
14494
|
+
|
|
14495
|
+
### `defineJobs()`
|
|
14496
|
+
|
|
14497
|
+
```ts
|
|
14498
|
+
function defineJobs(jobs: J): J;
|
|
14499
|
+
```
|
|
14500
|
+
|
|
14501
|
+
Returns the job registry after validating each definition. Rejects invalid versions, missing `execute`/`key`, and retry configurations with non-positive `maxAttempts`.
|
|
14502
|
+
|
|
14503
|
+
| Parameter | Type | Description |
|
|
14504
|
+
| --- | --- | --- |
|
|
14505
|
+
| `jobs` | `J extends JobDefinitions` | Map of job name to definition |
|
|
14506
|
+
|
|
14507
|
+
**Returns:** `J` — the same registry, typed for payload inference.
|
|
14508
|
+
|
|
14509
|
+
**Example**
|
|
14510
|
+
|
|
14511
|
+
```ts
|
|
14512
|
+
import { defineJobs } from '@vielzeug/postmaster';
|
|
14513
|
+
|
|
14514
|
+
const jobs = defineJobs({
|
|
14515
|
+
createTodo: {
|
|
14516
|
+
version: 1,
|
|
14517
|
+
validate: (v) => v as { id: string; title: string },
|
|
14518
|
+
key: (p) => p.id,
|
|
14519
|
+
execute: async (payload, { key, signal }) => {
|
|
14520
|
+
await fetch('/api/todos', {
|
|
14521
|
+
method: 'POST',
|
|
14522
|
+
body: JSON.stringify(payload),
|
|
14523
|
+
headers: { 'Idempotency-Key': key },
|
|
14524
|
+
signal,
|
|
14525
|
+
});
|
|
14526
|
+
},
|
|
14527
|
+
},
|
|
14528
|
+
});
|
|
14529
|
+
```
|
|
14530
|
+
|
|
14531
|
+
---
|
|
14532
|
+
|
|
14533
|
+
### `createPostmaster()`
|
|
14534
|
+
|
|
14535
|
+
```ts
|
|
14536
|
+
function createPostmaster(options: CreatePostmasterOptions): Postmaster;
|
|
14537
|
+
```
|
|
14538
|
+
|
|
14539
|
+
Returns a Postmaster processor that claims, executes, retries, and dead-letters jobs from the borrowed store.
|
|
14540
|
+
|
|
14541
|
+
| Parameter | Type | Description |
|
|
14542
|
+
| --- | --- | --- |
|
|
14543
|
+
| `options.jobs` | `J` | Job registry from `defineJobs()` |
|
|
14544
|
+
| `options.store` | `PostmasterStore` | Borrowed store; not disposed with the processor |
|
|
14545
|
+
| `options.leaseDuration` | `number` | Lease duration in ms (default 30000, minimum 1000) |
|
|
14546
|
+
| `options.clock` | `() => number` | Deterministic clock for tests (default `Date.now`) |
|
|
14547
|
+
| `options.signal` | `AbortSignal` | External signal that disposes the processor |
|
|
14548
|
+
|
|
14549
|
+
**Returns:** `Postmaster`.
|
|
14550
|
+
|
|
14551
|
+
**Example**
|
|
14552
|
+
|
|
14553
|
+
```ts
|
|
14554
|
+
import { createPostmaster } from '@vielzeug/postmaster';
|
|
14555
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
14556
|
+
|
|
14557
|
+
const store = createIndexedDbPostmasterStore({ name: 'outbox' });
|
|
14558
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
14559
|
+
|
|
14560
|
+
await postmaster.start();
|
|
14561
|
+
await postmaster.dispose();
|
|
14562
|
+
await store.dispose();
|
|
14563
|
+
```
|
|
14564
|
+
|
|
14565
|
+
---
|
|
14566
|
+
|
|
14567
|
+
### `createIndexedDbPostmasterStore()`
|
|
14568
|
+
|
|
14569
|
+
```ts
|
|
14570
|
+
function createIndexedDbPostmasterStore(options: { name: string }): PostmasterStore;
|
|
14571
|
+
```
|
|
14572
|
+
|
|
14573
|
+
Returns a durable Postmaster store backed by Vault IndexedDB. Uses one internal table indexed by `status`, `availableAt`, and `leaseExpiresAt`. All operations run inside Vault transactions.
|
|
14574
|
+
|
|
14575
|
+
| Parameter | Type | Description |
|
|
14576
|
+
| --- | --- | --- |
|
|
14577
|
+
| `options.name` | `string` | IndexedDB database name |
|
|
14578
|
+
|
|
14579
|
+
**Returns:** `PostmasterStore`.
|
|
14580
|
+
|
|
14581
|
+
**Example**
|
|
14582
|
+
|
|
14583
|
+
```ts
|
|
14584
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
14585
|
+
|
|
14586
|
+
const store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });
|
|
14587
|
+
await store.dispose();
|
|
14588
|
+
```
|
|
14589
|
+
|
|
14590
|
+
---
|
|
14591
|
+
|
|
14592
|
+
### `createMemoryPostmasterStore()`
|
|
14593
|
+
|
|
14594
|
+
```ts
|
|
14595
|
+
function createMemoryPostmasterStore(entries?: readonly StoredJob[]): PostmasterStore;
|
|
14596
|
+
```
|
|
14597
|
+
|
|
14598
|
+
Returns a deterministic in-memory store for tests. Serializes all operations through a promise chain.
|
|
14599
|
+
|
|
14600
|
+
| Parameter | Type | Description |
|
|
14601
|
+
| --- | --- | --- |
|
|
14602
|
+
| `entries` | `readonly StoredJob[]` | Initial records (default empty) |
|
|
14603
|
+
|
|
14604
|
+
**Returns:** `PostmasterStore`.
|
|
14605
|
+
|
|
14606
|
+
**Example**
|
|
14607
|
+
|
|
14608
|
+
```ts
|
|
14609
|
+
import { createMemoryPostmasterStore } from '@vielzeug/postmaster/testing';
|
|
14610
|
+
|
|
14611
|
+
const store = createMemoryPostmasterStore();
|
|
14612
|
+
await store.dispose();
|
|
14613
|
+
```
|
|
14614
|
+
|
|
14615
|
+
## Postmaster Methods
|
|
14616
|
+
|
|
14617
|
+
### `enqueue()`
|
|
14618
|
+
|
|
14619
|
+
```ts
|
|
14620
|
+
enqueue(name: K, payload: InferJobPayload): Promise;
|
|
14621
|
+
```
|
|
14622
|
+
|
|
14623
|
+
Validates the payload (if `validate` is defined), derives the key, persists the job, and wakes the processor. Throws `PostmasterError` for an empty key or non-JSON-serializable payload.
|
|
14624
|
+
|
|
14625
|
+
---
|
|
14626
|
+
|
|
14627
|
+
### `start()`
|
|
14628
|
+
|
|
14629
|
+
```ts
|
|
14630
|
+
start(): Promise;
|
|
14631
|
+
```
|
|
14632
|
+
|
|
14633
|
+
Begins background processing. Idempotent.
|
|
14634
|
+
|
|
14635
|
+
---
|
|
14636
|
+
|
|
14637
|
+
### `flush()`
|
|
14638
|
+
|
|
14639
|
+
```ts
|
|
14640
|
+
flush(options?: { signal?: AbortSignal }): Promise;
|
|
14641
|
+
```
|
|
14642
|
+
|
|
14643
|
+
Processes every available job until the queue is empty or the signal aborts. Concurrent `flush()` calls join the same drain. Returns counts of processed, completed, dead-lettered, and retry-scheduled jobs.
|
|
14644
|
+
|
|
14645
|
+
---
|
|
14646
|
+
|
|
14647
|
+
### `list()`
|
|
14648
|
+
|
|
14649
|
+
```ts
|
|
14650
|
+
list(filter?: EntryFilter): Promise;
|
|
14651
|
+
```
|
|
14652
|
+
|
|
14653
|
+
Returns entries ordered by `createdAt`. Filter by `status` optionally.
|
|
14654
|
+
|
|
14655
|
+
---
|
|
14656
|
+
|
|
14657
|
+
### `stats()`
|
|
14658
|
+
|
|
14659
|
+
```ts
|
|
14660
|
+
stats(): Promise;
|
|
14661
|
+
```
|
|
14662
|
+
|
|
14663
|
+
Returns counts of queued, running, and dead-letter jobs.
|
|
14664
|
+
|
|
14665
|
+
---
|
|
14666
|
+
|
|
14667
|
+
### `retry()`
|
|
14668
|
+
|
|
14669
|
+
```ts
|
|
14670
|
+
retry(id: string): Promise;
|
|
14671
|
+
```
|
|
14672
|
+
|
|
14673
|
+
Moves a dead-letter job back to queued. Returns a discriminated result: `retried`, `not-found`, `not-dead-letter`, or `running`.
|
|
14674
|
+
|
|
14675
|
+
---
|
|
14676
|
+
|
|
14677
|
+
### `remove()`
|
|
14678
|
+
|
|
14679
|
+
```ts
|
|
14680
|
+
remove(id: string): Promise;
|
|
14681
|
+
```
|
|
14682
|
+
|
|
14683
|
+
Deletes a queued or dead-letter job. Returns a discriminated result: `removed`, `not-found`, or `running`.
|
|
14684
|
+
|
|
14685
|
+
---
|
|
14686
|
+
|
|
14687
|
+
### `tap()`
|
|
14688
|
+
|
|
14689
|
+
```ts
|
|
14690
|
+
tap(handler: (event: PostmasterEvent) => void, options?: { signal?: AbortSignal }): () => void;
|
|
14691
|
+
```
|
|
14692
|
+
|
|
14693
|
+
Observe runtime events (enqueued, started, completed, retry-scheduled, dead-lettered, removed, lease-lost, processor-error, dispose). Handler errors are swallowed — observability never affects processing. Returns an unsubscribe function. Pass `{ signal }` to auto-detach on abort.
|
|
14694
|
+
|
|
14695
|
+
---
|
|
14696
|
+
|
|
14697
|
+
### `dispose()`
|
|
14698
|
+
|
|
14699
|
+
```ts
|
|
14700
|
+
dispose(): Promise;
|
|
14701
|
+
[Symbol.asyncDispose](): Promise;
|
|
14702
|
+
```
|
|
14703
|
+
|
|
14704
|
+
Aborts owned work, releases all active leases, and tears down subscriptions. Idempotent. Does not dispose the borrowed store.
|
|
14705
|
+
|
|
14706
|
+
## Types
|
|
14707
|
+
|
|
14708
|
+
### `JobDefinition`
|
|
14709
|
+
|
|
14710
|
+
```ts
|
|
14711
|
+
interface JobDefinition {
|
|
14712
|
+
readonly version: number;
|
|
14713
|
+
readonly validate?: Validate;
|
|
14714
|
+
readonly key: (payload: T) => string;
|
|
14715
|
+
readonly execute: (payload: T, context: JobContext) => Promise;
|
|
14716
|
+
readonly retry?: RetryPolicy;
|
|
14717
|
+
readonly migrate?: (payload: unknown, fromVersion: number) => unknown;
|
|
14718
|
+
}
|
|
14719
|
+
```
|
|
14720
|
+
|
|
14721
|
+
`validate` is optional. Accepts a function `(value: unknown) => T` or any structural parser with `parse(value: unknown): T` (Spell schemas, Zod schemas, etc). Called once at enqueue. If omitted, payload trusted as-is.
|
|
14722
|
+
|
|
14723
|
+
---
|
|
14724
|
+
|
|
14725
|
+
### `Validate`
|
|
14726
|
+
|
|
14727
|
+
```ts
|
|
14728
|
+
type Validate = ((value: unknown) => T) | { parse(value: unknown): T };
|
|
14729
|
+
```
|
|
14730
|
+
|
|
14731
|
+
Accepts either a plain validation function or any object with a `parse(value: unknown): T` method. Spell's `Schema` and `s.object(...)` satisfy this contract directly — no adapter needed.
|
|
14732
|
+
|
|
14733
|
+
---
|
|
14734
|
+
|
|
14735
|
+
### `JobContext`
|
|
14736
|
+
|
|
14737
|
+
```ts
|
|
14738
|
+
interface JobContext {
|
|
14739
|
+
readonly attempt: number;
|
|
14740
|
+
readonly entryId: string;
|
|
14741
|
+
readonly key: string;
|
|
14742
|
+
readonly signal: AbortSignal;
|
|
14743
|
+
}
|
|
14744
|
+
```
|
|
14745
|
+
|
|
14746
|
+
---
|
|
14747
|
+
|
|
14748
|
+
### `RetryPolicy`
|
|
14749
|
+
|
|
14750
|
+
```ts
|
|
14751
|
+
interface RetryPolicy {
|
|
14752
|
+
readonly maxAttempts: number;
|
|
14753
|
+
readonly shouldRetry: (error: unknown, attempt: number) => boolean;
|
|
14754
|
+
readonly delay?: (attempt: number) => number;
|
|
14755
|
+
}
|
|
14756
|
+
```
|
|
14757
|
+
|
|
14758
|
+
`maxAttempts` is total executions including the first. `shouldRetry` is required when retries are enabled. Default delay uses Arsenal's `backoff(attempt)`.
|
|
14759
|
+
|
|
14760
|
+
---
|
|
14761
|
+
|
|
14762
|
+
### `StoredJob`
|
|
14763
|
+
|
|
14764
|
+
```ts
|
|
14765
|
+
interface StoredJob {
|
|
14766
|
+
readonly id: string;
|
|
14767
|
+
readonly name: string;
|
|
14768
|
+
readonly version: number;
|
|
14769
|
+
readonly payload: JsonValue;
|
|
14770
|
+
readonly key: string;
|
|
14771
|
+
readonly status: 'queued' | 'running' | 'dead-letter';
|
|
14772
|
+
readonly attempts: number;
|
|
14773
|
+
readonly createdAt: number;
|
|
14774
|
+
readonly updatedAt: number;
|
|
14775
|
+
readonly availableAt: number;
|
|
14776
|
+
readonly ownerId?: string;
|
|
14777
|
+
readonly leaseExpiresAt?: number;
|
|
14778
|
+
readonly failure?: StoredFailure;
|
|
14779
|
+
}
|
|
14780
|
+
```
|
|
14781
|
+
|
|
14782
|
+
---
|
|
14783
|
+
|
|
14784
|
+
### `StoredFailure`
|
|
14785
|
+
|
|
14786
|
+
```ts
|
|
14787
|
+
interface StoredFailure {
|
|
14788
|
+
readonly name: string;
|
|
14789
|
+
readonly message: string;
|
|
14790
|
+
readonly occurredAt: number;
|
|
14791
|
+
}
|
|
14792
|
+
```
|
|
14793
|
+
|
|
14794
|
+
Only a bounded error name/message/timestamp is persisted. Never persist arbitrary error objects, response bodies, headers, or stacks.
|
|
14795
|
+
|
|
14796
|
+
---
|
|
14797
|
+
|
|
14798
|
+
### `PostmasterEntry`
|
|
14799
|
+
|
|
14800
|
+
```ts
|
|
14801
|
+
type PostmasterEntry = Pick;
|
|
14802
|
+
```
|
|
14803
|
+
|
|
14804
|
+
The public entry view excludes `payload`, `ownerId`, and `leaseExpiresAt`.
|
|
14805
|
+
|
|
14806
|
+
---
|
|
14807
|
+
|
|
14808
|
+
### `PostmasterStore`
|
|
14809
|
+
|
|
14810
|
+
```ts
|
|
14811
|
+
interface PostmasterStore {
|
|
14812
|
+
transact(fn: (tx: StoreTx) => Promise): Promise;
|
|
14813
|
+
list(filter?: EntryFilter): Promise;
|
|
14814
|
+
subscribe(listener: () => void): () => void;
|
|
14815
|
+
dispose(): Promise;
|
|
14816
|
+
readonly disposed: boolean;
|
|
14817
|
+
readonly disposalSignal: AbortSignal;
|
|
14818
|
+
[Symbol.asyncDispose](): Promise;
|
|
14819
|
+
}
|
|
14820
|
+
|
|
14821
|
+
interface StoreTx {
|
|
14822
|
+
get(id: string): Promise;
|
|
14823
|
+
put(entry: StoredJob): Promise;
|
|
14824
|
+
delete(id: string): Promise;
|
|
14825
|
+
findClaimable(now: number): Promise;
|
|
14826
|
+
findNextWake(now: number): Promise;
|
|
14827
|
+
countByStatus(): Promise;
|
|
14828
|
+
}
|
|
14829
|
+
```
|
|
14830
|
+
|
|
14831
|
+
The store exposes transactional primitives. The processor owns all ownership and transition logic — stores implement storage, not the job state machine. `transact` wraps all operations in an atomic transaction. `findClaimable` returns the earliest eligible job (queued with `availableAt <= now`, or running with expired lease). `findNextWake` returns the earliest future wake time across queued and running jobs.
|
|
14832
|
+
|
|
14833
|
+
---
|
|
14834
|
+
|
|
14835
|
+
### `PostmasterEvent`
|
|
14836
|
+
|
|
14837
|
+
```ts
|
|
14838
|
+
type PostmasterEvent =
|
|
14839
|
+
| { readonly type: 'enqueued' | 'started' | 'completed' | 'retry-scheduled' | 'dead-lettered'; readonly entry: PostmasterEntry }
|
|
14840
|
+
| { readonly type: 'removed' | 'lease-lost'; readonly id: string }
|
|
14841
|
+
| { readonly type: 'processor-error'; readonly error: Error }
|
|
14842
|
+
| { readonly type: 'dispose' };
|
|
14843
|
+
```
|
|
14844
|
+
|
|
14845
|
+
---
|
|
14846
|
+
|
|
14847
|
+
### `FlushResult`
|
|
14848
|
+
|
|
14849
|
+
```ts
|
|
14850
|
+
interface FlushResult {
|
|
14851
|
+
readonly processed: number;
|
|
14852
|
+
readonly completed: number;
|
|
14853
|
+
readonly deadLettered: number;
|
|
14854
|
+
readonly retryScheduled: number;
|
|
14855
|
+
}
|
|
14856
|
+
```
|
|
14857
|
+
|
|
14858
|
+
---
|
|
14859
|
+
|
|
14860
|
+
### `RetryResult` / `RemoveResult`
|
|
14861
|
+
|
|
14862
|
+
```ts
|
|
14863
|
+
type RetryResult =
|
|
14864
|
+
| { readonly status: 'not-found' | 'not-dead-letter' | 'running' }
|
|
14865
|
+
| { readonly status: 'retried'; readonly entry: PostmasterEntry };
|
|
14866
|
+
|
|
14867
|
+
type RemoveResult =
|
|
14868
|
+
| { readonly status: 'not-found' | 'running' }
|
|
14869
|
+
| { readonly status: 'removed'; readonly id: string };
|
|
14870
|
+
```
|
|
14871
|
+
|
|
14872
|
+
## Errors
|
|
14873
|
+
|
|
14874
|
+
### `PostmasterError`
|
|
14875
|
+
|
|
14876
|
+
```ts
|
|
14877
|
+
class PostmasterError extends Error {
|
|
14878
|
+
constructor(message: string, options?: ErrorOptions);
|
|
14879
|
+
}
|
|
14880
|
+
```
|
|
14881
|
+
|
|
14882
|
+
Base class for package-defined errors. Use `instanceof PostmasterError` to narrow to the hierarchy. Covers configuration errors, serialization errors, and store failures.
|
|
14883
|
+
|
|
14884
|
+
---
|
|
14885
|
+
|
|
14886
|
+
### `PostmasterDisposedError`
|
|
14887
|
+
|
|
14888
|
+
```ts
|
|
14889
|
+
class PostmasterDisposedError extends PostmasterError {}
|
|
14890
|
+
```
|
|
14891
|
+
|
|
14892
|
+
Thrown when a public method is called after disposal.
|
|
14893
|
+
|
|
14894
|
+
---
|
|
14895
|
+
|
|
14896
|
+
### `PostmasterJobError`
|
|
14897
|
+
|
|
14898
|
+
```ts
|
|
14899
|
+
class PostmasterJobError extends PostmasterError {}
|
|
14900
|
+
```
|
|
14901
|
+
|
|
14902
|
+
Thrown when a job definition is missing, a version is incompatible, or a migration fails. These errors move the job to dead-letter rather than rejecting the public call.
|
|
14903
|
+
|
|
14904
|
+
### Usage Guide
|
|
14905
|
+
|
|
14906
|
+
## Basic Usage
|
|
14907
|
+
|
|
14908
|
+
Define typed jobs, create a durable store, enqueue work, and start the processor. Dispose both handles when the owner ends.
|
|
14909
|
+
|
|
14910
|
+
```ts
|
|
14911
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
14912
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
14913
|
+
|
|
14914
|
+
const jobs = defineJobs({
|
|
14915
|
+
createTodo: {
|
|
14916
|
+
version: 1,
|
|
14917
|
+
validate: (v: unknown) => v as { id: string; title: string },
|
|
14918
|
+
key: (p) => p.id,
|
|
14919
|
+
execute: async (payload, { key, signal }) => {
|
|
14920
|
+
await fetch('/api/todos', {
|
|
14921
|
+
method: 'POST',
|
|
14922
|
+
body: JSON.stringify(payload),
|
|
14923
|
+
headers: { 'Idempotency-Key': key },
|
|
14924
|
+
signal,
|
|
14925
|
+
});
|
|
14926
|
+
},
|
|
14927
|
+
},
|
|
14928
|
+
});
|
|
14929
|
+
|
|
14930
|
+
const store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });
|
|
14931
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
14932
|
+
|
|
14933
|
+
await postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });
|
|
14934
|
+
await postmaster.start();
|
|
14935
|
+
|
|
14936
|
+
// On page unload:
|
|
14937
|
+
await postmaster.dispose();
|
|
14938
|
+
await store.dispose();
|
|
14939
|
+
```
|
|
14940
|
+
|
|
14941
|
+
The store is borrowed by `createPostmaster()` and is not disposed with the processor. Dispose both explicitly.
|
|
14942
|
+
|
|
14943
|
+
## At-least-once delivery and idempotency
|
|
14944
|
+
|
|
14945
|
+
Postmaster provides **at-least-once delivery**. A crash after the remote write but before local completion can repeat the job. Every job must derive a stable idempotency key, and handlers must send or otherwise enforce that key.
|
|
14946
|
+
|
|
14947
|
+
```ts
|
|
14948
|
+
const jobs = defineJobs({
|
|
14949
|
+
createTodo: {
|
|
14950
|
+
version: 1,
|
|
14951
|
+
validate: (v: unknown) => v as { id: string; title: string },
|
|
14952
|
+
key: (p) => p.id,
|
|
14953
|
+
execute: async (payload, { key, signal }) => {
|
|
14954
|
+
await fetch('/api/todos', {
|
|
14955
|
+
method: 'POST',
|
|
14956
|
+
body: JSON.stringify(payload),
|
|
14957
|
+
headers: { 'Idempotency-Key': key },
|
|
14958
|
+
signal,
|
|
14959
|
+
});
|
|
14960
|
+
},
|
|
14961
|
+
},
|
|
14962
|
+
});
|
|
14963
|
+
```
|
|
14964
|
+
|
|
14965
|
+
Never assume exactly-once execution. Design handlers so a repeated delivery is safe.
|
|
14966
|
+
|
|
14967
|
+
## Postmaster jobs vs Courier mutations
|
|
14968
|
+
|
|
14969
|
+
Courier performs immediate HTTP requests and cache reconciliation. Postmaster coordinates durable delivery. Use Courier inside a Postmaster job when the write must survive reloads.
|
|
14970
|
+
|
|
14971
|
+
```ts
|
|
14972
|
+
import { createCourier, CourierNetworkError } from '@vielzeug/courier';
|
|
14973
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
14974
|
+
|
|
14975
|
+
const courier = createCourier({ baseUrl: 'https://api.example.com' });
|
|
14976
|
+
|
|
14977
|
+
const jobs = defineJobs({
|
|
14978
|
+
createTodo: {
|
|
14979
|
+
version: 1,
|
|
14980
|
+
validate: (v: unknown) => v as { id: string; title: string },
|
|
14981
|
+
key: (p) => p.id,
|
|
14982
|
+
execute: async (payload, { key, signal }) => {
|
|
14983
|
+
await courier.mutate({
|
|
14984
|
+
request: () =>
|
|
14985
|
+
courier.post('/todos', {
|
|
14986
|
+
body: payload,
|
|
14987
|
+
headers: { 'Idempotency-Key': key },
|
|
14988
|
+
signal,
|
|
14989
|
+
}),
|
|
14990
|
+
invalidateKeys: [['todos']],
|
|
14991
|
+
});
|
|
14992
|
+
},
|
|
14993
|
+
retry: { maxAttempts: 5, shouldRetry: (error) => error instanceof CourierNetworkError },
|
|
14994
|
+
},
|
|
14995
|
+
});
|
|
14996
|
+
```
|
|
14997
|
+
|
|
14998
|
+
Postmaster does not import Courier. The integration happens in your job definitions.
|
|
14999
|
+
|
|
15000
|
+
## Payload and version migration
|
|
15001
|
+
|
|
15002
|
+
Each job declares a `version` and an optional `validate` function. When a stored job's version is older than the registered version, Postmaster calls `migrate()` before validating. `validate` is called once at enqueue; omit it to accept the payload as-is. `validate` accepts a plain function `(value: unknown) => T` or any structural parser with `parse(value: unknown): T` — Spell schemas work directly:
|
|
15003
|
+
|
|
15004
|
+
```ts
|
|
15005
|
+
import { s } from '@vielzeug/spell';
|
|
15006
|
+
|
|
15007
|
+
const jobs = defineJobs({
|
|
15008
|
+
createTodo: {
|
|
15009
|
+
version: 2,
|
|
15010
|
+
validate: s.object({ id: s.string(), title: s.string(), priority: s.number().optional() }),
|
|
15011
|
+
key: (p) => p.id,
|
|
15012
|
+
migrate: (payload, fromVersion) => {
|
|
15013
|
+
if (fromVersion === 1) return { ...(payload as { id: string; title: string }), priority: 0 };
|
|
15014
|
+
return payload;
|
|
15015
|
+
},
|
|
15016
|
+
execute: async (payload, { key, signal }) => {
|
|
15017
|
+
await fetch('/api/todos', {
|
|
15018
|
+
method: 'POST',
|
|
15019
|
+
body: JSON.stringify(payload),
|
|
15020
|
+
headers: { 'Idempotency-Key': key },
|
|
15021
|
+
signal,
|
|
15022
|
+
});
|
|
15023
|
+
},
|
|
15024
|
+
},
|
|
15025
|
+
});
|
|
15026
|
+
```
|
|
15027
|
+
|
|
15028
|
+
Unknown job names, incompatible versions, failed migrations, and invalid persisted payloads move to dead-letter rather than being executed.
|
|
15029
|
+
|
|
15030
|
+
## Retry semantics
|
|
15031
|
+
|
|
15032
|
+
Retries are opt-in and explicitly classified. No `retry` block means one attempt followed by dead-letter.
|
|
15033
|
+
|
|
15034
|
+
```ts
|
|
15035
|
+
const jobs = defineJobs({
|
|
15036
|
+
syncTodo: {
|
|
15037
|
+
version: 1,
|
|
15038
|
+
validate: (v: unknown) => v as { id: string },
|
|
15039
|
+
key: (p) => p.id,
|
|
15040
|
+
execute: async (payload, { signal }) => {
|
|
15041
|
+
await fetch(`/api/todos/${payload.id}/sync`, { signal });
|
|
15042
|
+
},
|
|
15043
|
+
retry: {
|
|
15044
|
+
maxAttempts: 5,
|
|
15045
|
+
shouldRetry: (error) => error instanceof TypeError, // network errors only
|
|
15046
|
+
},
|
|
15047
|
+
},
|
|
15048
|
+
});
|
|
15049
|
+
```
|
|
15050
|
+
|
|
15051
|
+
- `maxAttempts` means total executions, including the first.
|
|
15052
|
+
- `shouldRetry` is required when retries are enabled. Postmaster never guesses whether a write is safe to repeat.
|
|
15053
|
+
- Default delay uses Arsenal's deterministic `backoff(attempt)` helper. Override with `delay`.
|
|
15054
|
+
- Delay must be finite and non-negative.
|
|
15055
|
+
- Lifecycle aborts caused by disposal are not classified as job failures.
|
|
15056
|
+
|
|
15057
|
+
## Dead-letter recovery
|
|
15058
|
+
|
|
15059
|
+
Jobs that exhaust retries or hit a terminal failure move to dead-letter. Inspect, retry, or remove them.
|
|
15060
|
+
|
|
15061
|
+
```ts
|
|
15062
|
+
const deadLettered = await postmaster.list({ status: 'dead-letter' });
|
|
15063
|
+
|
|
15064
|
+
for (const entry of deadLettered) {
|
|
15065
|
+
console.log(entry.id, entry.name, entry.failure);
|
|
15066
|
+
}
|
|
15067
|
+
|
|
15068
|
+
// Retry a dead-letter job back into the queue.
|
|
15069
|
+
await postmaster.retry(entry.id);
|
|
15070
|
+
|
|
15071
|
+
// Or remove it permanently.
|
|
15072
|
+
await postmaster.remove(entry.id);
|
|
15073
|
+
```
|
|
15074
|
+
|
|
15075
|
+
`retry()` and `remove()` return discriminated results so callers can distinguish `not-found`, `not-dead-letter`, `running`, and successful outcomes without exceptions.
|
|
15076
|
+
|
|
15077
|
+
## Lifecycle and disposal
|
|
15078
|
+
|
|
15079
|
+
`start()` begins background processing. `dispose()` stops claiming new work, aborts owned work, and is idempotent. `flush()` processes every available job synchronously.
|
|
15080
|
+
|
|
15081
|
+
```ts
|
|
15082
|
+
await postmaster.start();
|
|
15083
|
+
// ...on unload
|
|
15084
|
+
await postmaster.dispose();
|
|
15085
|
+
await store.dispose();
|
|
15086
|
+
```
|
|
15087
|
+
|
|
15088
|
+
Disposal aborts owned work, releases the active lease, and is idempotent. A controlled disposal abort does not consume the attempt — the job returns to queued.
|
|
15089
|
+
|
|
15090
|
+
## Events
|
|
15091
|
+
|
|
15092
|
+
Tap runtime events for observability. Handler errors are swallowed — observability never affects processing.
|
|
15093
|
+
|
|
15094
|
+
```ts
|
|
15095
|
+
const unsubscribe = postmaster.tap((event) => {
|
|
15096
|
+
switch (event.type) {
|
|
15097
|
+
case 'enqueued':
|
|
15098
|
+
console.log('enqueued', event.entry.id);
|
|
15099
|
+
break;
|
|
15100
|
+
case 'completed':
|
|
15101
|
+
console.log('completed', event.entry.id);
|
|
15102
|
+
break;
|
|
15103
|
+
case 'dead-lettered':
|
|
15104
|
+
console.error('dead-lettered', event.entry.id, event.entry.failure);
|
|
15105
|
+
break;
|
|
15106
|
+
case 'processor-error':
|
|
15107
|
+
console.error('processor error', event.error);
|
|
15108
|
+
break;
|
|
15109
|
+
}
|
|
15110
|
+
});
|
|
15111
|
+
```
|
|
15112
|
+
|
|
15113
|
+
Pass an `AbortSignal` to auto-detach:
|
|
15114
|
+
|
|
15115
|
+
```ts
|
|
15116
|
+
const controller = new AbortController();
|
|
15117
|
+
postmaster.tap(handler, { signal: controller.signal });
|
|
15118
|
+
controller.abort(); // stops tapping
|
|
15119
|
+
```
|
|
15120
|
+
|
|
15121
|
+
## Testing
|
|
15122
|
+
|
|
15123
|
+
Use the in-memory store for deterministic tests.
|
|
15124
|
+
|
|
15125
|
+
```ts
|
|
15126
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
15127
|
+
import { createMemoryPostmasterStore } from '@vielzeug/postmaster/testing';
|
|
15128
|
+
|
|
15129
|
+
const store = createMemoryPostmasterStore();
|
|
15130
|
+
const postmaster = createPostmaster({
|
|
15131
|
+
jobs: defineJobs({
|
|
15132
|
+
send: {
|
|
15133
|
+
version: 1,
|
|
15134
|
+
validate: (v: unknown) => String(v),
|
|
15135
|
+
key: (p) => p,
|
|
15136
|
+
execute: async () => {},
|
|
15137
|
+
},
|
|
15138
|
+
}),
|
|
15139
|
+
store,
|
|
15140
|
+
});
|
|
15141
|
+
|
|
15142
|
+
await postmaster.enqueue('send', 'hello');
|
|
15143
|
+
await postmaster.flush();
|
|
15144
|
+
await postmaster.dispose();
|
|
15145
|
+
```
|
|
15146
|
+
|
|
15147
|
+
Inject a deterministic clock to control retry scheduling.
|
|
15148
|
+
|
|
15149
|
+
```ts
|
|
15150
|
+
let now = 0;
|
|
15151
|
+
const postmaster = createPostmaster({ clock: () => now, jobs, store });
|
|
15152
|
+
```
|
|
15153
|
+
|
|
15154
|
+
## Framework Integration
|
|
15155
|
+
|
|
15156
|
+
Create the Postmaster after the component mounts, start processing, and dispose on unmount.
|
|
15157
|
+
|
|
15158
|
+
```tsx [React]
|
|
15159
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
15160
|
+
import { createPostmaster, defineJobs, type Postmaster } from '@vielzeug/postmaster';
|
|
15161
|
+
import { useEffect } from 'react';
|
|
15162
|
+
|
|
15163
|
+
const jobs = defineJobs({
|
|
15164
|
+
sync: {
|
|
15165
|
+
version: 1,
|
|
15166
|
+
validate: (v: unknown) => v as { id: string },
|
|
15167
|
+
key: (p) => p.id,
|
|
15168
|
+
execute: async (payload, { signal }) => {
|
|
15169
|
+
await fetch(`/api/sync/${payload.id}`, { signal });
|
|
15170
|
+
},
|
|
15171
|
+
},
|
|
15172
|
+
});
|
|
15173
|
+
|
|
15174
|
+
export function OutboxProvider() {
|
|
15175
|
+
useEffect(() => {
|
|
15176
|
+
const store = createIndexedDbPostmasterStore({ name: 'outbox' });
|
|
15177
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
15178
|
+
void postmaster.start();
|
|
15179
|
+
|
|
15180
|
+
return () => {
|
|
15181
|
+
void postmaster.dispose();
|
|
15182
|
+
void store.dispose();
|
|
15183
|
+
};
|
|
15184
|
+
}, []);
|
|
15185
|
+
|
|
15186
|
+
return null;
|
|
15187
|
+
}
|
|
15188
|
+
```
|
|
15189
|
+
|
|
15190
|
+
```vue [Vue 3]
|
|
15191
|
+
|
|
15192
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
15193
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
15194
|
+
import { onMounted, onUnmounted } from 'vue';
|
|
15195
|
+
|
|
15196
|
+
const jobs = defineJobs({
|
|
15197
|
+
sync: {
|
|
15198
|
+
version: 1,
|
|
15199
|
+
validate: (v: unknown) => v as { id: string },
|
|
15200
|
+
key: (p) => p.id,
|
|
15201
|
+
execute: async (payload, { signal }) => {
|
|
15202
|
+
await fetch(`/api/sync/${payload.id}`, { signal });
|
|
15203
|
+
},
|
|
15204
|
+
},
|
|
15205
|
+
});
|
|
15206
|
+
|
|
15207
|
+
let postmaster: ReturnType | undefined;
|
|
15208
|
+
let store: ReturnType | undefined;
|
|
15209
|
+
|
|
15210
|
+
onMounted(() => {
|
|
15211
|
+
store = createIndexedDbPostmasterStore({ name: 'outbox' });
|
|
15212
|
+
postmaster = createPostmaster({ jobs, store });
|
|
15213
|
+
void postmaster.start();
|
|
15214
|
+
});
|
|
15215
|
+
|
|
15216
|
+
onUnmounted(() => {
|
|
15217
|
+
void postmaster?.dispose();
|
|
15218
|
+
void store?.dispose();
|
|
15219
|
+
});
|
|
14196
15220
|
|
|
14197
15221
|
|
|
14198
15222
|
|
|
@@ -14200,92 +15224,115 @@ const open = ref(false);
|
|
|
14200
15224
|
|
|
14201
15225
|
```svelte [Svelte]
|
|
14202
15226
|
|
|
14203
|
-
import
|
|
15227
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
15228
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
15229
|
+
import { onMount } from 'svelte';
|
|
14204
15230
|
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
15231
|
+
const jobs = defineJobs({
|
|
15232
|
+
sync: {
|
|
15233
|
+
version: 1,
|
|
15234
|
+
validate: (v: unknown) => v as { id: string },
|
|
15235
|
+
key: (p) => p.id,
|
|
15236
|
+
execute: async (payload, { signal }) => {
|
|
15237
|
+
await fetch(`/api/sync/${payload.id}`, { signal });
|
|
15238
|
+
},
|
|
15239
|
+
},
|
|
15240
|
+
});
|
|
15241
|
+
|
|
15242
|
+
onMount(() => {
|
|
15243
|
+
const store = createIndexedDbPostmasterStore({ name: 'outbox' });
|
|
15244
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
15245
|
+
void postmaster.start();
|
|
15246
|
+
|
|
15247
|
+
return () => {
|
|
15248
|
+
void postmaster.dispose();
|
|
15249
|
+
void store.dispose();
|
|
15250
|
+
};
|
|
15251
|
+
});
|
|
14208
15252
|
|
|
14209
15253
|
```
|
|
14210
15254
|
|
|
14211
15255
|
## Working with Other Vielzeug Libraries
|
|
14212
15256
|
|
|
14213
|
-
###
|
|
15257
|
+
### Postmaster + Courier
|
|
14214
15258
|
|
|
14215
|
-
|
|
15259
|
+
Use Courier inside job handlers for HTTP transport and cache invalidation. Postmaster coordinates delivery; Courier performs the request.
|
|
14216
15260
|
|
|
14217
15261
|
```ts
|
|
14218
|
-
import {
|
|
14219
|
-
import {
|
|
15262
|
+
import { createCourier, CourierNetworkError } from '@vielzeug/courier';
|
|
15263
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
14220
15264
|
|
|
14221
|
-
|
|
14222
|
-
const theme = signal('light');
|
|
14223
|
-
const isDark = computed(() => theme.value === 'dark');
|
|
15265
|
+
const courier = createCourier({ baseUrl: 'https://api.example.com' });
|
|
14224
15266
|
|
|
14225
|
-
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
|
|
15267
|
+
const jobs = defineJobs({
|
|
15268
|
+
createTodo: {
|
|
15269
|
+
version: 1,
|
|
15270
|
+
validate: (v: unknown) => v as { id: string; title: string },
|
|
15271
|
+
key: (p) => p.id,
|
|
15272
|
+
execute: async (payload, { key, signal }) => {
|
|
15273
|
+
await courier.mutate({
|
|
15274
|
+
request: () =>
|
|
15275
|
+
courier.post('/todos', {
|
|
15276
|
+
body: payload,
|
|
15277
|
+
headers: { 'Idempotency-Key': key },
|
|
15278
|
+
signal,
|
|
15279
|
+
}),
|
|
15280
|
+
invalidateKeys: [['todos']],
|
|
15281
|
+
});
|
|
15282
|
+
},
|
|
15283
|
+
retry: { maxAttempts: 5, shouldRetry: (e) => e instanceof CourierNetworkError },
|
|
14233
15284
|
},
|
|
14234
15285
|
});
|
|
14235
15286
|
```
|
|
14236
15287
|
|
|
14237
|
-
###
|
|
15288
|
+
### Postmaster + Sentinel
|
|
14238
15289
|
|
|
14239
|
-
|
|
14240
|
-
custom element to native `ElementInternals` without imposing submission, validation, or dirty-state policy.
|
|
15290
|
+
Flush the outbox when the network returns. Sentinel reports online state; Postmaster does the rest.
|
|
14241
15291
|
|
|
14242
15292
|
```ts
|
|
14243
|
-
import {
|
|
14244
|
-
import {
|
|
15293
|
+
import { createNetwork } from '@vielzeug/sentinel';
|
|
15294
|
+
import { createPostmaster } from '@vielzeug/postmaster';
|
|
14245
15295
|
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
const form = createForm({ initialValues: { email: '' } });
|
|
15296
|
+
const network = createNetwork();
|
|
15297
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
14249
15298
|
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
event.preventDefault();
|
|
14253
|
-
void form.submit(async (values) => {
|
|
14254
|
-
console.log(values);
|
|
14255
|
-
});
|
|
14256
|
-
}}>
|
|
14257
|
-
|
|
14258
|
-
|
|
14259
|
-
`;
|
|
14260
|
-
},
|
|
15299
|
+
const unsubscribe = network.subscribe(() => {
|
|
15300
|
+
if (network.value.online) void postmaster.flush();
|
|
14261
15301
|
});
|
|
15302
|
+
|
|
15303
|
+
// On teardown:
|
|
15304
|
+
unsubscribe();
|
|
15305
|
+
network.dispose();
|
|
15306
|
+
await postmaster.dispose();
|
|
14262
15307
|
```
|
|
14263
15308
|
|
|
15309
|
+
### Postmaster + Vault
|
|
15310
|
+
|
|
15311
|
+
The IndexedDB adapter is built on Vault. Use Vault directly for unrelated storage; the Postmaster store owns its own database name.
|
|
15312
|
+
|
|
14264
15313
|
## Best Practices
|
|
14265
15314
|
|
|
14266
|
-
-
|
|
14267
|
-
-
|
|
14268
|
-
-
|
|
14269
|
-
-
|
|
14270
|
-
-
|
|
14271
|
-
-
|
|
14272
|
-
-
|
|
14273
|
-
-
|
|
14274
|
-
- Test component mounting and lifecycle with `@vielzeug/ore/testing`; import generic DOM events, queries, and waits
|
|
14275
|
-
from `@vielzeug/assay`.
|
|
15315
|
+
- **Derive** a stable idempotency key from every job payload and send it with the remote write.
|
|
15316
|
+
- **Dispose** both the processor and the store explicitly; the processor does not own the store.
|
|
15317
|
+
- **Classify** retryable errors explicitly with `shouldRetry`; never let Postmaster guess.
|
|
15318
|
+
- **Migrate** persisted payloads when job versions change; test migrations against stored fixtures.
|
|
15319
|
+
- **Inspect** the dead-letter queue regularly and retry or remove terminal failures.
|
|
15320
|
+
- **Avoid** persisting sensitive data in payloads or failure messages; IndexedDB is per-origin but not encrypted.
|
|
15321
|
+
- **Flush** the outbox when Sentinel reports the network returns.
|
|
15322
|
+
- **Test** with the in-memory store and a deterministic clock for reproducible retry timing.
|
|
14276
15323
|
|
|
14277
15324
|
### Examples
|
|
14278
15325
|
|
|
14279
15326
|
## Examples
|
|
14280
15327
|
|
|
14281
|
-
- [
|
|
14282
|
-
- [
|
|
14283
|
-
- [
|
|
14284
|
-
- [
|
|
14285
|
-
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
-
|
|
15328
|
+
- [Queue Offline Courier Mutations](./examples/queue-offline-courier-mutations.md)
|
|
15329
|
+
- [Resume When Network Returns](./examples/resume-when-network-returns.md)
|
|
15330
|
+
- [Recover Dead-Letter Jobs](./examples/recover-dead-letter-jobs.md)
|
|
15331
|
+
- [Service Worker Background Sync](./examples/service-worker-background-sync.md)
|
|
15332
|
+
|
|
15333
|
+
### REPL Examples
|
|
15334
|
+
|
|
15335
|
+
- defineJobs - Basic Outbox (id: `define-jobs`)
|
|
14289
15336
|
|
|
14290
15337
|
---
|
|
14291
15338
|
|
|
@@ -17067,7 +18114,10 @@ type Schema = {
|
|
|
17067
18114
|
|
|
17068
18115
|
const pulse = createPulse('wss://api.example.com/ws', {
|
|
17069
18116
|
reconnect: true,
|
|
17070
|
-
|
|
18117
|
+
});
|
|
18118
|
+
pulse.tap((event) => {
|
|
18119
|
+
if (event.type === 'error') console.error(event.error);
|
|
18120
|
+
if (event.type === 'status-change') console.log('status:', event.status);
|
|
17071
18121
|
});
|
|
17072
18122
|
const chat = pulse.channel('chat');
|
|
17073
18123
|
const lobby = pulse.room('lobby');
|
|
@@ -17091,7 +18141,7 @@ pulse.dispose();
|
|
|
17091
18141
|
- **`room()`** — named, schema-bound ref-counted room scopes with optional reactive presence. The first scope sends `join`; the last disposal sends `leave`.
|
|
17092
18142
|
- **`reconnect`** — ordered restoration of channel subscriptions, room memberships, and local presence state.
|
|
17093
18143
|
- **`transform`** — one synchronous transform or filter for application messages.
|
|
17094
|
-
- **`
|
|
18144
|
+
- **`tap()`** — subscribe to lifecycle events (status changes, errors, disposal) via a typed `PulseEvent` stream.
|
|
17095
18145
|
- **`heartbeat`** — ping/pong liveness detection that uses the same reconnect controller.
|
|
17096
18146
|
- **`status` and `rooms`** — ripple readables for transport and confirmed membership state.
|
|
17097
18147
|
|
|
@@ -17120,7 +18170,7 @@ pulse.dispose();
|
|
|
17120
18170
|
| `PulseChannel` | Scoped channel namespace with independent disposal. | Sync methods, async `wait()` | Each call returns a new scope; ref-counted subscription. |
|
|
17121
18171
|
| `RoomScope` | Ref-counted room membership with optional presence. | Sync methods, async `joined` | `joined` rejects on transport close or timeout. |
|
|
17122
18172
|
| `PulseSchema` | Declares server/client events, channels, and rooms. | Type-only | Infer all named scope types from this schema. |
|
|
17123
|
-
| `PulseOptions` | Configuration: heartbeat, reconnect, transform
|
|
18173
|
+
| `PulseOptions` | Configuration: heartbeat, reconnect, transform. | Type-only | `reconnect` and `heartbeat` default to `false`. |
|
|
17124
18174
|
| `PulseError` | Base class for all Pulse errors. | Runtime | Check `instanceof` against subclasses. |
|
|
17125
18175
|
|
|
17126
18176
|
## Package Entry Point
|
|
@@ -17183,7 +18233,6 @@ Declare all protocol surfaces once at construction. Named scopes infer their typ
|
|
|
17183
18233
|
```ts
|
|
17184
18234
|
type PulseOptions = {
|
|
17185
18235
|
heartbeat?: boolean | HeartbeatOptions;
|
|
17186
|
-
onError?: (error: PulseError) => void;
|
|
17187
18236
|
protocols?: string | string[];
|
|
17188
18237
|
reconnect?: boolean | ReconnectOptions;
|
|
17189
18238
|
transform?: OutgoingTransform;
|
|
@@ -17193,7 +18242,6 @@ type PulseOptions = {
|
|
|
17193
18242
|
| Option | Type | Default | Description |
|
|
17194
18243
|
| --- | --- | --- | --- |
|
|
17195
18244
|
| `heartbeat` | `boolean \| HeartbeatOptions` | `false` | Ping/pong keep-alive. |
|
|
17196
|
-
| `onError` | `(error: PulseError) => void` | — | Receives typed transport and protocol errors. |
|
|
17197
18245
|
| `protocols` | `string \| string[]` | — | Sub-protocols passed to the WebSocket constructor. |
|
|
17198
18246
|
| `reconnect` | `boolean \| ReconnectOptions` | `false` | Auto-reconnect on unexpected close. |
|
|
17199
18247
|
| `transform` | `OutgoingTransform` | — | Transform or filter outgoing application messages. |
|
|
@@ -17283,6 +18331,9 @@ type Pulse = {
|
|
|
17283
18331
|
// Status
|
|
17284
18332
|
readonly status: Readable;
|
|
17285
18333
|
|
|
18334
|
+
// Tap
|
|
18335
|
+
tap(handler: (event: PulseEvent) => void, options?: { signal?: AbortSignal }): () => void;
|
|
18336
|
+
|
|
17286
18337
|
[Symbol.dispose](): void;
|
|
17287
18338
|
};
|
|
17288
18339
|
```
|
|
@@ -17331,6 +18382,42 @@ Reactive set of rooms the client is currently a confirmed member of.
|
|
|
17331
18382
|
|
|
17332
18383
|
Reactive connection status: `'connecting' | 'open' | 'reconnecting' | 'closed'`.
|
|
17333
18384
|
|
|
18385
|
+
### `tap(handler, options?)`
|
|
18386
|
+
|
|
18387
|
+
Subscribes to lifecycle events emitted by the Pulse instance. The handler receives a discriminated-union `PulseEvent`. Returns an unsubscribe function.
|
|
18388
|
+
|
|
18389
|
+
| Parameter | Type | Description |
|
|
18390
|
+
| --- | --- | --- |
|
|
18391
|
+
| `handler` | `(event: PulseEvent) => void` | Called for each lifecycle event. |
|
|
18392
|
+
| `options.signal` | `AbortSignal` | Optional signal to stop the subscription. |
|
|
18393
|
+
|
|
18394
|
+
```ts
|
|
18395
|
+
const pulse = createPulse(url, { reconnect: true });
|
|
18396
|
+
pulse.tap((event) => {
|
|
18397
|
+
if (event.type === 'error') console.error(event.error);
|
|
18398
|
+
if (event.type === 'status-change') console.log('status:', event.status);
|
|
18399
|
+
});
|
|
18400
|
+
```
|
|
18401
|
+
|
|
18402
|
+
---
|
|
18403
|
+
|
|
18404
|
+
## `PulseEvent`
|
|
18405
|
+
|
|
18406
|
+
```ts
|
|
18407
|
+
type PulseEvent =
|
|
18408
|
+
| { type: 'status-change'; status: PulseStatus }
|
|
18409
|
+
| { type: 'error'; error: PulseError }
|
|
18410
|
+
| { type: 'dispose' };
|
|
18411
|
+
```
|
|
18412
|
+
|
|
18413
|
+
A discriminated union of lifecycle events emitted by a `Pulse` instance. Inspect `event.type` to narrow the payload.
|
|
18414
|
+
|
|
18415
|
+
| `type` | Payload | When |
|
|
18416
|
+
| --- | --- | --- |
|
|
18417
|
+
| `status-change` | `status: PulseStatus` | The connection status transitions. |
|
|
18418
|
+
| `error` | `error: PulseError` | A typed transport or protocol error occurs. |
|
|
18419
|
+
| `dispose` | — | The instance is disposed. |
|
|
18420
|
+
|
|
17334
18421
|
---
|
|
17335
18422
|
|
|
17336
18423
|
## `PulseChannel`
|
|
@@ -17561,7 +18648,11 @@ type Schema = {
|
|
|
17561
18648
|
const pulse = createPulse('wss://api.example.com/ws', {
|
|
17562
18649
|
reconnect: { delay: 1_000, maxAttempts: 5 },
|
|
17563
18650
|
heartbeat: { interval: 30_000, timeout: 5_000 },
|
|
17564
|
-
|
|
18651
|
+
});
|
|
18652
|
+
|
|
18653
|
+
pulse.tap((event) => {
|
|
18654
|
+
if (event.type === 'error') console.error(event.error);
|
|
18655
|
+
if (event.type === 'status-change') console.log('status:', event.status);
|
|
17565
18656
|
});
|
|
17566
18657
|
|
|
17567
18658
|
try {
|
|
@@ -17733,13 +18824,17 @@ Disposal is idempotent. It closes the connection, rejects pending room joins, cl
|
|
|
17733
18824
|
|
|
17734
18825
|
```ts
|
|
17735
18826
|
const pulse = createPulse('wss://api.example.com/ws', {
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
18827
|
+
reconnect: true,
|
|
18828
|
+
});
|
|
18829
|
+
|
|
18830
|
+
pulse.tap((event) => {
|
|
18831
|
+
if (event.type === 'error') {
|
|
18832
|
+
if (event.error instanceof PulseConnectionError) {
|
|
18833
|
+
console.error('Connection error:', event.error);
|
|
18834
|
+
} else if (event.error instanceof PulseProtocolError) {
|
|
18835
|
+
console.error('Protocol error:', event.error);
|
|
17741
18836
|
}
|
|
17742
|
-
}
|
|
18837
|
+
}
|
|
17743
18838
|
});
|
|
17744
18839
|
```
|
|
17745
18840
|
|
|
@@ -17758,7 +18853,7 @@ const pulse = createPulse('wss://api.example.com/ws', {
|
|
|
17758
18853
|
- Define the full schema at `createPulse()` so named scopes are type-safe without per-call generics.
|
|
17759
18854
|
- Use `using` declarations for channel and room scopes so disposal is automatic at block exit.
|
|
17760
18855
|
- Always call `dispose()` when done — it closes the connection, rejects pending joins, and clears listeners.
|
|
17761
|
-
-
|
|
18856
|
+
- Call `tap()` to observe lifecycle events; Pulse reports transport and protocol errors there rather than throwing asynchronously.
|
|
17762
18857
|
- Read `pulse.rooms` for post-reconnect membership; `joined` rejects on transport close.
|
|
17763
18858
|
- Set a `timeout` on room scopes when the server may never confirm membership.
|
|
17764
18859
|
- Keep `transform` synchronous; resolve async policy decisions before calling `send()`.
|
|
@@ -21258,7 +22353,7 @@ console.log(results[0]?.item.name); // Ada Lovelace
|
|
|
21258
22353
|
- Incremental updates — `add()` / `remove()` / `reindex()` patch individual items in O(field_length)
|
|
21259
22354
|
- `onMutate()` — Subscribe to index mutations; powers `createSearch()`'s reactivity and bulk reconciliation
|
|
21260
22355
|
- `segmentWords()` — Split unsegmented-script text (CJK, Thai, ...) into words via native `Intl.Segmenter`
|
|
21261
|
-
-
|
|
22356
|
+
- Event subscription via `search.tap()` — observe `query`/`isSearching`/`results`/`dispose` transitions; returns an unsubscribe function
|
|
21262
22357
|
|
|
21263
22358
|
## Documentation
|
|
21264
22359
|
|
|
@@ -21294,14 +22389,13 @@ console.log(results[0]?.item.name); // Ada Lovelace
|
|
|
21294
22389
|
| `toSearchMatcher()` | Adapt `ScoutIndex` to Sourcerer's `match` callback | Sync | Recomputes cached query matches after index mutation |
|
|
21295
22390
|
| `toFilterPredicate()` | Snapshot predicate from a one-time query | Sync | Re-call when query or corpus changes |
|
|
21296
22391
|
| `segmentWords()` | Split unsegmented-script text (CJK, Thai, ...) into words | Sync | Uses native `Intl.Segmenter` — not applied inside `tokenize()` itself (see Pitfalls) |
|
|
21297
|
-
| `
|
|
22392
|
+
| `SearchState.tap()` | Subscribe to `query`/`isSearching`/`results`/`dispose` events | Sync | Returns an unsubscribe function; pass `{ signal }` to tie to an external lifecycle |
|
|
21298
22393
|
|
|
21299
22394
|
## Package Entry Point
|
|
21300
22395
|
|
|
21301
22396
|
| Import | Purpose |
|
|
21302
22397
|
| --- | --- |
|
|
21303
|
-
| `@vielzeug/scout` | All exports — index/search/highlighting/adapters, `ScoutConfigurationError`, `ScoutDisposedError`, `ScoutError`, and all types |
|
|
21304
|
-
| `@vielzeug/scout/devtools` | `debugSearch` — reactive search state logger (dev only) |
|
|
22398
|
+
| `@vielzeug/scout` | All exports — index/search/highlighting/adapters, `ScoutConfigurationError`, `ScoutDisposedError`, `ScoutError`, `ScoutEvent`, and all types |
|
|
21305
22399
|
|
|
21306
22400
|
---
|
|
21307
22401
|
|
|
@@ -21454,6 +22548,7 @@ function createSearch(index: ScoutIndex, options?: CreateSearchOptions): SearchS
|
|
|
21454
22548
|
| `disposed` | `boolean` | `true` after `dispose()` has been called. |
|
|
21455
22549
|
| `clear()` | `() => void` | Resets query, cancels debounce, clears results synchronously. |
|
|
21456
22550
|
| `dispose()` | `() => void` | Releases all reactive subscriptions. |
|
|
22551
|
+
| `tap()` | `(handler, options?) => () => void` | Subscribe to `ScoutEvent` transitions; returns an unsubscribe function. |
|
|
21457
22552
|
| `[Symbol.dispose]()` | `() => void` | `using`-compatible disposal. |
|
|
21458
22553
|
|
|
21459
22554
|
**Example**
|
|
@@ -21664,35 +22759,39 @@ const index = createIndex(documents, {
|
|
|
21664
22759
|
|
|
21665
22760
|
---
|
|
21666
22761
|
|
|
21667
|
-
## `
|
|
22762
|
+
## `search.tap(handler, options?)`
|
|
22763
|
+
|
|
22764
|
+
Subscribes `handler` to `ScoutEvent` transitions emitted by a `SearchState` — `query` changes, `isSearching` transitions, `results` changes, and `dispose`. Returns an unsubscribe function; calling it removes the handler. Pass `{ signal }` to tie the subscription to an external `AbortSignal` — when the signal aborts (or `dispose()` is called, which aborts `disposalSignal`) the handler is removed automatically.
|
|
21668
22765
|
|
|
21669
22766
|
```ts
|
|
21670
|
-
|
|
22767
|
+
tap(
|
|
22768
|
+
handler: (event: ScoutEvent) => void,
|
|
22769
|
+
options?: { signal?: AbortSignal },
|
|
22770
|
+
): () => void
|
|
21671
22771
|
```
|
|
21672
22772
|
|
|
21673
|
-
Logs `query` → `isSearching` → `results` transitions of a `SearchState` to `console.debug`. Returns a function that unsubscribes all listeners installed by this call. Import from the dedicated sub-path so it's tree-shaken from production bundles.
|
|
21674
|
-
|
|
21675
|
-
Logs the full, literal search query string — if your queries may carry PII (names, emails, medical/financial terms typed by end users), don't enable this in production.
|
|
21676
|
-
|
|
21677
22773
|
**Example**
|
|
21678
22774
|
|
|
21679
22775
|
```ts
|
|
21680
22776
|
import { createIndex, createSearch } from '@vielzeug/scout';
|
|
21681
|
-
import { debugSearch } from '@vielzeug/scout/devtools';
|
|
21682
22777
|
|
|
21683
22778
|
const index = createIndex([{ name: 'Ada Lovelace' }], { fields: ['name'] });
|
|
21684
22779
|
const search = createSearch(index);
|
|
21685
|
-
|
|
22780
|
+
|
|
22781
|
+
const unsubscribe = search.tap((event) => {
|
|
22782
|
+
if (event.type === 'query-change') console.debug('query:', event.query);
|
|
22783
|
+
if (event.type === 'results-change') console.debug('results:', event.results.length);
|
|
22784
|
+
});
|
|
21686
22785
|
|
|
21687
22786
|
search.query.value = 'alice';
|
|
21688
|
-
//
|
|
21689
|
-
//
|
|
21690
|
-
// [scout:search] isSearching -> false
|
|
21691
|
-
// [scout:search] results -> 1 item(s)
|
|
22787
|
+
// query: alice
|
|
22788
|
+
// results: 1
|
|
21692
22789
|
|
|
21693
|
-
|
|
22790
|
+
unsubscribe();
|
|
21694
22791
|
```
|
|
21695
22792
|
|
|
22793
|
+
If your queries may carry PII (names, emails, medical/financial terms typed by end users), don't log `query-change` events in production.
|
|
22794
|
+
|
|
21696
22795
|
---
|
|
21697
22796
|
|
|
21698
22797
|
## Types
|
|
@@ -21778,12 +22877,32 @@ type SearchState = {
|
|
|
21778
22877
|
readonly disposed: boolean;
|
|
21779
22878
|
clear(): void;
|
|
21780
22879
|
dispose(): void;
|
|
22880
|
+
tap(handler: (event: ScoutEvent) => void, options?: { signal?: AbortSignal }): () => void;
|
|
21781
22881
|
[Symbol.dispose](): void;
|
|
21782
22882
|
};
|
|
21783
22883
|
```
|
|
21784
22884
|
|
|
21785
22885
|
See `createSearch()` above for member descriptions.
|
|
21786
22886
|
|
|
22887
|
+
### `ScoutEvent`
|
|
22888
|
+
|
|
22889
|
+
Discriminated union of events emitted by `SearchState.tap()`. Each variant carries a `type` discriminant; narrow with a `switch` or `if` on `event.type`.
|
|
22890
|
+
|
|
22891
|
+
```ts
|
|
22892
|
+
type ScoutEvent =
|
|
22893
|
+
| { type: 'query-change'; query: string }
|
|
22894
|
+
| { type: 'searching-change'; isSearching: boolean }
|
|
22895
|
+
| { type: 'results-change'; results: readonly SearchResult[] }
|
|
22896
|
+
| { type: 'dispose' };
|
|
22897
|
+
```
|
|
22898
|
+
|
|
22899
|
+
| `type` | Payload | Emitted when |
|
|
22900
|
+
| --- | --- | --- |
|
|
22901
|
+
| `query-change` | `query: string` | The writable `query` signal's value changes. |
|
|
22902
|
+
| `searching-change` | `isSearching: boolean` | The debounce window opens (`true`) or closes (`false`). |
|
|
22903
|
+
| `results-change` | `results: readonly SearchResult[]` | Committed results change after debounce. |
|
|
22904
|
+
| `dispose` | — | `dispose()` is called on the `SearchState`. |
|
|
22905
|
+
|
|
21787
22906
|
### `ReactiveSearch`
|
|
21788
22907
|
|
|
21789
22908
|
```ts
|
|
@@ -22126,25 +23245,29 @@ const parts = highlight(result.item.name, nameMatch?.ranges ?? []);
|
|
|
22126
23245
|
|
|
22127
23246
|
## Debug Logging
|
|
22128
23247
|
|
|
22129
|
-
|
|
22130
|
-
|
|
22131
|
-
`debugSearch()` logs the full, literal search query string — if your queries may carry PII (names, emails, medical/financial terms typed by end users), don't enable this in production.
|
|
23248
|
+
`search.tap()` subscribes a handler to `ScoutEvent` transitions emitted by a `SearchState` — `query` changes, `isSearching` transitions, `results` changes, and `dispose`. It returns an unsubscribe function. Pass `{ signal }` to tie the subscription to an external `AbortSignal` (or to `search.disposalSignal`, which aborts when `dispose()` is called).
|
|
22132
23249
|
|
|
22133
23250
|
```ts
|
|
22134
|
-
import {
|
|
23251
|
+
import { createIndex, createSearch } from '@vielzeug/scout';
|
|
22135
23252
|
|
|
22136
23253
|
const search = createSearch(index, { debounce: 150 });
|
|
22137
|
-
const
|
|
23254
|
+
const unsubscribe = search.tap((event) => {
|
|
23255
|
+
if (event.type === 'query-change') console.debug('query:', event.query);
|
|
23256
|
+
if (event.type === 'searching-change') console.debug('isSearching:', event.isSearching);
|
|
23257
|
+
if (event.type === 'results-change') console.debug('results:', event.results.length);
|
|
23258
|
+
});
|
|
22138
23259
|
|
|
22139
23260
|
search.query.value = 'alice';
|
|
22140
|
-
//
|
|
22141
|
-
//
|
|
22142
|
-
//
|
|
22143
|
-
//
|
|
23261
|
+
// query: alice
|
|
23262
|
+
// isSearching: true
|
|
23263
|
+
// isSearching: false
|
|
23264
|
+
// results: 1
|
|
22144
23265
|
|
|
22145
|
-
|
|
23266
|
+
unsubscribe();
|
|
22146
23267
|
```
|
|
22147
23268
|
|
|
23269
|
+
`query-change` events carry the full, literal search query string — if your queries may carry PII (names, emails, medical/financial terms typed by end users), don't log them in production.
|
|
23270
|
+
|
|
22148
23271
|
## Framework Integration
|
|
22149
23272
|
|
|
22150
23273
|
```tsx [React]
|
|
@@ -23543,7 +24666,7 @@ interface GridVirtualizer {
|
|
|
23543
24666
|
|
|
23544
24667
|
| Class | Thrown when | Notable properties |
|
|
23545
24668
|
| --- | --- | --- |
|
|
23546
|
-
| `ScrollError` | Base class for every Scroll error. | `ScrollError
|
|
24669
|
+
| `ScrollError` | Base class for every Scroll error. | Use `instanceof ScrollError` to narrow unknown errors narrows errors from this package. |
|
|
23547
24670
|
| `ScrollConfigurationError` | A constructor or `update()` receives invalid static configuration. | Extends `ScrollError`; malformed JavaScript values also use this class. |
|
|
23548
24671
|
| `ScrollRangeError` | A DOM virtual-list render detects that a caller mutated its items array without calling `setItems()` again. | Extends `ScrollError`; message includes stale index and current item count. |
|
|
23549
24672
|
|
|
@@ -24577,9 +25700,9 @@ const stopObserving = observeViewport();
|
|
|
24577
25700
|
|
|
24578
25701
|
## Documentation
|
|
24579
25702
|
|
|
24580
|
-
- [**Usage Guide**](./usage.md)
|
|
24581
|
-
- [**API Reference**](./api.md)
|
|
24582
|
-
- [**Examples**](./examples.md)
|
|
25703
|
+
- [**Usage Guide**](./usage.md)
|
|
25704
|
+
- [**API Reference**](./api.md)
|
|
25705
|
+
- [**Examples**](./examples.md)
|
|
24583
25706
|
|
|
24584
25707
|
## See Also
|
|
24585
25708
|
|
|
@@ -26167,7 +27290,7 @@ if (!result.success) {
|
|
|
26167
27290
|
|
|
26168
27291
|
## Errors
|
|
26169
27292
|
|
|
26170
|
-
- `SpellError` — base class. Use `SpellError
|
|
27293
|
+
- `SpellError` — base class. Use `instanceof SpellError` for cross-boundary narrowing.
|
|
26171
27294
|
- `SpellValidationError` — validation failure with `issues`, `bestMatch()`, `messagesAt()`, `flatten()`, and `flattenFirst()`.
|
|
26172
27295
|
- `SpellDefinitionError` — schema cannot create portable definition.
|
|
26173
27296
|
|
|
@@ -27505,10 +28628,10 @@ try {
|
|
|
27505
28628
|
- `/memory`, `/local-storage`, and `/session-storage` return portable `VaultStore` instances without loading other adapters.
|
|
27506
28629
|
- `observe()` emits current and changed table snapshots.
|
|
27507
28630
|
- `ttl` creates validated expiration durations.
|
|
27508
|
-
- `/indexeddb` returns `
|
|
28631
|
+
- `/indexeddb` returns `TransactionalVaultStore` with `batch()` and `iterate()`.
|
|
27509
28632
|
- `createSQLite()` is an opt-in, driver-neutral subpath for Node, Bun, and Deno SQLite drivers.
|
|
27510
28633
|
- `/indexeddb` also exports `defineMigration()` for schema upgrades.
|
|
27511
|
-
- `
|
|
28634
|
+
- `pruneExpired()` removes stale TTL entries on demand.
|
|
27512
28635
|
|
|
27513
28636
|
## Documentation
|
|
27514
28637
|
|
|
@@ -27533,9 +28656,10 @@ try {
|
|
|
27533
28656
|
| `createLocalStorage()` / `createSessionStorage()` | Web Storage-backed portable stores | Async API | Available only where the corresponding Web API exists |
|
|
27534
28657
|
| `createIndexedDB()` | Browser transactions and cursor iteration | Async API | Import from `/indexeddb` |
|
|
27535
28658
|
| `createSQLite()` | Driver-neutral SQLite store | Async API over a synchronous driver | Import from `/sqlite` |
|
|
28659
|
+
| `defineMigration()` | Declarative IndexedDB schema upgrade | Sync | Import from `/indexeddb` |
|
|
27536
28660
|
| `table()` | Typed record schema | Sync | The key field must be a string or finite number |
|
|
27537
28661
|
| `ttl` | Valid expiration durations | Sync | Durations must be positive |
|
|
27538
|
-
| `
|
|
28662
|
+
| `isExpired()` | Check an expiration timestamp | Sync | Returns `false` when no expiry is set |
|
|
27539
28663
|
|
|
27540
28664
|
## Package Entry Points
|
|
27541
28665
|
|
|
@@ -27622,23 +28746,20 @@ if (isExpired(record.expiresAt)) console.log('expired');
|
|
|
27622
28746
|
|
|
27623
28747
|
## Factories
|
|
27624
28748
|
|
|
27625
|
-
All factory options accept `schema
|
|
28749
|
+
All factory options accept `schema` and optional `validators`. The root entry does not export any factory.
|
|
27626
28750
|
|
|
27627
28751
|
### `createMemory()`
|
|
27628
28752
|
|
|
27629
28753
|
```ts
|
|
27630
|
-
function createMemory(options:
|
|
27631
|
-
name?: string;
|
|
27632
|
-
schema: S;
|
|
27633
|
-
} & BaseAdapterOptions): VaultStore;
|
|
28754
|
+
function createMemory(options: BaseAdapterOptions): VaultStore;
|
|
27634
28755
|
```
|
|
27635
28756
|
|
|
27636
|
-
Creates an in-memory portable store.
|
|
28757
|
+
Creates an in-memory portable store.
|
|
27637
28758
|
|
|
27638
28759
|
| Parameter | Description |
|
|
27639
28760
|
| --- | --- |
|
|
27640
28761
|
| `schema` | Tables created by `table()` |
|
|
27641
|
-
| `
|
|
28762
|
+
| `validators` | Optional per-table validators with a `parse(value): T` method |
|
|
27642
28763
|
|
|
27643
28764
|
**Returns:** `VaultStore`.
|
|
27644
28765
|
|
|
@@ -27654,20 +28775,20 @@ const store = createMemory({ schema: { users: table('id') } });
|
|
|
27654
28775
|
### `createLocalStorage()`
|
|
27655
28776
|
|
|
27656
28777
|
```ts
|
|
27657
|
-
function createLocalStorage(options: {
|
|
28778
|
+
function createLocalStorage(options: BaseAdapterOptions & {
|
|
27658
28779
|
name: string;
|
|
27659
28780
|
onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';
|
|
27660
|
-
|
|
27661
|
-
} & BaseAdapterOptions): VaultStore;
|
|
28781
|
+
}): VaultStore;
|
|
27662
28782
|
```
|
|
27663
28783
|
|
|
27664
28784
|
Creates a namespaced `localStorage` store.
|
|
27665
28785
|
|
|
27666
28786
|
| Parameter | Description |
|
|
27667
28787
|
| --- | --- |
|
|
28788
|
+
| `schema` | Tables created by `table()` |
|
|
28789
|
+
| `validators` | Optional per-table validators |
|
|
27668
28790
|
| `name` | Required storage namespace |
|
|
27669
28791
|
| `onQuotaExceeded` | Handles a Web Storage quota error; returning `'ignore'` drops that write |
|
|
27670
|
-
| `schema` | Tables created by `table()` |
|
|
27671
28792
|
|
|
27672
28793
|
**Returns:** `VaultStore`.
|
|
27673
28794
|
|
|
@@ -27683,11 +28804,10 @@ const store = createLocalStorage({ name: 'app', schema: { settings: table('id')
|
|
|
27683
28804
|
### `createSessionStorage()`
|
|
27684
28805
|
|
|
27685
28806
|
```ts
|
|
27686
|
-
function createSessionStorage(options: {
|
|
28807
|
+
function createSessionStorage(options: BaseAdapterOptions & {
|
|
27687
28808
|
name: string;
|
|
27688
28809
|
onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';
|
|
27689
|
-
|
|
27690
|
-
} & BaseAdapterOptions): VaultStore;
|
|
28810
|
+
}): VaultStore;
|
|
27691
28811
|
```
|
|
27692
28812
|
|
|
27693
28813
|
Creates a namespaced `sessionStorage` store. Its options and return type match `createLocalStorage()`.
|
|
@@ -27706,24 +28826,24 @@ const store = createSessionStorage({ name: 'checkout', schema: { cart: table('id
|
|
|
27706
28826
|
### `createIndexedDB()`
|
|
27707
28827
|
|
|
27708
28828
|
```ts
|
|
27709
|
-
function createIndexedDB(options: {
|
|
28829
|
+
function createIndexedDB(options: BaseAdapterOptions & {
|
|
27710
28830
|
migrate?: MigrationFn;
|
|
27711
28831
|
name: string;
|
|
27712
|
-
schema: S;
|
|
27713
28832
|
version?: number;
|
|
27714
|
-
}
|
|
28833
|
+
}): TransactionalVaultStore;
|
|
27715
28834
|
```
|
|
27716
28835
|
|
|
27717
28836
|
Creates an IndexedDB store with atomic batches, lazy cursor iteration, and optional schema migrations.
|
|
27718
28837
|
|
|
27719
28838
|
| Parameter | Description |
|
|
27720
28839
|
| --- | --- |
|
|
27721
|
-
| `name` | Required database name |
|
|
27722
28840
|
| `schema` | Tables and IndexedDB secondary indexes |
|
|
28841
|
+
| `validators` | Optional per-table validators |
|
|
28842
|
+
| `name` | Required database name |
|
|
27723
28843
|
| `version` | Positive schema version; defaults to `1` |
|
|
27724
28844
|
| `migrate` | Synchronous upgrade callback for version changes |
|
|
27725
28845
|
|
|
27726
|
-
**Returns:** `
|
|
28846
|
+
**Returns:** `TransactionalVaultStore`.
|
|
27727
28847
|
|
|
27728
28848
|
```ts
|
|
27729
28849
|
import { table } from '@vielzeug/vault';
|
|
@@ -27737,19 +28857,20 @@ const store = createIndexedDB({ name: 'app', schema: { users: table('id') } });
|
|
|
27737
28857
|
### `createSQLite()`
|
|
27738
28858
|
|
|
27739
28859
|
```ts
|
|
27740
|
-
function createSQLite(options: SQLiteVaultOptions):
|
|
28860
|
+
function createSQLite(options: SQLiteVaultOptions): TransactionalVaultStore;
|
|
27741
28861
|
```
|
|
27742
28862
|
|
|
27743
28863
|
Creates a namespaced SQLite store with atomic batches and keyset-paginated iteration. It accepts an application-provided positional-parameter driver and never opens or imports a runtime driver.
|
|
27744
28864
|
|
|
27745
28865
|
| Parameter | Description |
|
|
27746
28866
|
| --- | --- |
|
|
28867
|
+
| `schema` | Tables created by `table()` |
|
|
28868
|
+
| `validators` | Optional per-table validators |
|
|
27747
28869
|
| `database` | Caller-provided `SQLiteDatabase` connection |
|
|
27748
28870
|
| `name` | Namespace within the connection |
|
|
27749
|
-
| `schema`, `validators`, `logger`, `onMetrics` | Shared factory options |
|
|
27750
28871
|
| `closeOnDispose` | Closes the connection during disposal; defaults to `false` |
|
|
27751
28872
|
|
|
27752
|
-
**Returns:** `
|
|
28873
|
+
**Returns:** `TransactionalVaultStore`.
|
|
27753
28874
|
|
|
27754
28875
|
```ts
|
|
27755
28876
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -27776,11 +28897,9 @@ interface VaultStore {
|
|
|
27776
28897
|
count(table: K): Promise;
|
|
27777
28898
|
delete(table: K, key: KeyOf): Promise;
|
|
27778
28899
|
deleteMany(table: K, keys: KeyOf[]): Promise;
|
|
27779
|
-
entries(table: K): Promise, RecordOf]>>;
|
|
27780
28900
|
get(table: K, key: KeyOf): Promise | undefined>;
|
|
27781
28901
|
getAll(table: K): Promise[]>;
|
|
27782
28902
|
getMany(table: K, keys: KeyOf[]): Promise | undefined>>;
|
|
27783
|
-
getOrDefault(table: K, key: KeyOf, defaultFn: () => RecordOf, ttl?: number): Promise>;
|
|
27784
28903
|
has(table: K, key: KeyOf): Promise;
|
|
27785
28904
|
isEmpty(table: K): Promise;
|
|
27786
28905
|
keys(table: K, filter?: (record: RecordOf) => boolean): Promise[]>;
|
|
@@ -27790,7 +28909,6 @@ interface VaultStore {
|
|
|
27790
28909
|
update(table: K, key: KeyOf, changes: Partial>, ttl?: number): Promise | undefined>;
|
|
27791
28910
|
upsert(table: K, key: KeyOf, fn: (existing: RecordOf | undefined) => RecordOf, ttl?: number): Promise>;
|
|
27792
28911
|
pruneExpired(): Promise>;
|
|
27793
|
-
debug(): Promise>;
|
|
27794
28912
|
observe(table: K, listener: Observer>, options?: { immediate?: boolean; signal?: AbortSignal }): Unsubscribe;
|
|
27795
28913
|
dispose(): Promise;
|
|
27796
28914
|
readonly disposed: boolean;
|
|
@@ -27803,7 +28921,7 @@ The portable store API is returned by every factory. `observe()` emits the curre
|
|
|
27803
28921
|
|
|
27804
28922
|
---
|
|
27805
28923
|
|
|
27806
|
-
### `batch()`
|
|
28924
|
+
### `batch()` and `iterate()`
|
|
27807
28925
|
|
|
27808
28926
|
```ts
|
|
27809
28927
|
interface TransactionalVaultStore extends VaultStore {
|
|
@@ -27811,10 +28929,11 @@ interface TransactionalVaultStore extends VaultStore {
|
|
|
27811
28929
|
tables: readonly K[],
|
|
27812
28930
|
fn: (tx: TransactionContext) => Promise,
|
|
27813
28931
|
): Promise;
|
|
28932
|
+
iterate(table: K): AsyncIterable>;
|
|
27814
28933
|
}
|
|
27815
28934
|
```
|
|
27816
28935
|
|
|
27817
|
-
|
|
28936
|
+
`batch()` runs a scoped atomic callback. `iterate()` lazily yields table records — IndexedDB uses a cursor, SQLite uses keyset pagination. Both are provided by `createIndexedDB()` and `createSQLite()`.
|
|
27818
28937
|
|
|
27819
28938
|
| Parameter | Description |
|
|
27820
28939
|
| --- | --- |
|
|
@@ -27827,43 +28946,24 @@ Runs a scoped atomic callback. `IndexedDbVaultStore` and `SQLiteVaultStore` prov
|
|
|
27827
28946
|
await store.batch(['users'], async (tx) => {
|
|
27828
28947
|
await tx.put('users', { id: 1, name: 'Ada' });
|
|
27829
28948
|
});
|
|
27830
|
-
```
|
|
27831
|
-
|
|
27832
|
-
---
|
|
27833
|
-
|
|
27834
|
-
### `iterate()`
|
|
27835
|
-
|
|
27836
|
-
```ts
|
|
27837
|
-
interface IterableVaultStore extends VaultStore {
|
|
27838
|
-
iterate(table: K): AsyncIterable>;
|
|
27839
|
-
}
|
|
27840
|
-
```
|
|
27841
|
-
|
|
27842
|
-
Lazily yields table records. `IndexedDbVaultStore` uses a cursor; `SQLiteVaultStore` uses keyset pagination.
|
|
27843
28949
|
|
|
27844
|
-
**Returns:** An `AsyncIterable` of records.
|
|
27845
|
-
|
|
27846
|
-
```ts
|
|
27847
28950
|
for await (const user of store.iterate('users')) console.log(user);
|
|
27848
28951
|
```
|
|
27849
28952
|
|
|
27850
|
-
## Queries
|
|
28953
|
+
## Queries and Migrations
|
|
27851
28954
|
|
|
27852
28955
|
### `QueryBuilder`
|
|
27853
28956
|
|
|
27854
28957
|
```ts
|
|
27855
28958
|
interface QueryBuilder {
|
|
27856
|
-
between(field: string, lower: number | string, upper: number | string): QueryBuilder;
|
|
27857
28959
|
count(): Promise;
|
|
27858
28960
|
delete(): Promise;
|
|
27859
|
-
equals(field: K, value: V): QueryBuilder
|
|
27860
|
-
|
|
27861
|
-
filter(fn: (value: N, index: number, array: N[]) => boolean): QueryBuilder;
|
|
28961
|
+
equals(field: K, value: V): QueryBuilder;
|
|
28962
|
+
filter(fn: (value: T, index: number, array: T[]) => boolean): QueryBuilder;
|
|
27862
28963
|
first(): Promise;
|
|
27863
28964
|
limit(n: number): QueryBuilder;
|
|
27864
28965
|
offset(n: number): QueryBuilder;
|
|
27865
28966
|
orderBy(field: K, direction?: 'asc' | 'desc'): QueryBuilder;
|
|
27866
|
-
startsWith(field: keyof T, prefix: string, options?: { ignoreCase?: boolean }): QueryBuilder;
|
|
27867
28967
|
toArray(): Promise;
|
|
27868
28968
|
}
|
|
27869
28969
|
```
|
|
@@ -27871,36 +28971,7 @@ interface QueryBuilder {
|
|
|
27871
28971
|
Builds a lazy table query. `count()` ignores `limit()`, `offset()`, and `orderBy()` — it always returns the full filtered-set size.
|
|
27872
28972
|
|
|
27873
28973
|
```ts
|
|
27874
|
-
const page = await store.query('users').
|
|
27875
|
-
```
|
|
27876
|
-
|
|
27877
|
-
---
|
|
27878
|
-
|
|
27879
|
-
### `scheduleExpiredPrune()`
|
|
27880
|
-
|
|
27881
|
-
```ts
|
|
27882
|
-
function scheduleExpiredPrune(
|
|
27883
|
-
adapter: Pick, 'pruneExpired'>,
|
|
27884
|
-
options: {
|
|
27885
|
-
interval: number;
|
|
27886
|
-
onError?: (error: unknown) => void;
|
|
27887
|
-
signal?: AbortSignal;
|
|
27888
|
-
},
|
|
27889
|
-
): () => void;
|
|
27890
|
-
```
|
|
27891
|
-
|
|
27892
|
-
Schedules `pruneExpired()` at a finite, positive interval. Pass `signal: store.disposalSignal` to auto-cancel when the store is torn down.
|
|
27893
|
-
|
|
27894
|
-
**Returns:** A stop function.
|
|
27895
|
-
|
|
27896
|
-
```ts
|
|
27897
|
-
import { scheduleExpiredPrune, ttl } from '@vielzeug/vault';
|
|
27898
|
-
|
|
27899
|
-
const stop = scheduleExpiredPrune(store, {
|
|
27900
|
-
interval: ttl.hours(1),
|
|
27901
|
-
signal: store.disposalSignal,
|
|
27902
|
-
});
|
|
27903
|
-
stop();
|
|
28974
|
+
const page = await store.query('users').equals('role', 'admin').orderBy('name').limit(20).toArray();
|
|
27904
28975
|
```
|
|
27905
28976
|
|
|
27906
28977
|
---
|
|
@@ -27942,16 +29013,10 @@ type KeyOf =
|
|
|
27942
29013
|
|
|
27943
29014
|
```ts
|
|
27944
29015
|
type BaseAdapterOptions = {
|
|
27945
|
-
logger?: VaultLogger;
|
|
27946
|
-
onMetrics?: (event: MetricsEvent) => void;
|
|
27947
29016
|
schema: S;
|
|
27948
29017
|
validators?: TableValidators;
|
|
27949
29018
|
};
|
|
27950
29019
|
|
|
27951
|
-
type VaultLogger = {
|
|
27952
|
-
error(message: string, context?: Error | Record): void;
|
|
27953
|
-
};
|
|
27954
|
-
|
|
27955
29020
|
type RecordValidator = {
|
|
27956
29021
|
parse(value: unknown): T;
|
|
27957
29022
|
};
|
|
@@ -27959,23 +29024,9 @@ type RecordValidator = {
|
|
|
27959
29024
|
type TableValidators = {
|
|
27960
29025
|
[K in keyof S]?: RecordValidator>;
|
|
27961
29026
|
};
|
|
27962
|
-
|
|
27963
|
-
type MetricsEvent = {
|
|
27964
|
-
duration: number;
|
|
27965
|
-
operation: 'batch' | 'clear' | 'count' | 'delete' | 'deleteMany' | 'entries' | 'get' | 'getAll' |
|
|
27966
|
-
'getMany' | 'getOrDefault' | 'has' | 'isEmpty' | 'keys' | 'put' | 'putAll' | 'query' |
|
|
27967
|
-
'queryDelete' | 'update' | 'upsert';
|
|
27968
|
-
table: string;
|
|
27969
|
-
};
|
|
27970
|
-
|
|
27971
|
-
type DebugStats = { expiredCount: number; recordCount: number };
|
|
27972
|
-
type DebugInfo = { tables: Array };
|
|
27973
29027
|
```
|
|
27974
29028
|
|
|
27975
29029
|
```ts
|
|
27976
|
-
interface IndexedDbVaultStore
|
|
27977
|
-
extends TransactionalVaultStore, IterableVaultStore {}
|
|
27978
|
-
|
|
27979
29030
|
type MigrationContext = {
|
|
27980
29031
|
db: IDBDatabase;
|
|
27981
29032
|
newVersion: number | null;
|
|
@@ -28015,20 +29066,57 @@ type SQLiteVaultOptions = BaseAdapterOptions & {
|
|
|
28015
29066
|
database: SQLiteDatabase;
|
|
28016
29067
|
name: string;
|
|
28017
29068
|
};
|
|
29069
|
+
```
|
|
29070
|
+
|
|
29071
|
+
```ts
|
|
29072
|
+
interface TransactionalVaultStore extends VaultStore {
|
|
29073
|
+
batch(
|
|
29074
|
+
tables: readonly K[],
|
|
29075
|
+
fn: (tx: TransactionContext) => Promise,
|
|
29076
|
+
): Promise;
|
|
29077
|
+
iterate(table: K): AsyncIterable>;
|
|
29078
|
+
}
|
|
29079
|
+
```
|
|
29080
|
+
|
|
29081
|
+
Import `TransactionalVaultStore` from `@vielzeug/vault`.
|
|
28018
29082
|
|
|
28019
|
-
|
|
28020
|
-
|
|
29083
|
+
```ts
|
|
29084
|
+
interface TransactionContext {
|
|
29085
|
+
clear(table: T): Promise;
|
|
29086
|
+
count(table: T): Promise;
|
|
29087
|
+
delete(table: T, key: KeyOf): Promise;
|
|
29088
|
+
deleteMany(table: T, keys: KeyOf[]): Promise;
|
|
29089
|
+
get(table: T, key: KeyOf): Promise | undefined>;
|
|
29090
|
+
getAll(table: T): Promise[]>;
|
|
29091
|
+
getMany(table: T, keys: KeyOf[]): Promise | undefined>>;
|
|
29092
|
+
has(table: T, key: KeyOf): Promise;
|
|
29093
|
+
isEmpty(table: T): Promise;
|
|
29094
|
+
keys(table: T, filter?: (record: RecordOf) => boolean): Promise[]>;
|
|
29095
|
+
put(table: T, value: RecordOf, ttl?: number): Promise;
|
|
29096
|
+
putAll(table: T, values: RecordOf[], ttl?: number): Promise;
|
|
29097
|
+
query(table: T): QueryBuilder>;
|
|
29098
|
+
update(table: T, key: KeyOf, changes: Partial>, ttl?: number): Promise | undefined>;
|
|
29099
|
+
upsert(table: T, key: KeyOf, fn: (existing: RecordOf | undefined) => RecordOf, ttl?: number): Promise>;
|
|
29100
|
+
}
|
|
28021
29101
|
```
|
|
28022
29102
|
|
|
28023
29103
|
`TransactionContext` has the same CRUD, query, and TTL methods as `VaultStore`, narrowed to the tables declared in `batch()`. Import it from `@vielzeug/vault/indexeddb` or `@vielzeug/vault/sqlite`.
|
|
28024
29104
|
|
|
29105
|
+
```ts
|
|
29106
|
+
// Adapter-specific type aliases — both resolve to TransactionalVaultStore.
|
|
29107
|
+
type SQLiteVaultStore = TransactionalVaultStore;
|
|
29108
|
+
type IndexedDbVaultStore = TransactionalVaultStore;
|
|
29109
|
+
```
|
|
29110
|
+
|
|
29111
|
+
`SQLiteVaultStore` is exported from `@vielzeug/vault/sqlite`. `IndexedDbVaultStore` is exported from `@vielzeug/vault/indexeddb`.
|
|
29112
|
+
|
|
28025
29113
|
## Errors
|
|
28026
29114
|
|
|
28027
29115
|
| Error | Trigger |
|
|
28028
29116
|
| --- | --- |
|
|
28029
29117
|
| `VaultError` | Any Vault-originated validation, serialization, storage, or query error |
|
|
28030
29118
|
| `VaultDisposedError` | An operation after the store or observer hub is disposed |
|
|
28031
|
-
| `VaultScopeError` |
|
|
29119
|
+
| `VaultScopeError` | A `batch()` callback accesses a table outside its declared scope |
|
|
28032
29120
|
| `VaultQuotaError` | A LocalStorage or SessionStorage write exceeds the browser quota |
|
|
28033
29121
|
| `VaultMigrationError` | An IndexedDB migration callback throws |
|
|
28034
29122
|
|
|
@@ -28095,29 +29183,27 @@ console.log(updated);
|
|
|
28095
29183
|
Build a query from a table, then finish it with a terminal method. `count()` ignores pagination, which makes it suitable for page controls.
|
|
28096
29184
|
|
|
28097
29185
|
```ts
|
|
28098
|
-
const query = store.query('preferences').startsWith('
|
|
29186
|
+
const query = store.query('preferences').filter((p) => p.id.startsWith('theme'));
|
|
28099
29187
|
const preferences = await query.orderBy('id').limit(10).toArray();
|
|
28100
29188
|
const total = await query.count();
|
|
28101
29189
|
|
|
28102
29190
|
console.log({ preferences, total });
|
|
28103
29191
|
```
|
|
28104
29192
|
|
|
28105
|
-
|
|
29193
|
+
Queries scan the table in memory. Use `equals()` for exact field matches and `filter()` for custom predicates. For large tables, prefer `iterate()` on IndexedDB or SQLite instead of materializing every record.
|
|
28106
29194
|
|
|
28107
29195
|
## Use TTL and Pruning
|
|
28108
29196
|
|
|
28109
|
-
Use `ttl.*` helpers for expiring rows.
|
|
29197
|
+
Use `ttl.*` helpers for expiring rows. Call `pruneExpired()` to reclaim storage from stale rows that accumulate without reads.
|
|
28110
29198
|
|
|
28111
29199
|
```ts
|
|
28112
|
-
import {
|
|
29200
|
+
import { ttl } from '@vielzeug/vault';
|
|
28113
29201
|
|
|
28114
29202
|
await store.put('preferences', { id: 'temporary', theme: 'dark' }, ttl.hours(1));
|
|
28115
|
-
const stopPrune = scheduleExpiredPrune(store, {
|
|
28116
|
-
interval: ttl.hours(6),
|
|
28117
|
-
signal: store.disposalSignal,
|
|
28118
|
-
});
|
|
28119
29203
|
|
|
28120
|
-
|
|
29204
|
+
// Reclaim expired rows on a schedule owned by the application.
|
|
29205
|
+
const pruneInterval = setInterval(() => store.pruneExpired(), ttl.hours(6));
|
|
29206
|
+
store.disposalSignal.addEventListener('abort', () => clearInterval(pruneInterval));
|
|
28121
29207
|
```
|
|
28122
29208
|
|
|
28123
29209
|
## Observe a Table
|
|
@@ -28245,7 +29331,7 @@ Use Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple sig
|
|
|
28245
29331
|
- Use IndexedDB or SQLite for atomic work.
|
|
28246
29332
|
- Keep external asynchronous work outside `batch()` callbacks.
|
|
28247
29333
|
- Use `ttl.*` instead of raw durations.
|
|
28248
|
-
- Keep SQLite scans and writes off latency-sensitive event loops
|
|
29334
|
+
- Keep SQLite scans and writes off latency-sensitive event loops.
|
|
28249
29335
|
- Dispose stores when their owner ends.
|
|
28250
29336
|
|
|
28251
29337
|
### Examples
|
|
@@ -28263,10 +29349,10 @@ Use Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple sig
|
|
|
28263
29349
|
|
|
28264
29350
|
- Basic Setup - Initialize Vault (id: `basic-setup`)
|
|
28265
29351
|
- Bulk Operations (id: `bulk-operations`)
|
|
28266
|
-
- Cache-First with
|
|
29352
|
+
- Cache-First with get + put (id: `cache-first`)
|
|
28267
29353
|
- CRUD Operations (id: `crud-operations`)
|
|
28268
29354
|
- IndexedDB — Atomic Batch & iterate() (id: `indexed-db`)
|
|
28269
|
-
- TTL —
|
|
29355
|
+
- TTL — pruneExpired with disposalSignal (id: `prune-schedule`)
|
|
28270
29356
|
- Query Builder — Filters, Pagination, count (id: `query-builder`)
|
|
28271
29357
|
- Reactive — observe() (id: `reactive-observe`)
|
|
28272
29358
|
- TTL & Expiration (id: `ttl-expiration`)
|
|
@@ -28352,6 +29438,7 @@ else console.log(decision.reason);
|
|
|
28352
29438
|
- `WILDCARD` and `ANONYMOUS` model broad or unauthenticated access explicitly.
|
|
28353
29439
|
- `owns()` and `predicate` constrain rules with synchronous request data.
|
|
28354
29440
|
- `explain()`, `trace()`, and `detectConflicts()` make policy decisions diagnosable.
|
|
29441
|
+
- `tap()` subscribes to decision events for logging and diagnostics.
|
|
28355
29442
|
- `forUser()` creates a principal-bound view for repeated checks.
|
|
28356
29443
|
- `checkAll()` evaluates multiple resource/action pairs in one call.
|
|
28357
29444
|
|
|
@@ -28377,10 +29464,10 @@ else console.log(decision.reason);
|
|
|
28377
29464
|
| `createWard` | Creates immutable policy | Sync | Rules cannot be mutated after creation |
|
|
28378
29465
|
| `allow` / `deny` / `ruleFor` | Builds policy rules | Sync | Priority wins before specificity |
|
|
28379
29466
|
| `Ward.explain` | Returns one decision | Sync | Pass resource data for predicate rules |
|
|
28380
|
-
| `Ward.trace` | Inspects decision candidates | Sync | Does not
|
|
29467
|
+
| `Ward.trace` | Inspects decision candidates | Sync | Does not fire a `decision` event |
|
|
28381
29468
|
| `Ward.forUser` | Binds a principal | Sync | Rebind when identity or roles change |
|
|
28382
29469
|
| `Ward.checkAll` | Batch permission checks | Sync | Pass resource data for predicate rules |
|
|
28383
|
-
| `Ward.allowedActions` | Filters known actions to allowed set | Sync | Does not
|
|
29470
|
+
| `Ward.allowedActions` | Filters known actions to allowed set | Sync | Does not fire a `decision` event |
|
|
28384
29471
|
| `Ward.rulesInScope` | Lists rules matching a principal/resource | Sync | Pass data to evaluate predicates |
|
|
28385
29472
|
| `Ward.detectConflicts` | Detects duplicate/shadowed rules | Sync | O(n²) — use `maxConflicts` for large policies |
|
|
28386
29473
|
| `predicate.owns` / `owns` | Ownership predicate on resource data | Sync | Skipped for anonymous principals |
|
|
@@ -28392,7 +29479,6 @@ else console.log(decision.reason);
|
|
|
28392
29479
|
| Import | Purpose |
|
|
28393
29480
|
| --- | --- |
|
|
28394
29481
|
| `@vielzeug/ward` | Rules, factory, predicates, pattern helpers, errors, and public types |
|
|
28395
|
-
| `@vielzeug/ward/devtools` | `debugWard()` diagnostic factory |
|
|
28396
29482
|
|
|
28397
29483
|
## Core Factory
|
|
28398
29484
|
|
|
@@ -28405,14 +29491,13 @@ createWard(
|
|
|
28405
29491
|
): Ward;
|
|
28406
29492
|
```
|
|
28407
29493
|
|
|
28408
|
-
Creates an immutable ward instance. `rules` accepts a flat mix of single rules and rule arrays — `allow()`/`deny()`/`ruleFor()` results can be passed directly without spread. Validates `
|
|
29494
|
+
Creates an immutable ward instance. `rules` accepts a flat mix of single rules and rule arrays — `allow()`/`deny()`/`ruleFor()` results can be passed directly without spread. Validates `onConflict` and `maxConflicts` options before compiling rules; invalid values throw `WardConfigError`.
|
|
28409
29495
|
|
|
28410
29496
|
**Parameters:**
|
|
28411
29497
|
|
|
28412
29498
|
| Name | Type | Description |
|
|
28413
29499
|
| --- | --- | --- |
|
|
28414
29500
|
| `rules` | `readonly (WardRule \| readonly WardRule[])[]` | Rule list. Single rules and rule arrays can be mixed. |
|
|
28415
|
-
| `options.logger` | `(ctx: WardLoggerContext) => void` | Called for `explain()` and `checkAll()` decisions. |
|
|
28416
29501
|
| `options.onConflict` | `(conflict: WardConflict) => void` | Called synchronously per conflict at creation time. |
|
|
28417
29502
|
| `options.strict` | `boolean` | Throws `WardConfigError` on the first conflict. |
|
|
28418
29503
|
| `options.maxConflicts` | `number` | Caps the number of conflicts returned by `detectConflicts()`. |
|
|
@@ -28498,7 +29583,7 @@ checkAll(
|
|
|
28498
29583
|
): WardDecisionResult[];
|
|
28499
29584
|
```
|
|
28500
29585
|
|
|
28501
|
-
Evaluates multiple resource/action pairs for one principal.
|
|
29586
|
+
Evaluates multiple resource/action pairs for one principal. Fires a `decision` event for each result via `tap()`.
|
|
28502
29587
|
|
|
28503
29588
|
**Returns:** `WardDecisionResult[]` — each entry carries `action`, `resource`, and the decision.
|
|
28504
29589
|
|
|
@@ -28521,7 +29606,7 @@ explain(input: WardDecisionInput): WardDecision;
|
|
|
28521
29606
|
}
|
|
28522
29607
|
```
|
|
28523
29608
|
|
|
28524
|
-
Returns one decision.
|
|
29609
|
+
Returns one decision. Fires a `decision` event via `tap()`.
|
|
28525
29610
|
|
|
28526
29611
|
**Returns:** `WardDecision` — `{ allowed: true; rule }` or `{ allowed: false; reason: 'explicit-deny'; rule }` or `{ allowed: false; reason: 'no-matching-rule' }`.
|
|
28527
29612
|
|
|
@@ -28533,7 +29618,7 @@ Returns one decision. Invokes the logger.
|
|
|
28533
29618
|
trace(input: WardDecisionInput): WardTrace;
|
|
28534
29619
|
```
|
|
28535
29620
|
|
|
28536
|
-
Same request shape as `explain()`. Returns winner + candidate list. Does not fire
|
|
29621
|
+
Same request shape as `explain()`. Returns winner + candidate list. Does not fire a `decision` event.
|
|
28537
29622
|
|
|
28538
29623
|
**Returns:** `WardTrace` — `{ candidates: WardTraceCandidate[]; decision: WardDecision }`.
|
|
28539
29624
|
|
|
@@ -28556,7 +29641,7 @@ Input shape:
|
|
|
28556
29641
|
}
|
|
28557
29642
|
```
|
|
28558
29643
|
|
|
28559
|
-
Filters the provided `knownActions` list to those the principal may perform. Does not
|
|
29644
|
+
Filters the provided `knownActions` list to those the principal may perform. Does not fire a `decision` event.
|
|
28560
29645
|
|
|
28561
29646
|
**Returns:** `TAction[]` — the subset of `knownActions` that `explain()` would allow.
|
|
28562
29647
|
|
|
@@ -28712,17 +29797,37 @@ Tests whether the `broad` pattern covers the `narrow` pattern. `'*'` covers ever
|
|
|
28712
29797
|
|
|
28713
29798
|
---
|
|
28714
29799
|
|
|
28715
|
-
##
|
|
29800
|
+
## Observability
|
|
29801
|
+
|
|
29802
|
+
### `tap(handler, options?)`
|
|
29803
|
+
|
|
29804
|
+
```ts
|
|
29805
|
+
tap(
|
|
29806
|
+
handler: (event: WardEvent) => void,
|
|
29807
|
+
options?: { signal?: AbortSignal },
|
|
29808
|
+
): () => void;
|
|
29809
|
+
```
|
|
29810
|
+
|
|
29811
|
+
Subscribes a handler to ward events. Each `explain()` and `checkAll()` decision fires a `decision` event. `trace()` and `allowedActions()` do not fire events.
|
|
29812
|
+
|
|
29813
|
+
Pass an `AbortSignal` to unsubscribe automatically; the returned function unsubscribes manually.
|
|
28716
29814
|
|
|
28717
|
-
|
|
29815
|
+
**Returns:** `() => void` — call to unsubscribe the handler.
|
|
28718
29816
|
|
|
28719
|
-
|
|
29817
|
+
**Example:**
|
|
28720
29818
|
|
|
28721
29819
|
```ts
|
|
28722
|
-
|
|
29820
|
+
const ward = createWard(rules);
|
|
29821
|
+
ward.tap((event) => console.debug(`ward:${event.type}`, event.decision));
|
|
28723
29822
|
```
|
|
28724
29823
|
|
|
28725
|
-
|
|
29824
|
+
With a logger from `@vielzeug/rune`:
|
|
29825
|
+
|
|
29826
|
+
```ts
|
|
29827
|
+
import { createLogger } from '@vielzeug/rune';
|
|
29828
|
+
const log = createLogger({ name: 'ward' });
|
|
29829
|
+
ward.tap((event) => log.debug(event, 'ward:decision'));
|
|
29830
|
+
```
|
|
28726
29831
|
|
|
28727
29832
|
---
|
|
28728
29833
|
|
|
@@ -28847,6 +29952,7 @@ export type Ward = {
|
|
|
28847
29952
|
explain(input: WardDecisionInput): WardDecision;
|
|
28848
29953
|
forUser(principal: UserPrincipal): BoundWard;
|
|
28849
29954
|
rulesInScope(input: WardRulesInScopeInput): ReadonlyArray>>;
|
|
29955
|
+
tap(handler: (event: WardEvent) => void, options?: { signal?: AbortSignal }): () => void;
|
|
28850
29956
|
trace(input: WardDecisionInput): WardTrace;
|
|
28851
29957
|
};
|
|
28852
29958
|
|
|
@@ -28858,7 +29964,9 @@ export type BoundWard = {
|
|
|
28858
29964
|
trace(input: BoundWardDecisionInput): WardTrace;
|
|
28859
29965
|
};
|
|
28860
29966
|
|
|
28861
|
-
export type
|
|
29967
|
+
export type WardEvent = {
|
|
29968
|
+
type: 'decision';
|
|
29969
|
+
decision: WardDecision;
|
|
28862
29970
|
action: TAction;
|
|
28863
29971
|
data?: TData;
|
|
28864
29972
|
principal: Principal;
|
|
@@ -28866,7 +29974,6 @@ export type WardLoggerContext = WardDecision & {
|
|
|
28866
29974
|
};
|
|
28867
29975
|
|
|
28868
29976
|
export type WardOptions = {
|
|
28869
|
-
logger?: (context: WardLoggerContext) => void;
|
|
28870
29977
|
maxConflicts?: number;
|
|
28871
29978
|
onConflict?: (conflict: WardConflict) => void;
|
|
28872
29979
|
strict?: boolean;
|
|
@@ -28877,12 +29984,12 @@ export type WardOptions = {
|
|
|
28877
29984
|
|
|
28878
29985
|
`Ward`, `BoundWard`, `WardDecision`, `WardDecisionResult`, `WardTrace`, `WardTraceCandidate`, `WardConflict`,
|
|
28879
29986
|
`NormalizedWardRule`, `WardOptions`, `WardCheck`, `WardAllowedActionsInput`, `WardRulesInScopeInput`, `RuleContext`,
|
|
28880
|
-
`
|
|
29987
|
+
`WardEvent`, `WardPredicate`, and `ConflictKind` are exported from the root entry point.
|
|
28881
29988
|
|
|
28882
29989
|
## Errors
|
|
28883
29990
|
|
|
28884
|
-
- `WardError` is the base error class; use `WardError
|
|
28885
|
-
- `WardConfigError` reports malformed rules, invalid `createWard` options (`
|
|
29991
|
+
- `WardError` is the base error class; use `instanceof WardError` for narrowing.
|
|
29992
|
+
- `WardConfigError` reports malformed rules, invalid `createWard` options (`onConflict`, `maxConflicts`), invalid principals, and strict conflict initialization.
|
|
28886
29993
|
- `WardPredicateError` reports a throwing synchronous predicate and includes its `ruleIndex` and cause.
|
|
28887
29994
|
|
|
28888
29995
|
### Usage Guide
|
|
@@ -28952,7 +30059,7 @@ const actions = ward.allowedActions({
|
|
|
28952
30059
|
});
|
|
28953
30060
|
```
|
|
28954
30061
|
|
|
28955
|
-
It does not fire
|
|
30062
|
+
It does not fire a `decision` event.
|
|
28956
30063
|
|
|
28957
30064
|
## Rule Introspection
|
|
28958
30065
|
|
|
@@ -28979,7 +30086,34 @@ trace.candidates.forEach((c) => {
|
|
|
28979
30086
|
});
|
|
28980
30087
|
```
|
|
28981
30088
|
|
|
28982
|
-
`trace()` does not fire
|
|
30089
|
+
`trace()` does not fire a `decision` event.
|
|
30090
|
+
|
|
30091
|
+
## Observing Decisions
|
|
30092
|
+
|
|
30093
|
+
`tap()` subscribes a handler to ward events. Each `explain()` and `checkAll()` decision fires a `decision` event; `trace()` and `allowedActions()` do not.
|
|
30094
|
+
|
|
30095
|
+
```ts
|
|
30096
|
+
const ward = createWard(rules);
|
|
30097
|
+
ward.tap((event) => console.debug(`ward:${event.type}`, event.decision));
|
|
30098
|
+
```
|
|
30099
|
+
|
|
30100
|
+
Pass an `AbortSignal` to unsubscribe automatically, or call the returned function to unsubscribe manually:
|
|
30101
|
+
|
|
30102
|
+
```ts
|
|
30103
|
+
const controller = new AbortController();
|
|
30104
|
+
const unsubscribe = ward.tap((event) => console.debug(event), { signal: controller.signal });
|
|
30105
|
+
|
|
30106
|
+
// later
|
|
30107
|
+
unsubscribe(); // or controller.abort();
|
|
30108
|
+
```
|
|
30109
|
+
|
|
30110
|
+
For structured logging, forward events to a `@vielzeug/rune` logger:
|
|
30111
|
+
|
|
30112
|
+
```ts
|
|
30113
|
+
import { createLogger } from '@vielzeug/rune';
|
|
30114
|
+
const log = createLogger({ name: 'ward' });
|
|
30115
|
+
ward.tap((event) => log.debug(event, 'ward:decision'));
|
|
30116
|
+
```
|
|
28983
30117
|
|
|
28984
30118
|
## Predicate Helpers
|
|
28985
30119
|
|
|
@@ -29079,7 +30213,7 @@ container.register('ward', ward);
|
|
|
29079
30213
|
- [Priority and Overrides](./examples/inheritance-and-overrides.md)
|
|
29080
30214
|
- [Bound Guard in UI Layer](./examples/bound-guard-in-ui-layer.md)
|
|
29081
30215
|
- [Rule Specificity](./examples/disabling-wildcard-fallback.md)
|
|
29082
|
-
- [
|
|
30216
|
+
- [Auditing Decisions](./examples/logger-for-auditing.md)
|
|
29083
30217
|
- [Fresh Ward Per Test](./examples/snapshot-restore-for-test-isolation.md)
|
|
29084
30218
|
- [Conflict Detection](./examples/conflict-detection.md)
|
|
29085
30219
|
- [Trace a Decision](./examples/trace-decision.md)
|
|
@@ -29213,7 +30347,7 @@ router.dispose();
|
|
|
29213
30347
|
- `match()` / `load()` — Inspect routes synchronously or load route data without navigation.
|
|
29214
30348
|
- `preload()` — Warms route data for a later matching navigation.
|
|
29215
30349
|
- `createMemoryHistory()` — Runs routers in tests and non-browser environments.
|
|
29216
|
-
- `
|
|
30350
|
+
- `subscribe()` — Reactive subscription to navigation state changes.
|
|
29217
30351
|
|
|
29218
30352
|
## Documentation
|
|
29219
30353
|
|
|
@@ -29253,10 +30387,9 @@ router.dispose();
|
|
|
29253
30387
|
|
|
29254
30388
|
## Package Entry Points
|
|
29255
30389
|
|
|
29256
|
-
| Import
|
|
29257
|
-
|
|
|
29258
|
-
| `@vielzeug/wayfinder`
|
|
29259
|
-
| `@vielzeug/wayfinder/devtools` | `debugRouter` — navigation logger (dev only) |
|
|
30390
|
+
| Import | Purpose |
|
|
30391
|
+
| --------------------- | ---------------------- |
|
|
30392
|
+
| `@vielzeug/wayfinder` | Main exports and types |
|
|
29260
30393
|
|
|
29261
30394
|
## `createRouter(options)`
|
|
29262
30395
|
|
|
@@ -30062,44 +31195,6 @@ Thrown on middleware misuse — currently only when a middleware function calls
|
|
|
30062
31195
|
| `/files/:rest*` | `/files/a/b/c` | Wildcard suffix captured as one named param |
|
|
30063
31196
|
| `*` | anything | Global catch-all |
|
|
30064
31197
|
|
|
30065
|
-
## `debugRouter(options)`
|
|
30066
|
-
|
|
30067
|
-
```ts
|
|
30068
|
-
import { debugRouter } from '@vielzeug/wayfinder/devtools';
|
|
30069
|
-
|
|
30070
|
-
const router = debugRouter({ routes });
|
|
30071
|
-
// [wayfinder:nav] idle / [home] ← logged when initial navigation settles
|
|
30072
|
-
// [wayfinder:nav] loading /dashboard
|
|
30073
|
-
// [wayfinder:nav] idle /dashboard [dashboard.index]
|
|
30074
|
-
```
|
|
30075
|
-
|
|
30076
|
-
Wraps `createRouter()` and attaches a `subscribe` listener that logs every navigation state change to `console.debug`. Returns the same `Router` instance — all methods are identical to `createRouter()`. The first logged entry appears when the initial navigation completes (not synchronously at construction).
|
|
30077
|
-
|
|
30078
|
-
Import from the dedicated sub-path so the `console.debug` reference is tree-shaken from production bundles when not imported.
|
|
30079
|
-
|
|
30080
|
-
### `DebugRouterOptions`
|
|
30081
|
-
|
|
30082
|
-
Extends `RouterOptions` with one additional field:
|
|
30083
|
-
|
|
30084
|
-
| Option | Type | Default | Description |
|
|
30085
|
-
| ------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
30086
|
-
| `label` | `string` | `'nav'` | Label used in log prefixes. Produces `[wayfinder:]`. Useful when running multiple routers simultaneously. |
|
|
30087
|
-
|
|
30088
|
-
```ts
|
|
30089
|
-
// Multi-router setup — distinguish logs by label:
|
|
30090
|
-
const main = debugRouter({ routes, label: 'main' });
|
|
30091
|
-
const modal = debugRouter({ routes: modalRoutes, label: 'modal' });
|
|
30092
|
-
// [wayfinder:main] idle /dashboard
|
|
30093
|
-
// [wayfinder:modal] loading /confirm
|
|
30094
|
-
```
|
|
30095
|
-
|
|
30096
|
-
| Log format | When |
|
|
30097
|
-
| ------------------------------------------------------- | -------------------------------------- |
|
|
30098
|
-
| `[wayfinder:nav] idle /path [routeName]` | Navigation settled |
|
|
30099
|
-
| `[wayfinder:nav] loading /path` | Data loaders in flight |
|
|
30100
|
-
| `[wayfinder:nav] streaming /path [routeName]` | Streaming loader emitting partial data |
|
|
30101
|
-
| `[wayfinder:nav] error /path [routeName] ` | Navigation error |
|
|
30102
|
-
|
|
30103
31198
|
## Design Notes
|
|
30104
31199
|
|
|
30105
31200
|
- Wayfinder no longer exposes imperative registration methods like `on()`, `group()`, or `use()`.
|
|
@@ -30741,43 +31836,50 @@ export function useRouter() {
|
|
|
30741
31836
|
|
|
30742
31837
|
For full RouterView and RouterLink patterns, see [React Integration](./examples/react-integration.md), [Vue Integration](./examples/vue-integration.md), and [Svelte Integration](./examples/svelte-integration.md).
|
|
30743
31838
|
|
|
30744
|
-
## Debug
|
|
31839
|
+
## Debug Logging
|
|
30745
31840
|
|
|
30746
|
-
|
|
31841
|
+
`router.subscribe()` is the reactive subscription API — it receives every state change, including `loading`, `streaming`, and `error` transitions. Attach a listener that logs to `console.debug` to inspect navigation without any dedicated debug tooling.
|
|
30747
31842
|
|
|
30748
31843
|
```ts
|
|
30749
|
-
import {
|
|
31844
|
+
import { createRouter } from '@vielzeug/wayfinder';
|
|
30750
31845
|
|
|
30751
|
-
const router =
|
|
30752
|
-
|
|
30753
|
-
|
|
30754
|
-
dashboard: { path: '/dashboard', data: () => fetchDashboard() },
|
|
30755
|
-
},
|
|
31846
|
+
const router = createRouter({ routes });
|
|
31847
|
+
const stop = router.subscribe((state) => {
|
|
31848
|
+
console.debug(`[wayfinder] ${state.status} ${state.location.pathname}`);
|
|
30756
31849
|
});
|
|
30757
31850
|
|
|
30758
31851
|
// Logged once the initial navigation completes:
|
|
30759
|
-
// [wayfinder
|
|
31852
|
+
// [wayfinder] idle /
|
|
30760
31853
|
|
|
30761
31854
|
// On navigate({ name: 'dashboard' }):
|
|
30762
|
-
// [wayfinder
|
|
30763
|
-
// [wayfinder
|
|
31855
|
+
// [wayfinder] loading /dashboard
|
|
31856
|
+
// [wayfinder] idle /dashboard
|
|
30764
31857
|
```
|
|
30765
31858
|
|
|
30766
|
-
The
|
|
31859
|
+
The returned function unsubscribes the listener — call it when the logger is no longer needed (e.g. on teardown):
|
|
30767
31860
|
|
|
30768
|
-
|
|
31861
|
+
```ts
|
|
31862
|
+
stop();
|
|
31863
|
+
```
|
|
31864
|
+
|
|
31865
|
+
Errors are surfaced on the state object, so you can log them explicitly:
|
|
30769
31866
|
|
|
30770
31867
|
```ts
|
|
30771
|
-
|
|
31868
|
+
router.subscribe((state) => {
|
|
31869
|
+
if (state.status === 'error') {
|
|
31870
|
+
console.error(`[wayfinder] ${state.location.pathname}`, state.error);
|
|
31871
|
+
}
|
|
31872
|
+
});
|
|
30772
31873
|
```
|
|
30773
31874
|
|
|
30774
|
-
Use
|
|
31875
|
+
Use a label when running multiple routers to distinguish their log output:
|
|
30775
31876
|
|
|
30776
31877
|
```ts
|
|
30777
|
-
const main =
|
|
30778
|
-
|
|
30779
|
-
|
|
30780
|
-
|
|
31878
|
+
const main = createRouter({ routes });
|
|
31879
|
+
main.subscribe((state) => console.debug(`[wayfinder:main] ${state.status} ${state.location.pathname}`));
|
|
31880
|
+
|
|
31881
|
+
const modal = createRouter({ routes: modalRoutes });
|
|
31882
|
+
modal.subscribe((state) => console.debug(`[wayfinder:modal] ${state.status} ${state.location.pathname}`));
|
|
30781
31883
|
```
|
|
30782
31884
|
|
|
30783
31885
|
Debug logging has no effect on behavior and should not be enabled in production.
|
|
@@ -30861,7 +31963,7 @@ router.subscribe((state) => {
|
|
|
30861
31963
|
### REPL Examples
|
|
30862
31964
|
|
|
30863
31965
|
- Basic Routing — Route State and Navigation (id: `basic-routing`)
|
|
30864
|
-
-
|
|
31966
|
+
- Navigation Logging (id: `debug-router`)
|
|
30865
31967
|
- Guards and Redirects — Auth Flows (id: `middleware-auth`)
|
|
30866
31968
|
- Middleware Chain — Execution Flow (id: `middleware-chain`)
|
|
30867
31969
|
- Named Routes — Type-Safe Navigation (id: `named-routes`)
|