@vielzeug/codex 2.2.9 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +85 -41
- package/data/llms-full.txt +1514 -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 +51 -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 +4150 -4190
- package/data/search.json +80 -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.3.0
|
|
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,1036 @@ 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, with optional delayed eligibility via `availableAt`.
|
|
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(
|
|
14621
|
+
name: K,
|
|
14622
|
+
payload: InferJobPayload,
|
|
14623
|
+
options?: EnqueueOptions,
|
|
14624
|
+
): Promise;
|
|
14625
|
+
```
|
|
14626
|
+
|
|
14627
|
+
Validates the payload (if `validate` is defined), derives the key, persists the job, and wakes the processor. Throws `PostmasterError` for an empty key, non-JSON-serializable payload, or invalid `availableAt`.
|
|
14628
|
+
|
|
14629
|
+
| Parameter | Type | Description |
|
|
14630
|
+
| --- | --- | --- |
|
|
14631
|
+
| `name` | `K` | Registered job name |
|
|
14632
|
+
| `payload` | `InferJobPayload` | Job payload (validated if `validate` is defined) |
|
|
14633
|
+
| `options.availableAt` | `number` | Earliest epoch timestamp (ms) the job may be claimed. Defaults to the Postmaster clock. Must be a finite non-negative safe integer. |
|
|
14634
|
+
|
|
14635
|
+
**Delayed eligibility.** The job persists immediately but cannot be claimed before `availableAt`. Postmaster does not guarantee execution at that time — only that the job will not be claimed earlier. A live processor (`start()` or `flush()`) is required for execution. Past timestamps remain immediately eligible.
|
|
14636
|
+
|
|
14637
|
+
**Example**
|
|
14638
|
+
|
|
14639
|
+
```ts
|
|
14640
|
+
await postmaster.enqueue('sendDigest', { userId }, { availableAt: Date.now() + 60_000 });
|
|
14641
|
+
```
|
|
14642
|
+
|
|
14643
|
+
---
|
|
14644
|
+
|
|
14645
|
+
### `start()`
|
|
14646
|
+
|
|
14647
|
+
```ts
|
|
14648
|
+
start(): Promise;
|
|
14649
|
+
```
|
|
14650
|
+
|
|
14651
|
+
Begins background processing. Idempotent.
|
|
14652
|
+
|
|
14653
|
+
---
|
|
14654
|
+
|
|
14655
|
+
### `flush()`
|
|
14656
|
+
|
|
14657
|
+
```ts
|
|
14658
|
+
flush(options?: { signal?: AbortSignal }): Promise;
|
|
14659
|
+
```
|
|
14660
|
+
|
|
14661
|
+
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.
|
|
14662
|
+
|
|
14663
|
+
---
|
|
14664
|
+
|
|
14665
|
+
### `list()`
|
|
14666
|
+
|
|
14667
|
+
```ts
|
|
14668
|
+
list(filter?: EntryFilter): Promise;
|
|
14669
|
+
```
|
|
14670
|
+
|
|
14671
|
+
Returns entries ordered by `createdAt`. Filter by `status` optionally.
|
|
14672
|
+
|
|
14673
|
+
---
|
|
14674
|
+
|
|
14675
|
+
### `stats()`
|
|
14676
|
+
|
|
14677
|
+
```ts
|
|
14678
|
+
stats(): Promise;
|
|
14679
|
+
```
|
|
14680
|
+
|
|
14681
|
+
Returns counts of queued, running, and dead-letter jobs.
|
|
14682
|
+
|
|
14683
|
+
---
|
|
14684
|
+
|
|
14685
|
+
### `retry()`
|
|
14686
|
+
|
|
14687
|
+
```ts
|
|
14688
|
+
retry(id: string): Promise;
|
|
14689
|
+
```
|
|
14690
|
+
|
|
14691
|
+
Moves a dead-letter job back to queued. Returns a discriminated result: `retried`, `not-found`, `not-dead-letter`, or `running`.
|
|
14692
|
+
|
|
14693
|
+
---
|
|
14694
|
+
|
|
14695
|
+
### `remove()`
|
|
14696
|
+
|
|
14697
|
+
```ts
|
|
14698
|
+
remove(id: string): Promise;
|
|
14699
|
+
```
|
|
14700
|
+
|
|
14701
|
+
Deletes a queued or dead-letter job. Returns a discriminated result: `removed`, `not-found`, or `running`.
|
|
14702
|
+
|
|
14703
|
+
---
|
|
14704
|
+
|
|
14705
|
+
### `tap()`
|
|
14706
|
+
|
|
14707
|
+
```ts
|
|
14708
|
+
tap(handler: (event: PostmasterEvent) => void, options?: { signal?: AbortSignal }): () => void;
|
|
14709
|
+
```
|
|
14710
|
+
|
|
14711
|
+
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.
|
|
14712
|
+
|
|
14713
|
+
---
|
|
14714
|
+
|
|
14715
|
+
### `dispose()`
|
|
14716
|
+
|
|
14717
|
+
```ts
|
|
14718
|
+
dispose(): Promise;
|
|
14719
|
+
[Symbol.asyncDispose](): Promise;
|
|
14720
|
+
```
|
|
14721
|
+
|
|
14722
|
+
Aborts owned work, releases all active leases, and tears down subscriptions. Idempotent. Does not dispose the borrowed store.
|
|
14723
|
+
|
|
14724
|
+
## Types
|
|
14725
|
+
|
|
14726
|
+
### `EnqueueOptions`
|
|
14727
|
+
|
|
14728
|
+
```ts
|
|
14729
|
+
interface EnqueueOptions {
|
|
14730
|
+
readonly availableAt?: number;
|
|
14731
|
+
}
|
|
14732
|
+
```
|
|
14733
|
+
|
|
14734
|
+
Options for `enqueue()`. `availableAt` is the earliest epoch timestamp (ms) at which the job may be claimed. Defaults to the Postmaster clock at enqueue time. Past timestamps remain immediately eligible. Postmaster does not guarantee execution at the requested time — only that the job will not be claimed before it. A live processor is required for execution.
|
|
14735
|
+
|
|
14736
|
+
---
|
|
14737
|
+
|
|
14738
|
+
### `JobDefinition`
|
|
14739
|
+
|
|
14740
|
+
```ts
|
|
14741
|
+
interface JobDefinition {
|
|
14742
|
+
readonly version: number;
|
|
14743
|
+
readonly validate?: Validate;
|
|
14744
|
+
readonly key: (payload: T) => string;
|
|
14745
|
+
readonly execute: (payload: T, context: JobContext) => Promise;
|
|
14746
|
+
readonly retry?: RetryPolicy;
|
|
14747
|
+
readonly migrate?: (payload: unknown, fromVersion: number) => unknown;
|
|
14748
|
+
}
|
|
14749
|
+
```
|
|
14750
|
+
|
|
14751
|
+
`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.
|
|
14752
|
+
|
|
14753
|
+
---
|
|
14754
|
+
|
|
14755
|
+
### `Validate`
|
|
14756
|
+
|
|
14757
|
+
```ts
|
|
14758
|
+
type Validate = ((value: unknown) => T) | { parse(value: unknown): T };
|
|
14759
|
+
```
|
|
14760
|
+
|
|
14761
|
+
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.
|
|
14762
|
+
|
|
14763
|
+
---
|
|
14764
|
+
|
|
14765
|
+
### `JobContext`
|
|
14766
|
+
|
|
14767
|
+
```ts
|
|
14768
|
+
interface JobContext {
|
|
14769
|
+
readonly attempt: number;
|
|
14770
|
+
readonly entryId: string;
|
|
14771
|
+
readonly key: string;
|
|
14772
|
+
readonly signal: AbortSignal;
|
|
14773
|
+
}
|
|
14774
|
+
```
|
|
14775
|
+
|
|
14776
|
+
---
|
|
14777
|
+
|
|
14778
|
+
### `RetryPolicy`
|
|
14779
|
+
|
|
14780
|
+
```ts
|
|
14781
|
+
interface RetryPolicy {
|
|
14782
|
+
readonly maxAttempts: number;
|
|
14783
|
+
readonly shouldRetry: (error: unknown, attempt: number) => boolean;
|
|
14784
|
+
readonly delay?: (attempt: number) => number;
|
|
14785
|
+
}
|
|
14786
|
+
```
|
|
14787
|
+
|
|
14788
|
+
`maxAttempts` is total executions including the first. `shouldRetry` is required when retries are enabled. Default delay uses Arsenal's `backoff(attempt)`.
|
|
14789
|
+
|
|
14790
|
+
---
|
|
14791
|
+
|
|
14792
|
+
### `StoredJob`
|
|
14793
|
+
|
|
14794
|
+
```ts
|
|
14795
|
+
interface StoredJob {
|
|
14796
|
+
readonly id: string;
|
|
14797
|
+
readonly name: string;
|
|
14798
|
+
readonly version: number;
|
|
14799
|
+
readonly payload: JsonValue;
|
|
14800
|
+
readonly key: string;
|
|
14801
|
+
readonly status: 'queued' | 'running' | 'dead-letter';
|
|
14802
|
+
readonly attempts: number;
|
|
14803
|
+
readonly createdAt: number;
|
|
14804
|
+
readonly updatedAt: number;
|
|
14805
|
+
readonly availableAt: number;
|
|
14806
|
+
readonly ownerId?: string;
|
|
14807
|
+
readonly leaseExpiresAt?: number;
|
|
14808
|
+
readonly failure?: StoredFailure;
|
|
14809
|
+
}
|
|
14810
|
+
```
|
|
14811
|
+
|
|
14812
|
+
---
|
|
14813
|
+
|
|
14814
|
+
### `StoredFailure`
|
|
14815
|
+
|
|
14816
|
+
```ts
|
|
14817
|
+
interface StoredFailure {
|
|
14818
|
+
readonly name: string;
|
|
14819
|
+
readonly message: string;
|
|
14820
|
+
readonly occurredAt: number;
|
|
14821
|
+
}
|
|
14822
|
+
```
|
|
14823
|
+
|
|
14824
|
+
Only a bounded error name/message/timestamp is persisted. Never persist arbitrary error objects, response bodies, headers, or stacks.
|
|
14825
|
+
|
|
14826
|
+
---
|
|
14827
|
+
|
|
14828
|
+
### `PostmasterEntry`
|
|
14829
|
+
|
|
14830
|
+
```ts
|
|
14831
|
+
type PostmasterEntry = Pick;
|
|
14832
|
+
```
|
|
14833
|
+
|
|
14834
|
+
The public entry view excludes `payload`, `ownerId`, and `leaseExpiresAt`.
|
|
14835
|
+
|
|
14836
|
+
---
|
|
14837
|
+
|
|
14838
|
+
### `PostmasterStore`
|
|
14839
|
+
|
|
14840
|
+
```ts
|
|
14841
|
+
interface PostmasterStore {
|
|
14842
|
+
transact(fn: (tx: StoreTx) => Promise): Promise;
|
|
14843
|
+
list(filter?: EntryFilter): Promise;
|
|
14844
|
+
subscribe(listener: () => void): () => void;
|
|
14845
|
+
dispose(): Promise;
|
|
14846
|
+
readonly disposed: boolean;
|
|
14847
|
+
readonly disposalSignal: AbortSignal;
|
|
14848
|
+
[Symbol.asyncDispose](): Promise;
|
|
14849
|
+
}
|
|
14850
|
+
|
|
14851
|
+
interface StoreTx {
|
|
14852
|
+
get(id: string): Promise;
|
|
14853
|
+
put(entry: StoredJob): Promise;
|
|
14854
|
+
delete(id: string): Promise;
|
|
14855
|
+
findClaimable(now: number): Promise;
|
|
14856
|
+
findNextWake(now: number): Promise;
|
|
14857
|
+
countByStatus(): Promise;
|
|
14858
|
+
}
|
|
14859
|
+
```
|
|
14860
|
+
|
|
14861
|
+
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.
|
|
14862
|
+
|
|
14863
|
+
---
|
|
14864
|
+
|
|
14865
|
+
### `PostmasterEvent`
|
|
14866
|
+
|
|
14867
|
+
```ts
|
|
14868
|
+
type PostmasterEvent =
|
|
14869
|
+
| { readonly type: 'enqueued' | 'started' | 'completed' | 'retry-scheduled' | 'dead-lettered'; readonly entry: PostmasterEntry }
|
|
14870
|
+
| { readonly type: 'removed' | 'lease-lost'; readonly id: string }
|
|
14871
|
+
| { readonly type: 'processor-error'; readonly error: Error }
|
|
14872
|
+
| { readonly type: 'dispose' };
|
|
14873
|
+
```
|
|
14874
|
+
|
|
14875
|
+
---
|
|
14876
|
+
|
|
14877
|
+
### `FlushResult`
|
|
14878
|
+
|
|
14879
|
+
```ts
|
|
14880
|
+
interface FlushResult {
|
|
14881
|
+
readonly processed: number;
|
|
14882
|
+
readonly completed: number;
|
|
14883
|
+
readonly deadLettered: number;
|
|
14884
|
+
readonly retryScheduled: number;
|
|
14885
|
+
}
|
|
14886
|
+
```
|
|
14887
|
+
|
|
14888
|
+
---
|
|
14889
|
+
|
|
14890
|
+
### `RetryResult` / `RemoveResult`
|
|
14891
|
+
|
|
14892
|
+
```ts
|
|
14893
|
+
type RetryResult =
|
|
14894
|
+
| { readonly status: 'not-found' | 'not-dead-letter' | 'running' }
|
|
14895
|
+
| { readonly status: 'retried'; readonly entry: PostmasterEntry };
|
|
14896
|
+
|
|
14897
|
+
type RemoveResult =
|
|
14898
|
+
| { readonly status: 'not-found' | 'running' }
|
|
14899
|
+
| { readonly status: 'removed'; readonly id: string };
|
|
14900
|
+
```
|
|
14901
|
+
|
|
14902
|
+
## Errors
|
|
14903
|
+
|
|
14904
|
+
### `PostmasterError`
|
|
14905
|
+
|
|
14906
|
+
```ts
|
|
14907
|
+
class PostmasterError extends Error {
|
|
14908
|
+
constructor(message: string, options?: ErrorOptions);
|
|
14909
|
+
}
|
|
14910
|
+
```
|
|
14911
|
+
|
|
14912
|
+
Base class for package-defined errors. Use `instanceof PostmasterError` to narrow to the hierarchy. Covers configuration errors, serialization errors, and store failures.
|
|
14913
|
+
|
|
14914
|
+
---
|
|
14915
|
+
|
|
14916
|
+
### `PostmasterDisposedError`
|
|
14917
|
+
|
|
14918
|
+
```ts
|
|
14919
|
+
class PostmasterDisposedError extends PostmasterError {}
|
|
14920
|
+
```
|
|
14921
|
+
|
|
14922
|
+
Thrown when a public method is called after disposal.
|
|
14923
|
+
|
|
14924
|
+
---
|
|
14925
|
+
|
|
14926
|
+
### `PostmasterJobError`
|
|
14927
|
+
|
|
14928
|
+
```ts
|
|
14929
|
+
class PostmasterJobError extends PostmasterError {}
|
|
14930
|
+
```
|
|
14931
|
+
|
|
14932
|
+
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.
|
|
14933
|
+
|
|
14934
|
+
### Usage Guide
|
|
14935
|
+
|
|
14936
|
+
## Basic Usage
|
|
14937
|
+
|
|
14938
|
+
Define typed jobs, create a durable store, enqueue work, and start the processor. Dispose both handles when the owner ends.
|
|
14939
|
+
|
|
14940
|
+
```ts
|
|
14941
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
14942
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
14943
|
+
|
|
14944
|
+
const jobs = defineJobs({
|
|
14945
|
+
createTodo: {
|
|
14946
|
+
version: 1,
|
|
14947
|
+
validate: (v: unknown) => v as { id: string; title: string },
|
|
14948
|
+
key: (p) => p.id,
|
|
14949
|
+
execute: async (payload, { key, signal }) => {
|
|
14950
|
+
await fetch('/api/todos', {
|
|
14951
|
+
method: 'POST',
|
|
14952
|
+
body: JSON.stringify(payload),
|
|
14953
|
+
headers: { 'Idempotency-Key': key },
|
|
14954
|
+
signal,
|
|
14955
|
+
});
|
|
14956
|
+
},
|
|
14957
|
+
},
|
|
14958
|
+
});
|
|
14959
|
+
|
|
14960
|
+
const store = createIndexedDbPostmasterStore({ name: 'my-app-outbox' });
|
|
14961
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
14962
|
+
|
|
14963
|
+
await postmaster.enqueue('createTodo', { id: crypto.randomUUID(), title: 'Buy milk' });
|
|
14964
|
+
await postmaster.start();
|
|
14965
|
+
|
|
14966
|
+
// On page unload:
|
|
14967
|
+
await postmaster.dispose();
|
|
14968
|
+
await store.dispose();
|
|
14969
|
+
```
|
|
14970
|
+
|
|
14971
|
+
The store is borrowed by `createPostmaster()` and is not disposed with the processor. Dispose both explicitly.
|
|
14972
|
+
|
|
14973
|
+
## At-least-once delivery and idempotency
|
|
14974
|
+
|
|
14975
|
+
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.
|
|
14976
|
+
|
|
14977
|
+
```ts
|
|
14978
|
+
const jobs = defineJobs({
|
|
14979
|
+
createTodo: {
|
|
14980
|
+
version: 1,
|
|
14981
|
+
validate: (v: unknown) => v as { id: string; title: string },
|
|
14982
|
+
key: (p) => p.id,
|
|
14983
|
+
execute: async (payload, { key, signal }) => {
|
|
14984
|
+
await fetch('/api/todos', {
|
|
14985
|
+
method: 'POST',
|
|
14986
|
+
body: JSON.stringify(payload),
|
|
14987
|
+
headers: { 'Idempotency-Key': key },
|
|
14988
|
+
signal,
|
|
14989
|
+
});
|
|
14990
|
+
},
|
|
14991
|
+
},
|
|
14992
|
+
});
|
|
14993
|
+
```
|
|
14994
|
+
|
|
14995
|
+
Never assume exactly-once execution. Design handlers so a repeated delivery is safe.
|
|
14996
|
+
|
|
14997
|
+
## Postmaster jobs vs Courier mutations
|
|
14998
|
+
|
|
14999
|
+
Courier performs immediate HTTP requests and cache reconciliation. Postmaster coordinates durable delivery. Use Courier inside a Postmaster job when the write must survive reloads.
|
|
15000
|
+
|
|
15001
|
+
```ts
|
|
15002
|
+
import { createCourier, CourierNetworkError } from '@vielzeug/courier';
|
|
15003
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
15004
|
+
|
|
15005
|
+
const courier = createCourier({ baseUrl: 'https://api.example.com' });
|
|
15006
|
+
|
|
15007
|
+
const jobs = defineJobs({
|
|
15008
|
+
createTodo: {
|
|
15009
|
+
version: 1,
|
|
15010
|
+
validate: (v: unknown) => v as { id: string; title: string },
|
|
15011
|
+
key: (p) => p.id,
|
|
15012
|
+
execute: async (payload, { key, signal }) => {
|
|
15013
|
+
await courier.mutate({
|
|
15014
|
+
request: () =>
|
|
15015
|
+
courier.post('/todos', {
|
|
15016
|
+
body: payload,
|
|
15017
|
+
headers: { 'Idempotency-Key': key },
|
|
15018
|
+
signal,
|
|
15019
|
+
}),
|
|
15020
|
+
invalidateKeys: [['todos']],
|
|
15021
|
+
});
|
|
15022
|
+
},
|
|
15023
|
+
retry: { maxAttempts: 5, shouldRetry: (error) => error instanceof CourierNetworkError },
|
|
15024
|
+
},
|
|
15025
|
+
});
|
|
15026
|
+
```
|
|
15027
|
+
|
|
15028
|
+
Postmaster does not import Courier. The integration happens in your job definitions.
|
|
15029
|
+
|
|
15030
|
+
## Payload and version migration
|
|
15031
|
+
|
|
15032
|
+
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:
|
|
15033
|
+
|
|
15034
|
+
```ts
|
|
15035
|
+
import { s } from '@vielzeug/spell';
|
|
15036
|
+
|
|
15037
|
+
const jobs = defineJobs({
|
|
15038
|
+
createTodo: {
|
|
15039
|
+
version: 2,
|
|
15040
|
+
validate: s.object({ id: s.string(), title: s.string(), priority: s.number().optional() }),
|
|
15041
|
+
key: (p) => p.id,
|
|
15042
|
+
migrate: (payload, fromVersion) => {
|
|
15043
|
+
if (fromVersion === 1) return { ...(payload as { id: string; title: string }), priority: 0 };
|
|
15044
|
+
return payload;
|
|
15045
|
+
},
|
|
15046
|
+
execute: async (payload, { key, signal }) => {
|
|
15047
|
+
await fetch('/api/todos', {
|
|
15048
|
+
method: 'POST',
|
|
15049
|
+
body: JSON.stringify(payload),
|
|
15050
|
+
headers: { 'Idempotency-Key': key },
|
|
15051
|
+
signal,
|
|
15052
|
+
});
|
|
15053
|
+
},
|
|
15054
|
+
},
|
|
15055
|
+
});
|
|
15056
|
+
```
|
|
15057
|
+
|
|
15058
|
+
Unknown job names, incompatible versions, failed migrations, and invalid persisted payloads move to dead-letter rather than being executed.
|
|
15059
|
+
|
|
15060
|
+
## Retry semantics
|
|
15061
|
+
|
|
15062
|
+
Retries are opt-in and explicitly classified. No `retry` block means one attempt followed by dead-letter.
|
|
15063
|
+
|
|
15064
|
+
```ts
|
|
15065
|
+
const jobs = defineJobs({
|
|
15066
|
+
syncTodo: {
|
|
15067
|
+
version: 1,
|
|
15068
|
+
validate: (v: unknown) => v as { id: string },
|
|
15069
|
+
key: (p) => p.id,
|
|
15070
|
+
execute: async (payload, { signal }) => {
|
|
15071
|
+
await fetch(`/api/todos/${payload.id}/sync`, { signal });
|
|
15072
|
+
},
|
|
15073
|
+
retry: {
|
|
15074
|
+
maxAttempts: 5,
|
|
15075
|
+
shouldRetry: (error) => error instanceof TypeError, // network errors only
|
|
15076
|
+
},
|
|
15077
|
+
},
|
|
15078
|
+
});
|
|
15079
|
+
```
|
|
15080
|
+
|
|
15081
|
+
- `maxAttempts` means total executions, including the first.
|
|
15082
|
+
- `shouldRetry` is required when retries are enabled. Postmaster never guesses whether a write is safe to repeat.
|
|
15083
|
+
- Default delay uses Arsenal's deterministic `backoff(attempt)` helper. Override with `delay`.
|
|
15084
|
+
- Delay must be finite and non-negative.
|
|
15085
|
+
- Lifecycle aborts caused by disposal are not classified as job failures.
|
|
15086
|
+
|
|
15087
|
+
## Delayed eligibility
|
|
15088
|
+
|
|
15089
|
+
`enqueue()` accepts an optional `availableAt` timestamp. The job persists immediately but cannot be claimed before that time. Use this for scheduled writes, cooldowns, or any work that must survive a reload but should not run yet.
|
|
15090
|
+
|
|
15091
|
+
```ts
|
|
15092
|
+
await postmaster.enqueue('sendDigest', { userId }, { availableAt: Date.now() + 60_000 });
|
|
15093
|
+
```
|
|
15094
|
+
|
|
15095
|
+
Postmaster does not guarantee execution at `availableAt` — only that the job will not be claimed earlier. A live processor (`start()` or `flush()`) is required for execution. In a browser, a closed page or suspended service worker will run the job when the processor next becomes active. Past timestamps remain immediately eligible. The same mechanism already backs retry delays, so delayed eligibility reuses the existing claim, wake, and persistence paths.
|
|
15096
|
+
|
|
15097
|
+
## Dead-letter recovery
|
|
15098
|
+
|
|
15099
|
+
Jobs that exhaust retries or hit a terminal failure move to dead-letter. Inspect, retry, or remove them.
|
|
15100
|
+
|
|
15101
|
+
```ts
|
|
15102
|
+
const deadLettered = await postmaster.list({ status: 'dead-letter' });
|
|
15103
|
+
|
|
15104
|
+
for (const entry of deadLettered) {
|
|
15105
|
+
console.log(entry.id, entry.name, entry.failure);
|
|
15106
|
+
}
|
|
15107
|
+
|
|
15108
|
+
// Retry a dead-letter job back into the queue.
|
|
15109
|
+
await postmaster.retry(entry.id);
|
|
15110
|
+
|
|
15111
|
+
// Or remove it permanently.
|
|
15112
|
+
await postmaster.remove(entry.id);
|
|
15113
|
+
```
|
|
15114
|
+
|
|
15115
|
+
`retry()` and `remove()` return discriminated results so callers can distinguish `not-found`, `not-dead-letter`, `running`, and successful outcomes without exceptions.
|
|
15116
|
+
|
|
15117
|
+
## Lifecycle and disposal
|
|
15118
|
+
|
|
15119
|
+
`start()` begins background processing. `dispose()` stops claiming new work, aborts owned work, and is idempotent. `flush()` processes every available job synchronously.
|
|
15120
|
+
|
|
15121
|
+
```ts
|
|
15122
|
+
await postmaster.start();
|
|
15123
|
+
// ...on unload
|
|
15124
|
+
await postmaster.dispose();
|
|
15125
|
+
await store.dispose();
|
|
15126
|
+
```
|
|
15127
|
+
|
|
15128
|
+
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.
|
|
15129
|
+
|
|
15130
|
+
## Events
|
|
15131
|
+
|
|
15132
|
+
Tap runtime events for observability. Handler errors are swallowed — observability never affects processing.
|
|
15133
|
+
|
|
15134
|
+
```ts
|
|
15135
|
+
const unsubscribe = postmaster.tap((event) => {
|
|
15136
|
+
switch (event.type) {
|
|
15137
|
+
case 'enqueued':
|
|
15138
|
+
console.log('enqueued', event.entry.id);
|
|
15139
|
+
break;
|
|
15140
|
+
case 'completed':
|
|
15141
|
+
console.log('completed', event.entry.id);
|
|
15142
|
+
break;
|
|
15143
|
+
case 'dead-lettered':
|
|
15144
|
+
console.error('dead-lettered', event.entry.id, event.entry.failure);
|
|
15145
|
+
break;
|
|
15146
|
+
case 'processor-error':
|
|
15147
|
+
console.error('processor error', event.error);
|
|
15148
|
+
break;
|
|
15149
|
+
}
|
|
15150
|
+
});
|
|
15151
|
+
```
|
|
15152
|
+
|
|
15153
|
+
Pass an `AbortSignal` to auto-detach:
|
|
15154
|
+
|
|
15155
|
+
```ts
|
|
15156
|
+
const controller = new AbortController();
|
|
15157
|
+
postmaster.tap(handler, { signal: controller.signal });
|
|
15158
|
+
controller.abort(); // stops tapping
|
|
15159
|
+
```
|
|
15160
|
+
|
|
15161
|
+
## Testing
|
|
15162
|
+
|
|
15163
|
+
Use the in-memory store for deterministic tests.
|
|
15164
|
+
|
|
15165
|
+
```ts
|
|
15166
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
15167
|
+
import { createMemoryPostmasterStore } from '@vielzeug/postmaster/testing';
|
|
15168
|
+
|
|
15169
|
+
const store = createMemoryPostmasterStore();
|
|
15170
|
+
const postmaster = createPostmaster({
|
|
15171
|
+
jobs: defineJobs({
|
|
15172
|
+
send: {
|
|
15173
|
+
version: 1,
|
|
15174
|
+
validate: (v: unknown) => String(v),
|
|
15175
|
+
key: (p) => p,
|
|
15176
|
+
execute: async () => {},
|
|
15177
|
+
},
|
|
15178
|
+
}),
|
|
15179
|
+
store,
|
|
15180
|
+
});
|
|
15181
|
+
|
|
15182
|
+
await postmaster.enqueue('send', 'hello');
|
|
15183
|
+
await postmaster.flush();
|
|
15184
|
+
await postmaster.dispose();
|
|
15185
|
+
```
|
|
15186
|
+
|
|
15187
|
+
Inject a deterministic clock to control retry scheduling.
|
|
15188
|
+
|
|
15189
|
+
```ts
|
|
15190
|
+
let now = 0;
|
|
15191
|
+
const postmaster = createPostmaster({ clock: () => now, jobs, store });
|
|
15192
|
+
```
|
|
15193
|
+
|
|
15194
|
+
## Framework Integration
|
|
15195
|
+
|
|
15196
|
+
Create the Postmaster after the component mounts, start processing, and dispose on unmount.
|
|
15197
|
+
|
|
15198
|
+
```tsx [React]
|
|
15199
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
15200
|
+
import { createPostmaster, defineJobs, type Postmaster } from '@vielzeug/postmaster';
|
|
15201
|
+
import { useEffect } from 'react';
|
|
15202
|
+
|
|
15203
|
+
const jobs = defineJobs({
|
|
15204
|
+
sync: {
|
|
15205
|
+
version: 1,
|
|
15206
|
+
validate: (v: unknown) => v as { id: string },
|
|
15207
|
+
key: (p) => p.id,
|
|
15208
|
+
execute: async (payload, { signal }) => {
|
|
15209
|
+
await fetch(`/api/sync/${payload.id}`, { signal });
|
|
15210
|
+
},
|
|
15211
|
+
},
|
|
15212
|
+
});
|
|
15213
|
+
|
|
15214
|
+
export function OutboxProvider() {
|
|
15215
|
+
useEffect(() => {
|
|
15216
|
+
const store = createIndexedDbPostmasterStore({ name: 'outbox' });
|
|
15217
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
15218
|
+
void postmaster.start();
|
|
15219
|
+
|
|
15220
|
+
return () => {
|
|
15221
|
+
void postmaster.dispose();
|
|
15222
|
+
void store.dispose();
|
|
15223
|
+
};
|
|
15224
|
+
}, []);
|
|
15225
|
+
|
|
15226
|
+
return null;
|
|
15227
|
+
}
|
|
15228
|
+
```
|
|
15229
|
+
|
|
15230
|
+
```vue [Vue 3]
|
|
15231
|
+
|
|
15232
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
15233
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
15234
|
+
import { onMounted, onUnmounted } from 'vue';
|
|
15235
|
+
|
|
15236
|
+
const jobs = defineJobs({
|
|
15237
|
+
sync: {
|
|
15238
|
+
version: 1,
|
|
15239
|
+
validate: (v: unknown) => v as { id: string },
|
|
15240
|
+
key: (p) => p.id,
|
|
15241
|
+
execute: async (payload, { signal }) => {
|
|
15242
|
+
await fetch(`/api/sync/${payload.id}`, { signal });
|
|
15243
|
+
},
|
|
15244
|
+
},
|
|
15245
|
+
});
|
|
15246
|
+
|
|
15247
|
+
let postmaster: ReturnType | undefined;
|
|
15248
|
+
let store: ReturnType | undefined;
|
|
15249
|
+
|
|
15250
|
+
onMounted(() => {
|
|
15251
|
+
store = createIndexedDbPostmasterStore({ name: 'outbox' });
|
|
15252
|
+
postmaster = createPostmaster({ jobs, store });
|
|
15253
|
+
void postmaster.start();
|
|
15254
|
+
});
|
|
15255
|
+
|
|
15256
|
+
onUnmounted(() => {
|
|
15257
|
+
void postmaster?.dispose();
|
|
15258
|
+
void store?.dispose();
|
|
15259
|
+
});
|
|
14196
15260
|
|
|
14197
15261
|
|
|
14198
15262
|
|
|
@@ -14200,92 +15264,117 @@ const open = ref(false);
|
|
|
14200
15264
|
|
|
14201
15265
|
```svelte [Svelte]
|
|
14202
15266
|
|
|
14203
|
-
import
|
|
15267
|
+
import { createIndexedDbPostmasterStore } from '@vielzeug/postmaster/indexeddb';
|
|
15268
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
15269
|
+
import { onMount } from 'svelte';
|
|
14204
15270
|
|
|
14205
|
-
|
|
14206
|
-
|
|
14207
|
-
|
|
15271
|
+
const jobs = defineJobs({
|
|
15272
|
+
sync: {
|
|
15273
|
+
version: 1,
|
|
15274
|
+
validate: (v: unknown) => v as { id: string },
|
|
15275
|
+
key: (p) => p.id,
|
|
15276
|
+
execute: async (payload, { signal }) => {
|
|
15277
|
+
await fetch(`/api/sync/${payload.id}`, { signal });
|
|
15278
|
+
},
|
|
15279
|
+
},
|
|
15280
|
+
});
|
|
15281
|
+
|
|
15282
|
+
onMount(() => {
|
|
15283
|
+
const store = createIndexedDbPostmasterStore({ name: 'outbox' });
|
|
15284
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
15285
|
+
void postmaster.start();
|
|
15286
|
+
|
|
15287
|
+
return () => {
|
|
15288
|
+
void postmaster.dispose();
|
|
15289
|
+
void store.dispose();
|
|
15290
|
+
};
|
|
15291
|
+
});
|
|
14208
15292
|
|
|
14209
15293
|
```
|
|
14210
15294
|
|
|
14211
15295
|
## Working with Other Vielzeug Libraries
|
|
14212
15296
|
|
|
14213
|
-
###
|
|
15297
|
+
### Postmaster + Courier
|
|
14214
15298
|
|
|
14215
|
-
|
|
15299
|
+
Use Courier inside job handlers for HTTP transport and cache invalidation. Postmaster coordinates delivery; Courier performs the request.
|
|
14216
15300
|
|
|
14217
15301
|
```ts
|
|
14218
|
-
import {
|
|
14219
|
-
import {
|
|
15302
|
+
import { createCourier, CourierNetworkError } from '@vielzeug/courier';
|
|
15303
|
+
import { createPostmaster, defineJobs } from '@vielzeug/postmaster';
|
|
14220
15304
|
|
|
14221
|
-
|
|
14222
|
-
const theme = signal('light');
|
|
14223
|
-
const isDark = computed(() => theme.value === 'dark');
|
|
15305
|
+
const courier = createCourier({ baseUrl: 'https://api.example.com' });
|
|
14224
15306
|
|
|
14225
|
-
|
|
14226
|
-
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
|
|
15307
|
+
const jobs = defineJobs({
|
|
15308
|
+
createTodo: {
|
|
15309
|
+
version: 1,
|
|
15310
|
+
validate: (v: unknown) => v as { id: string; title: string },
|
|
15311
|
+
key: (p) => p.id,
|
|
15312
|
+
execute: async (payload, { key, signal }) => {
|
|
15313
|
+
await courier.mutate({
|
|
15314
|
+
request: () =>
|
|
15315
|
+
courier.post('/todos', {
|
|
15316
|
+
body: payload,
|
|
15317
|
+
headers: { 'Idempotency-Key': key },
|
|
15318
|
+
signal,
|
|
15319
|
+
}),
|
|
15320
|
+
invalidateKeys: [['todos']],
|
|
15321
|
+
});
|
|
15322
|
+
},
|
|
15323
|
+
retry: { maxAttempts: 5, shouldRetry: (e) => e instanceof CourierNetworkError },
|
|
14233
15324
|
},
|
|
14234
15325
|
});
|
|
14235
15326
|
```
|
|
14236
15327
|
|
|
14237
|
-
###
|
|
15328
|
+
### Postmaster + Sentinel
|
|
14238
15329
|
|
|
14239
|
-
|
|
14240
|
-
custom element to native `ElementInternals` without imposing submission, validation, or dirty-state policy.
|
|
15330
|
+
Flush the outbox when the network returns. Sentinel reports online state; Postmaster does the rest.
|
|
14241
15331
|
|
|
14242
15332
|
```ts
|
|
14243
|
-
import {
|
|
14244
|
-
import {
|
|
15333
|
+
import { createNetwork } from '@vielzeug/sentinel';
|
|
15334
|
+
import { createPostmaster } from '@vielzeug/postmaster';
|
|
14245
15335
|
|
|
14246
|
-
|
|
14247
|
-
|
|
14248
|
-
const form = createForm({ initialValues: { email: '' } });
|
|
15336
|
+
const network = createNetwork();
|
|
15337
|
+
const postmaster = createPostmaster({ jobs, store });
|
|
14249
15338
|
|
|
14250
|
-
|
|
14251
|
-
|
|
14252
|
-
event.preventDefault();
|
|
14253
|
-
void form.submit(async (values) => {
|
|
14254
|
-
console.log(values);
|
|
14255
|
-
});
|
|
14256
|
-
}}>
|
|
14257
|
-
|
|
14258
|
-
|
|
14259
|
-
`;
|
|
14260
|
-
},
|
|
15339
|
+
const unsubscribe = network.subscribe(() => {
|
|
15340
|
+
if (network.value.online) void postmaster.flush();
|
|
14261
15341
|
});
|
|
15342
|
+
|
|
15343
|
+
// On teardown:
|
|
15344
|
+
unsubscribe();
|
|
15345
|
+
network.dispose();
|
|
15346
|
+
await postmaster.dispose();
|
|
14262
15347
|
```
|
|
14263
15348
|
|
|
15349
|
+
### Postmaster + Vault
|
|
15350
|
+
|
|
15351
|
+
The IndexedDB adapter is built on Vault. Use Vault directly for unrelated storage; the Postmaster store owns its own database name.
|
|
15352
|
+
|
|
14264
15353
|
## Best Practices
|
|
14265
15354
|
|
|
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`.
|
|
15355
|
+
- **Derive** a stable idempotency key from every job payload and send it with the remote write.
|
|
15356
|
+
- **Dispose** both the processor and the store explicitly; the processor does not own the store.
|
|
15357
|
+
- **Classify** retryable errors explicitly with `shouldRetry`; never let Postmaster guess.
|
|
15358
|
+
- **Migrate** persisted payloads when job versions change; test migrations against stored fixtures.
|
|
15359
|
+
- **Inspect** the dead-letter queue regularly and retry or remove terminal failures.
|
|
15360
|
+
- **Avoid** persisting sensitive data in payloads or failure messages; IndexedDB is per-origin but not encrypted.
|
|
15361
|
+
- **Flush** the outbox when Sentinel reports the network returns.
|
|
15362
|
+
- **Test** with the in-memory store and a deterministic clock for reproducible retry timing.
|
|
14276
15363
|
|
|
14277
15364
|
### Examples
|
|
14278
15365
|
|
|
14279
15366
|
## Examples
|
|
14280
15367
|
|
|
14281
|
-
- [
|
|
14282
|
-
- [
|
|
14283
|
-
- [
|
|
14284
|
-
- [
|
|
14285
|
-
- [
|
|
14286
|
-
|
|
14287
|
-
|
|
14288
|
-
|
|
15368
|
+
- [Queue Offline Courier Mutations](./examples/queue-offline-courier-mutations.md)
|
|
15369
|
+
- [Resume When Network Returns](./examples/resume-when-network-returns.md)
|
|
15370
|
+
- [Delayed Eligibility](./examples/delayed-eligibility.md)
|
|
15371
|
+
- [Recover Dead-Letter Jobs](./examples/recover-dead-letter-jobs.md)
|
|
15372
|
+
- [Service Worker Background Sync](./examples/service-worker-background-sync.md)
|
|
15373
|
+
|
|
15374
|
+
### REPL Examples
|
|
15375
|
+
|
|
15376
|
+
- defineJobs - Basic Outbox (id: `define-jobs`)
|
|
15377
|
+
- delayedEnqueue - Delayed Eligibility (id: `delayed-enqueue`)
|
|
14289
15378
|
|
|
14290
15379
|
---
|
|
14291
15380
|
|
|
@@ -17067,7 +18156,10 @@ type Schema = {
|
|
|
17067
18156
|
|
|
17068
18157
|
const pulse = createPulse('wss://api.example.com/ws', {
|
|
17069
18158
|
reconnect: true,
|
|
17070
|
-
|
|
18159
|
+
});
|
|
18160
|
+
pulse.tap((event) => {
|
|
18161
|
+
if (event.type === 'error') console.error(event.error);
|
|
18162
|
+
if (event.type === 'status-change') console.log('status:', event.status);
|
|
17071
18163
|
});
|
|
17072
18164
|
const chat = pulse.channel('chat');
|
|
17073
18165
|
const lobby = pulse.room('lobby');
|
|
@@ -17091,7 +18183,7 @@ pulse.dispose();
|
|
|
17091
18183
|
- **`room()`** — named, schema-bound ref-counted room scopes with optional reactive presence. The first scope sends `join`; the last disposal sends `leave`.
|
|
17092
18184
|
- **`reconnect`** — ordered restoration of channel subscriptions, room memberships, and local presence state.
|
|
17093
18185
|
- **`transform`** — one synchronous transform or filter for application messages.
|
|
17094
|
-
- **`
|
|
18186
|
+
- **`tap()`** — subscribe to lifecycle events (status changes, errors, disposal) via a typed `PulseEvent` stream.
|
|
17095
18187
|
- **`heartbeat`** — ping/pong liveness detection that uses the same reconnect controller.
|
|
17096
18188
|
- **`status` and `rooms`** — ripple readables for transport and confirmed membership state.
|
|
17097
18189
|
|
|
@@ -17120,7 +18212,7 @@ pulse.dispose();
|
|
|
17120
18212
|
| `PulseChannel` | Scoped channel namespace with independent disposal. | Sync methods, async `wait()` | Each call returns a new scope; ref-counted subscription. |
|
|
17121
18213
|
| `RoomScope` | Ref-counted room membership with optional presence. | Sync methods, async `joined` | `joined` rejects on transport close or timeout. |
|
|
17122
18214
|
| `PulseSchema` | Declares server/client events, channels, and rooms. | Type-only | Infer all named scope types from this schema. |
|
|
17123
|
-
| `PulseOptions` | Configuration: heartbeat, reconnect, transform
|
|
18215
|
+
| `PulseOptions` | Configuration: heartbeat, reconnect, transform. | Type-only | `reconnect` and `heartbeat` default to `false`. |
|
|
17124
18216
|
| `PulseError` | Base class for all Pulse errors. | Runtime | Check `instanceof` against subclasses. |
|
|
17125
18217
|
|
|
17126
18218
|
## Package Entry Point
|
|
@@ -17183,7 +18275,6 @@ Declare all protocol surfaces once at construction. Named scopes infer their typ
|
|
|
17183
18275
|
```ts
|
|
17184
18276
|
type PulseOptions = {
|
|
17185
18277
|
heartbeat?: boolean | HeartbeatOptions;
|
|
17186
|
-
onError?: (error: PulseError) => void;
|
|
17187
18278
|
protocols?: string | string[];
|
|
17188
18279
|
reconnect?: boolean | ReconnectOptions;
|
|
17189
18280
|
transform?: OutgoingTransform;
|
|
@@ -17193,7 +18284,6 @@ type PulseOptions = {
|
|
|
17193
18284
|
| Option | Type | Default | Description |
|
|
17194
18285
|
| --- | --- | --- | --- |
|
|
17195
18286
|
| `heartbeat` | `boolean \| HeartbeatOptions` | `false` | Ping/pong keep-alive. |
|
|
17196
|
-
| `onError` | `(error: PulseError) => void` | — | Receives typed transport and protocol errors. |
|
|
17197
18287
|
| `protocols` | `string \| string[]` | — | Sub-protocols passed to the WebSocket constructor. |
|
|
17198
18288
|
| `reconnect` | `boolean \| ReconnectOptions` | `false` | Auto-reconnect on unexpected close. |
|
|
17199
18289
|
| `transform` | `OutgoingTransform` | — | Transform or filter outgoing application messages. |
|
|
@@ -17283,6 +18373,9 @@ type Pulse = {
|
|
|
17283
18373
|
// Status
|
|
17284
18374
|
readonly status: Readable;
|
|
17285
18375
|
|
|
18376
|
+
// Tap
|
|
18377
|
+
tap(handler: (event: PulseEvent) => void, options?: { signal?: AbortSignal }): () => void;
|
|
18378
|
+
|
|
17286
18379
|
[Symbol.dispose](): void;
|
|
17287
18380
|
};
|
|
17288
18381
|
```
|
|
@@ -17331,6 +18424,42 @@ Reactive set of rooms the client is currently a confirmed member of.
|
|
|
17331
18424
|
|
|
17332
18425
|
Reactive connection status: `'connecting' | 'open' | 'reconnecting' | 'closed'`.
|
|
17333
18426
|
|
|
18427
|
+
### `tap(handler, options?)`
|
|
18428
|
+
|
|
18429
|
+
Subscribes to lifecycle events emitted by the Pulse instance. The handler receives a discriminated-union `PulseEvent`. Returns an unsubscribe function.
|
|
18430
|
+
|
|
18431
|
+
| Parameter | Type | Description |
|
|
18432
|
+
| --- | --- | --- |
|
|
18433
|
+
| `handler` | `(event: PulseEvent) => void` | Called for each lifecycle event. |
|
|
18434
|
+
| `options.signal` | `AbortSignal` | Optional signal to stop the subscription. |
|
|
18435
|
+
|
|
18436
|
+
```ts
|
|
18437
|
+
const pulse = createPulse(url, { reconnect: true });
|
|
18438
|
+
pulse.tap((event) => {
|
|
18439
|
+
if (event.type === 'error') console.error(event.error);
|
|
18440
|
+
if (event.type === 'status-change') console.log('status:', event.status);
|
|
18441
|
+
});
|
|
18442
|
+
```
|
|
18443
|
+
|
|
18444
|
+
---
|
|
18445
|
+
|
|
18446
|
+
## `PulseEvent`
|
|
18447
|
+
|
|
18448
|
+
```ts
|
|
18449
|
+
type PulseEvent =
|
|
18450
|
+
| { type: 'status-change'; status: PulseStatus }
|
|
18451
|
+
| { type: 'error'; error: PulseError }
|
|
18452
|
+
| { type: 'dispose' };
|
|
18453
|
+
```
|
|
18454
|
+
|
|
18455
|
+
A discriminated union of lifecycle events emitted by a `Pulse` instance. Inspect `event.type` to narrow the payload.
|
|
18456
|
+
|
|
18457
|
+
| `type` | Payload | When |
|
|
18458
|
+
| --- | --- | --- |
|
|
18459
|
+
| `status-change` | `status: PulseStatus` | The connection status transitions. |
|
|
18460
|
+
| `error` | `error: PulseError` | A typed transport or protocol error occurs. |
|
|
18461
|
+
| `dispose` | — | The instance is disposed. |
|
|
18462
|
+
|
|
17334
18463
|
---
|
|
17335
18464
|
|
|
17336
18465
|
## `PulseChannel`
|
|
@@ -17561,7 +18690,11 @@ type Schema = {
|
|
|
17561
18690
|
const pulse = createPulse('wss://api.example.com/ws', {
|
|
17562
18691
|
reconnect: { delay: 1_000, maxAttempts: 5 },
|
|
17563
18692
|
heartbeat: { interval: 30_000, timeout: 5_000 },
|
|
17564
|
-
|
|
18693
|
+
});
|
|
18694
|
+
|
|
18695
|
+
pulse.tap((event) => {
|
|
18696
|
+
if (event.type === 'error') console.error(event.error);
|
|
18697
|
+
if (event.type === 'status-change') console.log('status:', event.status);
|
|
17565
18698
|
});
|
|
17566
18699
|
|
|
17567
18700
|
try {
|
|
@@ -17733,13 +18866,17 @@ Disposal is idempotent. It closes the connection, rejects pending room joins, cl
|
|
|
17733
18866
|
|
|
17734
18867
|
```ts
|
|
17735
18868
|
const pulse = createPulse('wss://api.example.com/ws', {
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
18869
|
+
reconnect: true,
|
|
18870
|
+
});
|
|
18871
|
+
|
|
18872
|
+
pulse.tap((event) => {
|
|
18873
|
+
if (event.type === 'error') {
|
|
18874
|
+
if (event.error instanceof PulseConnectionError) {
|
|
18875
|
+
console.error('Connection error:', event.error);
|
|
18876
|
+
} else if (event.error instanceof PulseProtocolError) {
|
|
18877
|
+
console.error('Protocol error:', event.error);
|
|
17741
18878
|
}
|
|
17742
|
-
}
|
|
18879
|
+
}
|
|
17743
18880
|
});
|
|
17744
18881
|
```
|
|
17745
18882
|
|
|
@@ -17758,7 +18895,7 @@ const pulse = createPulse('wss://api.example.com/ws', {
|
|
|
17758
18895
|
- Define the full schema at `createPulse()` so named scopes are type-safe without per-call generics.
|
|
17759
18896
|
- Use `using` declarations for channel and room scopes so disposal is automatic at block exit.
|
|
17760
18897
|
- Always call `dispose()` when done — it closes the connection, rejects pending joins, and clears listeners.
|
|
17761
|
-
-
|
|
18898
|
+
- Call `tap()` to observe lifecycle events; Pulse reports transport and protocol errors there rather than throwing asynchronously.
|
|
17762
18899
|
- Read `pulse.rooms` for post-reconnect membership; `joined` rejects on transport close.
|
|
17763
18900
|
- Set a `timeout` on room scopes when the server may never confirm membership.
|
|
17764
18901
|
- Keep `transform` synchronous; resolve async policy decisions before calling `send()`.
|
|
@@ -21258,7 +22395,7 @@ console.log(results[0]?.item.name); // Ada Lovelace
|
|
|
21258
22395
|
- Incremental updates — `add()` / `remove()` / `reindex()` patch individual items in O(field_length)
|
|
21259
22396
|
- `onMutate()` — Subscribe to index mutations; powers `createSearch()`'s reactivity and bulk reconciliation
|
|
21260
22397
|
- `segmentWords()` — Split unsegmented-script text (CJK, Thai, ...) into words via native `Intl.Segmenter`
|
|
21261
|
-
-
|
|
22398
|
+
- Event subscription via `search.tap()` — observe `query`/`isSearching`/`results`/`dispose` transitions; returns an unsubscribe function
|
|
21262
22399
|
|
|
21263
22400
|
## Documentation
|
|
21264
22401
|
|
|
@@ -21294,14 +22431,13 @@ console.log(results[0]?.item.name); // Ada Lovelace
|
|
|
21294
22431
|
| `toSearchMatcher()` | Adapt `ScoutIndex` to Sourcerer's `match` callback | Sync | Recomputes cached query matches after index mutation |
|
|
21295
22432
|
| `toFilterPredicate()` | Snapshot predicate from a one-time query | Sync | Re-call when query or corpus changes |
|
|
21296
22433
|
| `segmentWords()` | Split unsegmented-script text (CJK, Thai, ...) into words | Sync | Uses native `Intl.Segmenter` — not applied inside `tokenize()` itself (see Pitfalls) |
|
|
21297
|
-
| `
|
|
22434
|
+
| `SearchState.tap()` | Subscribe to `query`/`isSearching`/`results`/`dispose` events | Sync | Returns an unsubscribe function; pass `{ signal }` to tie to an external lifecycle |
|
|
21298
22435
|
|
|
21299
22436
|
## Package Entry Point
|
|
21300
22437
|
|
|
21301
22438
|
| Import | Purpose |
|
|
21302
22439
|
| --- | --- |
|
|
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) |
|
|
22440
|
+
| `@vielzeug/scout` | All exports — index/search/highlighting/adapters, `ScoutConfigurationError`, `ScoutDisposedError`, `ScoutError`, `ScoutEvent`, and all types |
|
|
21305
22441
|
|
|
21306
22442
|
---
|
|
21307
22443
|
|
|
@@ -21454,6 +22590,7 @@ function createSearch(index: ScoutIndex, options?: CreateSearchOptions): SearchS
|
|
|
21454
22590
|
| `disposed` | `boolean` | `true` after `dispose()` has been called. |
|
|
21455
22591
|
| `clear()` | `() => void` | Resets query, cancels debounce, clears results synchronously. |
|
|
21456
22592
|
| `dispose()` | `() => void` | Releases all reactive subscriptions. |
|
|
22593
|
+
| `tap()` | `(handler, options?) => () => void` | Subscribe to `ScoutEvent` transitions; returns an unsubscribe function. |
|
|
21457
22594
|
| `[Symbol.dispose]()` | `() => void` | `using`-compatible disposal. |
|
|
21458
22595
|
|
|
21459
22596
|
**Example**
|
|
@@ -21664,35 +22801,39 @@ const index = createIndex(documents, {
|
|
|
21664
22801
|
|
|
21665
22802
|
---
|
|
21666
22803
|
|
|
21667
|
-
## `
|
|
22804
|
+
## `search.tap(handler, options?)`
|
|
22805
|
+
|
|
22806
|
+
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
22807
|
|
|
21669
22808
|
```ts
|
|
21670
|
-
|
|
22809
|
+
tap(
|
|
22810
|
+
handler: (event: ScoutEvent) => void,
|
|
22811
|
+
options?: { signal?: AbortSignal },
|
|
22812
|
+
): () => void
|
|
21671
22813
|
```
|
|
21672
22814
|
|
|
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
22815
|
**Example**
|
|
21678
22816
|
|
|
21679
22817
|
```ts
|
|
21680
22818
|
import { createIndex, createSearch } from '@vielzeug/scout';
|
|
21681
|
-
import { debugSearch } from '@vielzeug/scout/devtools';
|
|
21682
22819
|
|
|
21683
22820
|
const index = createIndex([{ name: 'Ada Lovelace' }], { fields: ['name'] });
|
|
21684
22821
|
const search = createSearch(index);
|
|
21685
|
-
|
|
22822
|
+
|
|
22823
|
+
const unsubscribe = search.tap((event) => {
|
|
22824
|
+
if (event.type === 'query-change') console.debug('query:', event.query);
|
|
22825
|
+
if (event.type === 'results-change') console.debug('results:', event.results.length);
|
|
22826
|
+
});
|
|
21686
22827
|
|
|
21687
22828
|
search.query.value = 'alice';
|
|
21688
|
-
//
|
|
21689
|
-
//
|
|
21690
|
-
// [scout:search] isSearching -> false
|
|
21691
|
-
// [scout:search] results -> 1 item(s)
|
|
22829
|
+
// query: alice
|
|
22830
|
+
// results: 1
|
|
21692
22831
|
|
|
21693
|
-
|
|
22832
|
+
unsubscribe();
|
|
21694
22833
|
```
|
|
21695
22834
|
|
|
22835
|
+
If your queries may carry PII (names, emails, medical/financial terms typed by end users), don't log `query-change` events in production.
|
|
22836
|
+
|
|
21696
22837
|
---
|
|
21697
22838
|
|
|
21698
22839
|
## Types
|
|
@@ -21778,12 +22919,32 @@ type SearchState = {
|
|
|
21778
22919
|
readonly disposed: boolean;
|
|
21779
22920
|
clear(): void;
|
|
21780
22921
|
dispose(): void;
|
|
22922
|
+
tap(handler: (event: ScoutEvent) => void, options?: { signal?: AbortSignal }): () => void;
|
|
21781
22923
|
[Symbol.dispose](): void;
|
|
21782
22924
|
};
|
|
21783
22925
|
```
|
|
21784
22926
|
|
|
21785
22927
|
See `createSearch()` above for member descriptions.
|
|
21786
22928
|
|
|
22929
|
+
### `ScoutEvent`
|
|
22930
|
+
|
|
22931
|
+
Discriminated union of events emitted by `SearchState.tap()`. Each variant carries a `type` discriminant; narrow with a `switch` or `if` on `event.type`.
|
|
22932
|
+
|
|
22933
|
+
```ts
|
|
22934
|
+
type ScoutEvent =
|
|
22935
|
+
| { type: 'query-change'; query: string }
|
|
22936
|
+
| { type: 'searching-change'; isSearching: boolean }
|
|
22937
|
+
| { type: 'results-change'; results: readonly SearchResult[] }
|
|
22938
|
+
| { type: 'dispose' };
|
|
22939
|
+
```
|
|
22940
|
+
|
|
22941
|
+
| `type` | Payload | Emitted when |
|
|
22942
|
+
| --- | --- | --- |
|
|
22943
|
+
| `query-change` | `query: string` | The writable `query` signal's value changes. |
|
|
22944
|
+
| `searching-change` | `isSearching: boolean` | The debounce window opens (`true`) or closes (`false`). |
|
|
22945
|
+
| `results-change` | `results: readonly SearchResult[]` | Committed results change after debounce. |
|
|
22946
|
+
| `dispose` | — | `dispose()` is called on the `SearchState`. |
|
|
22947
|
+
|
|
21787
22948
|
### `ReactiveSearch`
|
|
21788
22949
|
|
|
21789
22950
|
```ts
|
|
@@ -22126,25 +23287,29 @@ const parts = highlight(result.item.name, nameMatch?.ranges ?? []);
|
|
|
22126
23287
|
|
|
22127
23288
|
## Debug Logging
|
|
22128
23289
|
|
|
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.
|
|
23290
|
+
`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
23291
|
|
|
22133
23292
|
```ts
|
|
22134
|
-
import {
|
|
23293
|
+
import { createIndex, createSearch } from '@vielzeug/scout';
|
|
22135
23294
|
|
|
22136
23295
|
const search = createSearch(index, { debounce: 150 });
|
|
22137
|
-
const
|
|
23296
|
+
const unsubscribe = search.tap((event) => {
|
|
23297
|
+
if (event.type === 'query-change') console.debug('query:', event.query);
|
|
23298
|
+
if (event.type === 'searching-change') console.debug('isSearching:', event.isSearching);
|
|
23299
|
+
if (event.type === 'results-change') console.debug('results:', event.results.length);
|
|
23300
|
+
});
|
|
22138
23301
|
|
|
22139
23302
|
search.query.value = 'alice';
|
|
22140
|
-
//
|
|
22141
|
-
//
|
|
22142
|
-
//
|
|
22143
|
-
//
|
|
23303
|
+
// query: alice
|
|
23304
|
+
// isSearching: true
|
|
23305
|
+
// isSearching: false
|
|
23306
|
+
// results: 1
|
|
22144
23307
|
|
|
22145
|
-
|
|
23308
|
+
unsubscribe();
|
|
22146
23309
|
```
|
|
22147
23310
|
|
|
23311
|
+
`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.
|
|
23312
|
+
|
|
22148
23313
|
## Framework Integration
|
|
22149
23314
|
|
|
22150
23315
|
```tsx [React]
|
|
@@ -23543,7 +24708,7 @@ interface GridVirtualizer {
|
|
|
23543
24708
|
|
|
23544
24709
|
| Class | Thrown when | Notable properties |
|
|
23545
24710
|
| --- | --- | --- |
|
|
23546
|
-
| `ScrollError` | Base class for every Scroll error. | `ScrollError
|
|
24711
|
+
| `ScrollError` | Base class for every Scroll error. | Use `instanceof ScrollError` to narrow unknown errors narrows errors from this package. |
|
|
23547
24712
|
| `ScrollConfigurationError` | A constructor or `update()` receives invalid static configuration. | Extends `ScrollError`; malformed JavaScript values also use this class. |
|
|
23548
24713
|
| `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
24714
|
|
|
@@ -24577,9 +25742,9 @@ const stopObserving = observeViewport();
|
|
|
24577
25742
|
|
|
24578
25743
|
## Documentation
|
|
24579
25744
|
|
|
24580
|
-
- [**Usage Guide**](./usage.md)
|
|
24581
|
-
- [**API Reference**](./api.md)
|
|
24582
|
-
- [**Examples**](./examples.md)
|
|
25745
|
+
- [**Usage Guide**](./usage.md)
|
|
25746
|
+
- [**API Reference**](./api.md)
|
|
25747
|
+
- [**Examples**](./examples.md)
|
|
24583
25748
|
|
|
24584
25749
|
## See Also
|
|
24585
25750
|
|
|
@@ -26167,7 +27332,7 @@ if (!result.success) {
|
|
|
26167
27332
|
|
|
26168
27333
|
## Errors
|
|
26169
27334
|
|
|
26170
|
-
- `SpellError` — base class. Use `SpellError
|
|
27335
|
+
- `SpellError` — base class. Use `instanceof SpellError` for cross-boundary narrowing.
|
|
26171
27336
|
- `SpellValidationError` — validation failure with `issues`, `bestMatch()`, `messagesAt()`, `flatten()`, and `flattenFirst()`.
|
|
26172
27337
|
- `SpellDefinitionError` — schema cannot create portable definition.
|
|
26173
27338
|
|
|
@@ -27505,10 +28670,10 @@ try {
|
|
|
27505
28670
|
- `/memory`, `/local-storage`, and `/session-storage` return portable `VaultStore` instances without loading other adapters.
|
|
27506
28671
|
- `observe()` emits current and changed table snapshots.
|
|
27507
28672
|
- `ttl` creates validated expiration durations.
|
|
27508
|
-
- `/indexeddb` returns `
|
|
28673
|
+
- `/indexeddb` returns `TransactionalVaultStore` with `batch()` and `iterate()`.
|
|
27509
28674
|
- `createSQLite()` is an opt-in, driver-neutral subpath for Node, Bun, and Deno SQLite drivers.
|
|
27510
28675
|
- `/indexeddb` also exports `defineMigration()` for schema upgrades.
|
|
27511
|
-
- `
|
|
28676
|
+
- `pruneExpired()` removes stale TTL entries on demand.
|
|
27512
28677
|
|
|
27513
28678
|
## Documentation
|
|
27514
28679
|
|
|
@@ -27533,9 +28698,10 @@ try {
|
|
|
27533
28698
|
| `createLocalStorage()` / `createSessionStorage()` | Web Storage-backed portable stores | Async API | Available only where the corresponding Web API exists |
|
|
27534
28699
|
| `createIndexedDB()` | Browser transactions and cursor iteration | Async API | Import from `/indexeddb` |
|
|
27535
28700
|
| `createSQLite()` | Driver-neutral SQLite store | Async API over a synchronous driver | Import from `/sqlite` |
|
|
28701
|
+
| `defineMigration()` | Declarative IndexedDB schema upgrade | Sync | Import from `/indexeddb` |
|
|
27536
28702
|
| `table()` | Typed record schema | Sync | The key field must be a string or finite number |
|
|
27537
28703
|
| `ttl` | Valid expiration durations | Sync | Durations must be positive |
|
|
27538
|
-
| `
|
|
28704
|
+
| `isExpired()` | Check an expiration timestamp | Sync | Returns `false` when no expiry is set |
|
|
27539
28705
|
|
|
27540
28706
|
## Package Entry Points
|
|
27541
28707
|
|
|
@@ -27622,23 +28788,20 @@ if (isExpired(record.expiresAt)) console.log('expired');
|
|
|
27622
28788
|
|
|
27623
28789
|
## Factories
|
|
27624
28790
|
|
|
27625
|
-
All factory options accept `schema
|
|
28791
|
+
All factory options accept `schema` and optional `validators`. The root entry does not export any factory.
|
|
27626
28792
|
|
|
27627
28793
|
### `createMemory()`
|
|
27628
28794
|
|
|
27629
28795
|
```ts
|
|
27630
|
-
function createMemory(options:
|
|
27631
|
-
name?: string;
|
|
27632
|
-
schema: S;
|
|
27633
|
-
} & BaseAdapterOptions): VaultStore;
|
|
28796
|
+
function createMemory(options: BaseAdapterOptions): VaultStore;
|
|
27634
28797
|
```
|
|
27635
28798
|
|
|
27636
|
-
Creates an in-memory portable store.
|
|
28799
|
+
Creates an in-memory portable store.
|
|
27637
28800
|
|
|
27638
28801
|
| Parameter | Description |
|
|
27639
28802
|
| --- | --- |
|
|
27640
28803
|
| `schema` | Tables created by `table()` |
|
|
27641
|
-
| `
|
|
28804
|
+
| `validators` | Optional per-table validators with a `parse(value): T` method |
|
|
27642
28805
|
|
|
27643
28806
|
**Returns:** `VaultStore`.
|
|
27644
28807
|
|
|
@@ -27654,20 +28817,20 @@ const store = createMemory({ schema: { users: table('id') } });
|
|
|
27654
28817
|
### `createLocalStorage()`
|
|
27655
28818
|
|
|
27656
28819
|
```ts
|
|
27657
|
-
function createLocalStorage(options: {
|
|
28820
|
+
function createLocalStorage(options: BaseAdapterOptions & {
|
|
27658
28821
|
name: string;
|
|
27659
28822
|
onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';
|
|
27660
|
-
|
|
27661
|
-
} & BaseAdapterOptions): VaultStore;
|
|
28823
|
+
}): VaultStore;
|
|
27662
28824
|
```
|
|
27663
28825
|
|
|
27664
28826
|
Creates a namespaced `localStorage` store.
|
|
27665
28827
|
|
|
27666
28828
|
| Parameter | Description |
|
|
27667
28829
|
| --- | --- |
|
|
28830
|
+
| `schema` | Tables created by `table()` |
|
|
28831
|
+
| `validators` | Optional per-table validators |
|
|
27668
28832
|
| `name` | Required storage namespace |
|
|
27669
28833
|
| `onQuotaExceeded` | Handles a Web Storage quota error; returning `'ignore'` drops that write |
|
|
27670
|
-
| `schema` | Tables created by `table()` |
|
|
27671
28834
|
|
|
27672
28835
|
**Returns:** `VaultStore`.
|
|
27673
28836
|
|
|
@@ -27683,11 +28846,10 @@ const store = createLocalStorage({ name: 'app', schema: { settings: table('id')
|
|
|
27683
28846
|
### `createSessionStorage()`
|
|
27684
28847
|
|
|
27685
28848
|
```ts
|
|
27686
|
-
function createSessionStorage(options: {
|
|
28849
|
+
function createSessionStorage(options: BaseAdapterOptions & {
|
|
27687
28850
|
name: string;
|
|
27688
28851
|
onQuotaExceeded?: (table: keyof S, error: VaultQuotaError) => 'ignore' | 'throw';
|
|
27689
|
-
|
|
27690
|
-
} & BaseAdapterOptions): VaultStore;
|
|
28852
|
+
}): VaultStore;
|
|
27691
28853
|
```
|
|
27692
28854
|
|
|
27693
28855
|
Creates a namespaced `sessionStorage` store. Its options and return type match `createLocalStorage()`.
|
|
@@ -27706,24 +28868,24 @@ const store = createSessionStorage({ name: 'checkout', schema: { cart: table('id
|
|
|
27706
28868
|
### `createIndexedDB()`
|
|
27707
28869
|
|
|
27708
28870
|
```ts
|
|
27709
|
-
function createIndexedDB(options: {
|
|
28871
|
+
function createIndexedDB(options: BaseAdapterOptions & {
|
|
27710
28872
|
migrate?: MigrationFn;
|
|
27711
28873
|
name: string;
|
|
27712
|
-
schema: S;
|
|
27713
28874
|
version?: number;
|
|
27714
|
-
}
|
|
28875
|
+
}): TransactionalVaultStore;
|
|
27715
28876
|
```
|
|
27716
28877
|
|
|
27717
28878
|
Creates an IndexedDB store with atomic batches, lazy cursor iteration, and optional schema migrations.
|
|
27718
28879
|
|
|
27719
28880
|
| Parameter | Description |
|
|
27720
28881
|
| --- | --- |
|
|
27721
|
-
| `name` | Required database name |
|
|
27722
28882
|
| `schema` | Tables and IndexedDB secondary indexes |
|
|
28883
|
+
| `validators` | Optional per-table validators |
|
|
28884
|
+
| `name` | Required database name |
|
|
27723
28885
|
| `version` | Positive schema version; defaults to `1` |
|
|
27724
28886
|
| `migrate` | Synchronous upgrade callback for version changes |
|
|
27725
28887
|
|
|
27726
|
-
**Returns:** `
|
|
28888
|
+
**Returns:** `TransactionalVaultStore`.
|
|
27727
28889
|
|
|
27728
28890
|
```ts
|
|
27729
28891
|
import { table } from '@vielzeug/vault';
|
|
@@ -27737,19 +28899,20 @@ const store = createIndexedDB({ name: 'app', schema: { users: table('id') } });
|
|
|
27737
28899
|
### `createSQLite()`
|
|
27738
28900
|
|
|
27739
28901
|
```ts
|
|
27740
|
-
function createSQLite(options: SQLiteVaultOptions):
|
|
28902
|
+
function createSQLite(options: SQLiteVaultOptions): TransactionalVaultStore;
|
|
27741
28903
|
```
|
|
27742
28904
|
|
|
27743
28905
|
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
28906
|
|
|
27745
28907
|
| Parameter | Description |
|
|
27746
28908
|
| --- | --- |
|
|
28909
|
+
| `schema` | Tables created by `table()` |
|
|
28910
|
+
| `validators` | Optional per-table validators |
|
|
27747
28911
|
| `database` | Caller-provided `SQLiteDatabase` connection |
|
|
27748
28912
|
| `name` | Namespace within the connection |
|
|
27749
|
-
| `schema`, `validators`, `logger`, `onMetrics` | Shared factory options |
|
|
27750
28913
|
| `closeOnDispose` | Closes the connection during disposal; defaults to `false` |
|
|
27751
28914
|
|
|
27752
|
-
**Returns:** `
|
|
28915
|
+
**Returns:** `TransactionalVaultStore`.
|
|
27753
28916
|
|
|
27754
28917
|
```ts
|
|
27755
28918
|
import { DatabaseSync } from 'node:sqlite';
|
|
@@ -27776,11 +28939,9 @@ interface VaultStore {
|
|
|
27776
28939
|
count(table: K): Promise;
|
|
27777
28940
|
delete(table: K, key: KeyOf): Promise;
|
|
27778
28941
|
deleteMany(table: K, keys: KeyOf[]): Promise;
|
|
27779
|
-
entries(table: K): Promise, RecordOf]>>;
|
|
27780
28942
|
get(table: K, key: KeyOf): Promise | undefined>;
|
|
27781
28943
|
getAll(table: K): Promise[]>;
|
|
27782
28944
|
getMany(table: K, keys: KeyOf[]): Promise | undefined>>;
|
|
27783
|
-
getOrDefault(table: K, key: KeyOf, defaultFn: () => RecordOf, ttl?: number): Promise>;
|
|
27784
28945
|
has(table: K, key: KeyOf): Promise;
|
|
27785
28946
|
isEmpty(table: K): Promise;
|
|
27786
28947
|
keys(table: K, filter?: (record: RecordOf) => boolean): Promise[]>;
|
|
@@ -27790,7 +28951,6 @@ interface VaultStore {
|
|
|
27790
28951
|
update(table: K, key: KeyOf, changes: Partial>, ttl?: number): Promise | undefined>;
|
|
27791
28952
|
upsert(table: K, key: KeyOf, fn: (existing: RecordOf | undefined) => RecordOf, ttl?: number): Promise>;
|
|
27792
28953
|
pruneExpired(): Promise>;
|
|
27793
|
-
debug(): Promise>;
|
|
27794
28954
|
observe(table: K, listener: Observer>, options?: { immediate?: boolean; signal?: AbortSignal }): Unsubscribe;
|
|
27795
28955
|
dispose(): Promise;
|
|
27796
28956
|
readonly disposed: boolean;
|
|
@@ -27803,7 +28963,7 @@ The portable store API is returned by every factory. `observe()` emits the curre
|
|
|
27803
28963
|
|
|
27804
28964
|
---
|
|
27805
28965
|
|
|
27806
|
-
### `batch()`
|
|
28966
|
+
### `batch()` and `iterate()`
|
|
27807
28967
|
|
|
27808
28968
|
```ts
|
|
27809
28969
|
interface TransactionalVaultStore extends VaultStore {
|
|
@@ -27811,10 +28971,11 @@ interface TransactionalVaultStore extends VaultStore {
|
|
|
27811
28971
|
tables: readonly K[],
|
|
27812
28972
|
fn: (tx: TransactionContext) => Promise,
|
|
27813
28973
|
): Promise;
|
|
28974
|
+
iterate(table: K): AsyncIterable>;
|
|
27814
28975
|
}
|
|
27815
28976
|
```
|
|
27816
28977
|
|
|
27817
|
-
|
|
28978
|
+
`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
28979
|
|
|
27819
28980
|
| Parameter | Description |
|
|
27820
28981
|
| --- | --- |
|
|
@@ -27827,43 +28988,24 @@ Runs a scoped atomic callback. `IndexedDbVaultStore` and `SQLiteVaultStore` prov
|
|
|
27827
28988
|
await store.batch(['users'], async (tx) => {
|
|
27828
28989
|
await tx.put('users', { id: 1, name: 'Ada' });
|
|
27829
28990
|
});
|
|
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
28991
|
|
|
27844
|
-
**Returns:** An `AsyncIterable` of records.
|
|
27845
|
-
|
|
27846
|
-
```ts
|
|
27847
28992
|
for await (const user of store.iterate('users')) console.log(user);
|
|
27848
28993
|
```
|
|
27849
28994
|
|
|
27850
|
-
## Queries
|
|
28995
|
+
## Queries and Migrations
|
|
27851
28996
|
|
|
27852
28997
|
### `QueryBuilder`
|
|
27853
28998
|
|
|
27854
28999
|
```ts
|
|
27855
29000
|
interface QueryBuilder {
|
|
27856
|
-
between(field: string, lower: number | string, upper: number | string): QueryBuilder;
|
|
27857
29001
|
count(): Promise;
|
|
27858
29002
|
delete(): Promise;
|
|
27859
|
-
equals(field: K, value: V): QueryBuilder
|
|
27860
|
-
|
|
27861
|
-
filter(fn: (value: N, index: number, array: N[]) => boolean): QueryBuilder;
|
|
29003
|
+
equals(field: K, value: V): QueryBuilder;
|
|
29004
|
+
filter(fn: (value: T, index: number, array: T[]) => boolean): QueryBuilder;
|
|
27862
29005
|
first(): Promise;
|
|
27863
29006
|
limit(n: number): QueryBuilder;
|
|
27864
29007
|
offset(n: number): QueryBuilder;
|
|
27865
29008
|
orderBy(field: K, direction?: 'asc' | 'desc'): QueryBuilder;
|
|
27866
|
-
startsWith(field: keyof T, prefix: string, options?: { ignoreCase?: boolean }): QueryBuilder;
|
|
27867
29009
|
toArray(): Promise;
|
|
27868
29010
|
}
|
|
27869
29011
|
```
|
|
@@ -27871,36 +29013,7 @@ interface QueryBuilder {
|
|
|
27871
29013
|
Builds a lazy table query. `count()` ignores `limit()`, `offset()`, and `orderBy()` — it always returns the full filtered-set size.
|
|
27872
29014
|
|
|
27873
29015
|
```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();
|
|
29016
|
+
const page = await store.query('users').equals('role', 'admin').orderBy('name').limit(20).toArray();
|
|
27904
29017
|
```
|
|
27905
29018
|
|
|
27906
29019
|
---
|
|
@@ -27942,16 +29055,10 @@ type KeyOf =
|
|
|
27942
29055
|
|
|
27943
29056
|
```ts
|
|
27944
29057
|
type BaseAdapterOptions = {
|
|
27945
|
-
logger?: VaultLogger;
|
|
27946
|
-
onMetrics?: (event: MetricsEvent) => void;
|
|
27947
29058
|
schema: S;
|
|
27948
29059
|
validators?: TableValidators;
|
|
27949
29060
|
};
|
|
27950
29061
|
|
|
27951
|
-
type VaultLogger = {
|
|
27952
|
-
error(message: string, context?: Error | Record): void;
|
|
27953
|
-
};
|
|
27954
|
-
|
|
27955
29062
|
type RecordValidator = {
|
|
27956
29063
|
parse(value: unknown): T;
|
|
27957
29064
|
};
|
|
@@ -27959,23 +29066,9 @@ type RecordValidator = {
|
|
|
27959
29066
|
type TableValidators = {
|
|
27960
29067
|
[K in keyof S]?: RecordValidator>;
|
|
27961
29068
|
};
|
|
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
29069
|
```
|
|
27974
29070
|
|
|
27975
29071
|
```ts
|
|
27976
|
-
interface IndexedDbVaultStore
|
|
27977
|
-
extends TransactionalVaultStore, IterableVaultStore {}
|
|
27978
|
-
|
|
27979
29072
|
type MigrationContext = {
|
|
27980
29073
|
db: IDBDatabase;
|
|
27981
29074
|
newVersion: number | null;
|
|
@@ -28015,20 +29108,57 @@ type SQLiteVaultOptions = BaseAdapterOptions & {
|
|
|
28015
29108
|
database: SQLiteDatabase;
|
|
28016
29109
|
name: string;
|
|
28017
29110
|
};
|
|
29111
|
+
```
|
|
29112
|
+
|
|
29113
|
+
```ts
|
|
29114
|
+
interface TransactionalVaultStore extends VaultStore {
|
|
29115
|
+
batch(
|
|
29116
|
+
tables: readonly K[],
|
|
29117
|
+
fn: (tx: TransactionContext) => Promise,
|
|
29118
|
+
): Promise;
|
|
29119
|
+
iterate(table: K): AsyncIterable>;
|
|
29120
|
+
}
|
|
29121
|
+
```
|
|
29122
|
+
|
|
29123
|
+
Import `TransactionalVaultStore` from `@vielzeug/vault`.
|
|
28018
29124
|
|
|
28019
|
-
|
|
28020
|
-
|
|
29125
|
+
```ts
|
|
29126
|
+
interface TransactionContext {
|
|
29127
|
+
clear(table: T): Promise;
|
|
29128
|
+
count(table: T): Promise;
|
|
29129
|
+
delete(table: T, key: KeyOf): Promise;
|
|
29130
|
+
deleteMany(table: T, keys: KeyOf[]): Promise;
|
|
29131
|
+
get(table: T, key: KeyOf): Promise | undefined>;
|
|
29132
|
+
getAll(table: T): Promise[]>;
|
|
29133
|
+
getMany(table: T, keys: KeyOf[]): Promise | undefined>>;
|
|
29134
|
+
has(table: T, key: KeyOf): Promise;
|
|
29135
|
+
isEmpty(table: T): Promise;
|
|
29136
|
+
keys(table: T, filter?: (record: RecordOf) => boolean): Promise[]>;
|
|
29137
|
+
put(table: T, value: RecordOf, ttl?: number): Promise;
|
|
29138
|
+
putAll(table: T, values: RecordOf[], ttl?: number): Promise;
|
|
29139
|
+
query(table: T): QueryBuilder>;
|
|
29140
|
+
update(table: T, key: KeyOf, changes: Partial>, ttl?: number): Promise | undefined>;
|
|
29141
|
+
upsert(table: T, key: KeyOf, fn: (existing: RecordOf | undefined) => RecordOf, ttl?: number): Promise>;
|
|
29142
|
+
}
|
|
28021
29143
|
```
|
|
28022
29144
|
|
|
28023
29145
|
`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
29146
|
|
|
29147
|
+
```ts
|
|
29148
|
+
// Adapter-specific type aliases — both resolve to TransactionalVaultStore.
|
|
29149
|
+
type SQLiteVaultStore = TransactionalVaultStore;
|
|
29150
|
+
type IndexedDbVaultStore = TransactionalVaultStore;
|
|
29151
|
+
```
|
|
29152
|
+
|
|
29153
|
+
`SQLiteVaultStore` is exported from `@vielzeug/vault/sqlite`. `IndexedDbVaultStore` is exported from `@vielzeug/vault/indexeddb`.
|
|
29154
|
+
|
|
28025
29155
|
## Errors
|
|
28026
29156
|
|
|
28027
29157
|
| Error | Trigger |
|
|
28028
29158
|
| --- | --- |
|
|
28029
29159
|
| `VaultError` | Any Vault-originated validation, serialization, storage, or query error |
|
|
28030
29160
|
| `VaultDisposedError` | An operation after the store or observer hub is disposed |
|
|
28031
|
-
| `VaultScopeError` |
|
|
29161
|
+
| `VaultScopeError` | A `batch()` callback accesses a table outside its declared scope |
|
|
28032
29162
|
| `VaultQuotaError` | A LocalStorage or SessionStorage write exceeds the browser quota |
|
|
28033
29163
|
| `VaultMigrationError` | An IndexedDB migration callback throws |
|
|
28034
29164
|
|
|
@@ -28095,29 +29225,27 @@ console.log(updated);
|
|
|
28095
29225
|
Build a query from a table, then finish it with a terminal method. `count()` ignores pagination, which makes it suitable for page controls.
|
|
28096
29226
|
|
|
28097
29227
|
```ts
|
|
28098
|
-
const query = store.query('preferences').startsWith('
|
|
29228
|
+
const query = store.query('preferences').filter((p) => p.id.startsWith('theme'));
|
|
28099
29229
|
const preferences = await query.orderBy('id').limit(10).toArray();
|
|
28100
29230
|
const total = await query.count();
|
|
28101
29231
|
|
|
28102
29232
|
console.log({ preferences, total });
|
|
28103
29233
|
```
|
|
28104
29234
|
|
|
28105
|
-
|
|
29235
|
+
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
29236
|
|
|
28107
29237
|
## Use TTL and Pruning
|
|
28108
29238
|
|
|
28109
|
-
Use `ttl.*` helpers for expiring rows.
|
|
29239
|
+
Use `ttl.*` helpers for expiring rows. Call `pruneExpired()` to reclaim storage from stale rows that accumulate without reads.
|
|
28110
29240
|
|
|
28111
29241
|
```ts
|
|
28112
|
-
import {
|
|
29242
|
+
import { ttl } from '@vielzeug/vault';
|
|
28113
29243
|
|
|
28114
29244
|
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
29245
|
|
|
28120
|
-
|
|
29246
|
+
// Reclaim expired rows on a schedule owned by the application.
|
|
29247
|
+
const pruneInterval = setInterval(() => store.pruneExpired(), ttl.hours(6));
|
|
29248
|
+
store.disposalSignal.addEventListener('abort', () => clearInterval(pruneInterval));
|
|
28121
29249
|
```
|
|
28122
29250
|
|
|
28123
29251
|
## Observe a Table
|
|
@@ -28245,7 +29373,7 @@ Use Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple sig
|
|
|
28245
29373
|
- Use IndexedDB or SQLite for atomic work.
|
|
28246
29374
|
- Keep external asynchronous work outside `batch()` callbacks.
|
|
28247
29375
|
- Use `ttl.*` instead of raw durations.
|
|
28248
|
-
- Keep SQLite scans and writes off latency-sensitive event loops
|
|
29376
|
+
- Keep SQLite scans and writes off latency-sensitive event loops.
|
|
28249
29377
|
- Dispose stores when their owner ends.
|
|
28250
29378
|
|
|
28251
29379
|
### Examples
|
|
@@ -28263,10 +29391,10 @@ Use Forge’s Vault helpers for explicit form-draft persistence. Keep Ripple sig
|
|
|
28263
29391
|
|
|
28264
29392
|
- Basic Setup - Initialize Vault (id: `basic-setup`)
|
|
28265
29393
|
- Bulk Operations (id: `bulk-operations`)
|
|
28266
|
-
- Cache-First with
|
|
29394
|
+
- Cache-First with get + put (id: `cache-first`)
|
|
28267
29395
|
- CRUD Operations (id: `crud-operations`)
|
|
28268
29396
|
- IndexedDB — Atomic Batch & iterate() (id: `indexed-db`)
|
|
28269
|
-
- TTL —
|
|
29397
|
+
- TTL — pruneExpired with disposalSignal (id: `prune-schedule`)
|
|
28270
29398
|
- Query Builder — Filters, Pagination, count (id: `query-builder`)
|
|
28271
29399
|
- Reactive — observe() (id: `reactive-observe`)
|
|
28272
29400
|
- TTL & Expiration (id: `ttl-expiration`)
|
|
@@ -28352,6 +29480,7 @@ else console.log(decision.reason);
|
|
|
28352
29480
|
- `WILDCARD` and `ANONYMOUS` model broad or unauthenticated access explicitly.
|
|
28353
29481
|
- `owns()` and `predicate` constrain rules with synchronous request data.
|
|
28354
29482
|
- `explain()`, `trace()`, and `detectConflicts()` make policy decisions diagnosable.
|
|
29483
|
+
- `tap()` subscribes to decision events for logging and diagnostics.
|
|
28355
29484
|
- `forUser()` creates a principal-bound view for repeated checks.
|
|
28356
29485
|
- `checkAll()` evaluates multiple resource/action pairs in one call.
|
|
28357
29486
|
|
|
@@ -28377,10 +29506,10 @@ else console.log(decision.reason);
|
|
|
28377
29506
|
| `createWard` | Creates immutable policy | Sync | Rules cannot be mutated after creation |
|
|
28378
29507
|
| `allow` / `deny` / `ruleFor` | Builds policy rules | Sync | Priority wins before specificity |
|
|
28379
29508
|
| `Ward.explain` | Returns one decision | Sync | Pass resource data for predicate rules |
|
|
28380
|
-
| `Ward.trace` | Inspects decision candidates | Sync | Does not
|
|
29509
|
+
| `Ward.trace` | Inspects decision candidates | Sync | Does not fire a `decision` event |
|
|
28381
29510
|
| `Ward.forUser` | Binds a principal | Sync | Rebind when identity or roles change |
|
|
28382
29511
|
| `Ward.checkAll` | Batch permission checks | Sync | Pass resource data for predicate rules |
|
|
28383
|
-
| `Ward.allowedActions` | Filters known actions to allowed set | Sync | Does not
|
|
29512
|
+
| `Ward.allowedActions` | Filters known actions to allowed set | Sync | Does not fire a `decision` event |
|
|
28384
29513
|
| `Ward.rulesInScope` | Lists rules matching a principal/resource | Sync | Pass data to evaluate predicates |
|
|
28385
29514
|
| `Ward.detectConflicts` | Detects duplicate/shadowed rules | Sync | O(n²) — use `maxConflicts` for large policies |
|
|
28386
29515
|
| `predicate.owns` / `owns` | Ownership predicate on resource data | Sync | Skipped for anonymous principals |
|
|
@@ -28392,7 +29521,6 @@ else console.log(decision.reason);
|
|
|
28392
29521
|
| Import | Purpose |
|
|
28393
29522
|
| --- | --- |
|
|
28394
29523
|
| `@vielzeug/ward` | Rules, factory, predicates, pattern helpers, errors, and public types |
|
|
28395
|
-
| `@vielzeug/ward/devtools` | `debugWard()` diagnostic factory |
|
|
28396
29524
|
|
|
28397
29525
|
## Core Factory
|
|
28398
29526
|
|
|
@@ -28405,14 +29533,13 @@ createWard(
|
|
|
28405
29533
|
): Ward;
|
|
28406
29534
|
```
|
|
28407
29535
|
|
|
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 `
|
|
29536
|
+
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
29537
|
|
|
28410
29538
|
**Parameters:**
|
|
28411
29539
|
|
|
28412
29540
|
| Name | Type | Description |
|
|
28413
29541
|
| --- | --- | --- |
|
|
28414
29542
|
| `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
29543
|
| `options.onConflict` | `(conflict: WardConflict) => void` | Called synchronously per conflict at creation time. |
|
|
28417
29544
|
| `options.strict` | `boolean` | Throws `WardConfigError` on the first conflict. |
|
|
28418
29545
|
| `options.maxConflicts` | `number` | Caps the number of conflicts returned by `detectConflicts()`. |
|
|
@@ -28498,7 +29625,7 @@ checkAll(
|
|
|
28498
29625
|
): WardDecisionResult[];
|
|
28499
29626
|
```
|
|
28500
29627
|
|
|
28501
|
-
Evaluates multiple resource/action pairs for one principal.
|
|
29628
|
+
Evaluates multiple resource/action pairs for one principal. Fires a `decision` event for each result via `tap()`.
|
|
28502
29629
|
|
|
28503
29630
|
**Returns:** `WardDecisionResult[]` — each entry carries `action`, `resource`, and the decision.
|
|
28504
29631
|
|
|
@@ -28521,7 +29648,7 @@ explain(input: WardDecisionInput): WardDecision;
|
|
|
28521
29648
|
}
|
|
28522
29649
|
```
|
|
28523
29650
|
|
|
28524
|
-
Returns one decision.
|
|
29651
|
+
Returns one decision. Fires a `decision` event via `tap()`.
|
|
28525
29652
|
|
|
28526
29653
|
**Returns:** `WardDecision` — `{ allowed: true; rule }` or `{ allowed: false; reason: 'explicit-deny'; rule }` or `{ allowed: false; reason: 'no-matching-rule' }`.
|
|
28527
29654
|
|
|
@@ -28533,7 +29660,7 @@ Returns one decision. Invokes the logger.
|
|
|
28533
29660
|
trace(input: WardDecisionInput): WardTrace;
|
|
28534
29661
|
```
|
|
28535
29662
|
|
|
28536
|
-
Same request shape as `explain()`. Returns winner + candidate list. Does not fire
|
|
29663
|
+
Same request shape as `explain()`. Returns winner + candidate list. Does not fire a `decision` event.
|
|
28537
29664
|
|
|
28538
29665
|
**Returns:** `WardTrace` — `{ candidates: WardTraceCandidate[]; decision: WardDecision }`.
|
|
28539
29666
|
|
|
@@ -28556,7 +29683,7 @@ Input shape:
|
|
|
28556
29683
|
}
|
|
28557
29684
|
```
|
|
28558
29685
|
|
|
28559
|
-
Filters the provided `knownActions` list to those the principal may perform. Does not
|
|
29686
|
+
Filters the provided `knownActions` list to those the principal may perform. Does not fire a `decision` event.
|
|
28560
29687
|
|
|
28561
29688
|
**Returns:** `TAction[]` — the subset of `knownActions` that `explain()` would allow.
|
|
28562
29689
|
|
|
@@ -28712,17 +29839,37 @@ Tests whether the `broad` pattern covers the `narrow` pattern. `'*'` covers ever
|
|
|
28712
29839
|
|
|
28713
29840
|
---
|
|
28714
29841
|
|
|
28715
|
-
##
|
|
29842
|
+
## Observability
|
|
29843
|
+
|
|
29844
|
+
### `tap(handler, options?)`
|
|
29845
|
+
|
|
29846
|
+
```ts
|
|
29847
|
+
tap(
|
|
29848
|
+
handler: (event: WardEvent) => void,
|
|
29849
|
+
options?: { signal?: AbortSignal },
|
|
29850
|
+
): () => void;
|
|
29851
|
+
```
|
|
29852
|
+
|
|
29853
|
+
Subscribes a handler to ward events. Each `explain()` and `checkAll()` decision fires a `decision` event. `trace()` and `allowedActions()` do not fire events.
|
|
29854
|
+
|
|
29855
|
+
Pass an `AbortSignal` to unsubscribe automatically; the returned function unsubscribes manually.
|
|
28716
29856
|
|
|
28717
|
-
|
|
29857
|
+
**Returns:** `() => void` — call to unsubscribe the handler.
|
|
28718
29858
|
|
|
28719
|
-
|
|
29859
|
+
**Example:**
|
|
28720
29860
|
|
|
28721
29861
|
```ts
|
|
28722
|
-
|
|
29862
|
+
const ward = createWard(rules);
|
|
29863
|
+
ward.tap((event) => console.debug(`ward:${event.type}`, event.decision));
|
|
28723
29864
|
```
|
|
28724
29865
|
|
|
28725
|
-
|
|
29866
|
+
With a logger from `@vielzeug/rune`:
|
|
29867
|
+
|
|
29868
|
+
```ts
|
|
29869
|
+
import { createLogger } from '@vielzeug/rune';
|
|
29870
|
+
const log = createLogger({ name: 'ward' });
|
|
29871
|
+
ward.tap((event) => log.debug(event, 'ward:decision'));
|
|
29872
|
+
```
|
|
28726
29873
|
|
|
28727
29874
|
---
|
|
28728
29875
|
|
|
@@ -28847,6 +29994,7 @@ export type Ward = {
|
|
|
28847
29994
|
explain(input: WardDecisionInput): WardDecision;
|
|
28848
29995
|
forUser(principal: UserPrincipal): BoundWard;
|
|
28849
29996
|
rulesInScope(input: WardRulesInScopeInput): ReadonlyArray>>;
|
|
29997
|
+
tap(handler: (event: WardEvent) => void, options?: { signal?: AbortSignal }): () => void;
|
|
28850
29998
|
trace(input: WardDecisionInput): WardTrace;
|
|
28851
29999
|
};
|
|
28852
30000
|
|
|
@@ -28858,7 +30006,9 @@ export type BoundWard = {
|
|
|
28858
30006
|
trace(input: BoundWardDecisionInput): WardTrace;
|
|
28859
30007
|
};
|
|
28860
30008
|
|
|
28861
|
-
export type
|
|
30009
|
+
export type WardEvent = {
|
|
30010
|
+
type: 'decision';
|
|
30011
|
+
decision: WardDecision;
|
|
28862
30012
|
action: TAction;
|
|
28863
30013
|
data?: TData;
|
|
28864
30014
|
principal: Principal;
|
|
@@ -28866,7 +30016,6 @@ export type WardLoggerContext = WardDecision & {
|
|
|
28866
30016
|
};
|
|
28867
30017
|
|
|
28868
30018
|
export type WardOptions = {
|
|
28869
|
-
logger?: (context: WardLoggerContext) => void;
|
|
28870
30019
|
maxConflicts?: number;
|
|
28871
30020
|
onConflict?: (conflict: WardConflict) => void;
|
|
28872
30021
|
strict?: boolean;
|
|
@@ -28877,12 +30026,12 @@ export type WardOptions = {
|
|
|
28877
30026
|
|
|
28878
30027
|
`Ward`, `BoundWard`, `WardDecision`, `WardDecisionResult`, `WardTrace`, `WardTraceCandidate`, `WardConflict`,
|
|
28879
30028
|
`NormalizedWardRule`, `WardOptions`, `WardCheck`, `WardAllowedActionsInput`, `WardRulesInScopeInput`, `RuleContext`,
|
|
28880
|
-
`
|
|
30029
|
+
`WardEvent`, `WardPredicate`, and `ConflictKind` are exported from the root entry point.
|
|
28881
30030
|
|
|
28882
30031
|
## Errors
|
|
28883
30032
|
|
|
28884
|
-
- `WardError` is the base error class; use `WardError
|
|
28885
|
-
- `WardConfigError` reports malformed rules, invalid `createWard` options (`
|
|
30033
|
+
- `WardError` is the base error class; use `instanceof WardError` for narrowing.
|
|
30034
|
+
- `WardConfigError` reports malformed rules, invalid `createWard` options (`onConflict`, `maxConflicts`), invalid principals, and strict conflict initialization.
|
|
28886
30035
|
- `WardPredicateError` reports a throwing synchronous predicate and includes its `ruleIndex` and cause.
|
|
28887
30036
|
|
|
28888
30037
|
### Usage Guide
|
|
@@ -28952,7 +30101,7 @@ const actions = ward.allowedActions({
|
|
|
28952
30101
|
});
|
|
28953
30102
|
```
|
|
28954
30103
|
|
|
28955
|
-
It does not fire
|
|
30104
|
+
It does not fire a `decision` event.
|
|
28956
30105
|
|
|
28957
30106
|
## Rule Introspection
|
|
28958
30107
|
|
|
@@ -28979,7 +30128,34 @@ trace.candidates.forEach((c) => {
|
|
|
28979
30128
|
});
|
|
28980
30129
|
```
|
|
28981
30130
|
|
|
28982
|
-
`trace()` does not fire
|
|
30131
|
+
`trace()` does not fire a `decision` event.
|
|
30132
|
+
|
|
30133
|
+
## Observing Decisions
|
|
30134
|
+
|
|
30135
|
+
`tap()` subscribes a handler to ward events. Each `explain()` and `checkAll()` decision fires a `decision` event; `trace()` and `allowedActions()` do not.
|
|
30136
|
+
|
|
30137
|
+
```ts
|
|
30138
|
+
const ward = createWard(rules);
|
|
30139
|
+
ward.tap((event) => console.debug(`ward:${event.type}`, event.decision));
|
|
30140
|
+
```
|
|
30141
|
+
|
|
30142
|
+
Pass an `AbortSignal` to unsubscribe automatically, or call the returned function to unsubscribe manually:
|
|
30143
|
+
|
|
30144
|
+
```ts
|
|
30145
|
+
const controller = new AbortController();
|
|
30146
|
+
const unsubscribe = ward.tap((event) => console.debug(event), { signal: controller.signal });
|
|
30147
|
+
|
|
30148
|
+
// later
|
|
30149
|
+
unsubscribe(); // or controller.abort();
|
|
30150
|
+
```
|
|
30151
|
+
|
|
30152
|
+
For structured logging, forward events to a `@vielzeug/rune` logger:
|
|
30153
|
+
|
|
30154
|
+
```ts
|
|
30155
|
+
import { createLogger } from '@vielzeug/rune';
|
|
30156
|
+
const log = createLogger({ name: 'ward' });
|
|
30157
|
+
ward.tap((event) => log.debug(event, 'ward:decision'));
|
|
30158
|
+
```
|
|
28983
30159
|
|
|
28984
30160
|
## Predicate Helpers
|
|
28985
30161
|
|
|
@@ -29079,7 +30255,7 @@ container.register('ward', ward);
|
|
|
29079
30255
|
- [Priority and Overrides](./examples/inheritance-and-overrides.md)
|
|
29080
30256
|
- [Bound Guard in UI Layer](./examples/bound-guard-in-ui-layer.md)
|
|
29081
30257
|
- [Rule Specificity](./examples/disabling-wildcard-fallback.md)
|
|
29082
|
-
- [
|
|
30258
|
+
- [Auditing Decisions](./examples/logger-for-auditing.md)
|
|
29083
30259
|
- [Fresh Ward Per Test](./examples/snapshot-restore-for-test-isolation.md)
|
|
29084
30260
|
- [Conflict Detection](./examples/conflict-detection.md)
|
|
29085
30261
|
- [Trace a Decision](./examples/trace-decision.md)
|
|
@@ -29213,7 +30389,7 @@ router.dispose();
|
|
|
29213
30389
|
- `match()` / `load()` — Inspect routes synchronously or load route data without navigation.
|
|
29214
30390
|
- `preload()` — Warms route data for a later matching navigation.
|
|
29215
30391
|
- `createMemoryHistory()` — Runs routers in tests and non-browser environments.
|
|
29216
|
-
- `
|
|
30392
|
+
- `subscribe()` — Reactive subscription to navigation state changes.
|
|
29217
30393
|
|
|
29218
30394
|
## Documentation
|
|
29219
30395
|
|
|
@@ -29253,10 +30429,9 @@ router.dispose();
|
|
|
29253
30429
|
|
|
29254
30430
|
## Package Entry Points
|
|
29255
30431
|
|
|
29256
|
-
| Import
|
|
29257
|
-
|
|
|
29258
|
-
| `@vielzeug/wayfinder`
|
|
29259
|
-
| `@vielzeug/wayfinder/devtools` | `debugRouter` — navigation logger (dev only) |
|
|
30432
|
+
| Import | Purpose |
|
|
30433
|
+
| --------------------- | ---------------------- |
|
|
30434
|
+
| `@vielzeug/wayfinder` | Main exports and types |
|
|
29260
30435
|
|
|
29261
30436
|
## `createRouter(options)`
|
|
29262
30437
|
|
|
@@ -30062,44 +31237,6 @@ Thrown on middleware misuse — currently only when a middleware function calls
|
|
|
30062
31237
|
| `/files/:rest*` | `/files/a/b/c` | Wildcard suffix captured as one named param |
|
|
30063
31238
|
| `*` | anything | Global catch-all |
|
|
30064
31239
|
|
|
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
31240
|
## Design Notes
|
|
30104
31241
|
|
|
30105
31242
|
- Wayfinder no longer exposes imperative registration methods like `on()`, `group()`, or `use()`.
|
|
@@ -30741,43 +31878,50 @@ export function useRouter() {
|
|
|
30741
31878
|
|
|
30742
31879
|
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
31880
|
|
|
30744
|
-
## Debug
|
|
31881
|
+
## Debug Logging
|
|
30745
31882
|
|
|
30746
|
-
|
|
31883
|
+
`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
31884
|
|
|
30748
31885
|
```ts
|
|
30749
|
-
import {
|
|
31886
|
+
import { createRouter } from '@vielzeug/wayfinder';
|
|
30750
31887
|
|
|
30751
|
-
const router =
|
|
30752
|
-
|
|
30753
|
-
|
|
30754
|
-
dashboard: { path: '/dashboard', data: () => fetchDashboard() },
|
|
30755
|
-
},
|
|
31888
|
+
const router = createRouter({ routes });
|
|
31889
|
+
const stop = router.subscribe((state) => {
|
|
31890
|
+
console.debug(`[wayfinder] ${state.status} ${state.location.pathname}`);
|
|
30756
31891
|
});
|
|
30757
31892
|
|
|
30758
31893
|
// Logged once the initial navigation completes:
|
|
30759
|
-
// [wayfinder
|
|
31894
|
+
// [wayfinder] idle /
|
|
30760
31895
|
|
|
30761
31896
|
// On navigate({ name: 'dashboard' }):
|
|
30762
|
-
// [wayfinder
|
|
30763
|
-
// [wayfinder
|
|
31897
|
+
// [wayfinder] loading /dashboard
|
|
31898
|
+
// [wayfinder] idle /dashboard
|
|
30764
31899
|
```
|
|
30765
31900
|
|
|
30766
|
-
The
|
|
31901
|
+
The returned function unsubscribes the listener — call it when the logger is no longer needed (e.g. on teardown):
|
|
30767
31902
|
|
|
30768
|
-
|
|
31903
|
+
```ts
|
|
31904
|
+
stop();
|
|
31905
|
+
```
|
|
31906
|
+
|
|
31907
|
+
Errors are surfaced on the state object, so you can log them explicitly:
|
|
30769
31908
|
|
|
30770
31909
|
```ts
|
|
30771
|
-
|
|
31910
|
+
router.subscribe((state) => {
|
|
31911
|
+
if (state.status === 'error') {
|
|
31912
|
+
console.error(`[wayfinder] ${state.location.pathname}`, state.error);
|
|
31913
|
+
}
|
|
31914
|
+
});
|
|
30772
31915
|
```
|
|
30773
31916
|
|
|
30774
|
-
Use
|
|
31917
|
+
Use a label when running multiple routers to distinguish their log output:
|
|
30775
31918
|
|
|
30776
31919
|
```ts
|
|
30777
|
-
const main =
|
|
30778
|
-
|
|
30779
|
-
|
|
30780
|
-
|
|
31920
|
+
const main = createRouter({ routes });
|
|
31921
|
+
main.subscribe((state) => console.debug(`[wayfinder:main] ${state.status} ${state.location.pathname}`));
|
|
31922
|
+
|
|
31923
|
+
const modal = createRouter({ routes: modalRoutes });
|
|
31924
|
+
modal.subscribe((state) => console.debug(`[wayfinder:modal] ${state.status} ${state.location.pathname}`));
|
|
30781
31925
|
```
|
|
30782
31926
|
|
|
30783
31927
|
Debug logging has no effect on behavior and should not be enabled in production.
|
|
@@ -30861,7 +32005,7 @@ router.subscribe((state) => {
|
|
|
30861
32005
|
### REPL Examples
|
|
30862
32006
|
|
|
30863
32007
|
- Basic Routing — Route State and Navigation (id: `basic-routing`)
|
|
30864
|
-
-
|
|
32008
|
+
- Navigation Logging (id: `debug-router`)
|
|
30865
32009
|
- Guards and Redirects — Auth Flows (id: `middleware-auth`)
|
|
30866
32010
|
- Middleware Chain — Execution Flow (id: `middleware-chain`)
|
|
30867
32011
|
- Named Routes — Type-Safe Navigation (id: `named-routes`)
|