@voltro/client 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/index.d.ts +100 -4
- package/dist/index.js +317 -274
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,35 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.4.0] — 2026-07-18
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **@voltro/protocol, @voltro/plugin-rbac, @voltro/runtime** — **Resource-aware declarative guards + one denial tag.** `guards: [{ scope, resource }]` now actually scopes to a resource. Register a resolver — either `setResourceScopeResolver((req) => Effect<boolean>)` from `@voltro/protocol`, or `rbacPlugin({ resolveResourceRoles: (subject, resourceId) => roleSlugs })` — and the framework asks it, per request, whether the caller holds the scope **on the extracted resource** (a team, workspace, document), before the executor runs (mutations: before the transaction opens). A globally-held scope or `admin:full` still short-circuits without a resolver call; a resolver error fails **closed**. With no resolver registered, `resource` stays advisory (global check) exactly as before, so single-tenant apps are unaffected. **BREAKING — `@voltro/plugin-rbac` no longer exports `Forbidden`.** rbac's `permission()` / `assertPermission()` / `anyPermission()` now fail with protocol's `ScopeError` — the SAME typed error the declarative `guards:` enforcement already raised. One denial tag across the framework: a client that matches `_tag === 'ScopeError'` (or `errorTag(err)`) recognizes both a descriptor-guard denial and a `permission()` denial with one branch. The `@voltro/plugin-rbac/errors` subpath is removed. Migration (the `voltro update` codemod rewrites the mechanical part): - `import { Forbidden } from '@voltro/plugin-rbac/errors'` → `import { ScopeError } from '@voltro/protocol'`; `import { Forbidden } from '@voltro/plugin-rbac'` → `import { ScopeError } from '@voltro/plugin-rbac'` (re-exported from the root). - Every `Forbidden` identifier → `ScopeError` (incl. `error: Forbidden` on a descriptor). - By hand: a constructed `new Forbidden({ required, reason })` becomes `new ScopeError({ required, message })` (the field is `message` and is REQUIRED); and any `Effect.catchTag('Forbidden', …)` / `err._tag === 'Forbidden'` string becomes `'ScopeError'`. Guards are normally caught, not constructed, so most apps only need the import rewrite the codemod does. Also note: `requireScope` / declarative `guards:` enforcement is now async-capable (it awaits the resource resolver when one is registered) — no change to handler code, which never called the internal enforcement directly.
|
|
47
|
+
- **@voltro/client, @voltro/plugin-rbac** — **`@voltro/plugin-rbac/web` is removed — the UI permission gate lives in `@voltro/client`.** The framework shipped `useCan` TWICE, with the same name and the same semantics, over two different React contexts. An app that mounted `<PermissionProvider>` and imported the plugin's `useCan` got an empty scope set and silently hid every gated affordance — no error, just a UI where nothing is permitted. One of the two had to go. `@voltro/client` survives because scopes are a FRAMEWORK concept: `@voltro/protocol` owns `ScopeError`, `guards: [{ scope }]` and `ctx.access`, and rbac is only one way to PRODUCE scopes — an app can register its own `setResourceScopeResolver` over its own tables and never install the plugin. The hook consumes scopes, which is generic; producing them is rbac-specific. Keeping the hook behind the plugin would force an rbac dependency on apps that deliberately don't use rbac, purely to gate a button. `@voltro/plugin-rbac` keeps everything that IS rbac: the roles map, role→scope compilation, the `permission()` / `assertPermission()` / `anyPermission()` server guards, the role tables, and `resolveResourceRoles`. The codemod rewrites every mechanical site: `RbacScopeProvider` → `PermissionProvider`, `canFromScopes` → `canCall`, `canAnyFromScopes` → `canCallAny`, and repoints `useCan` / `useCanAny` / `ADMIN_SCOPE` to `@voltro/client`. A raw `useContext(RbacScopeContext)` is ANNOTATED rather than rewritten — the client equivalent is `usePermissions()`, which returns `{ scopes }` instead of the bare array, so a silent rewrite would compile and then hand back the wrong shape.
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- **@voltro/runtime, @voltro/cli** — **`read({ where })` on aggregates.** Aggregate reads were all-or-nothing, so every consumer that wanted one slice pulled the whole materialised set over the wire and filtered in the component — the cost scaling with the aggregate, not the slice. `where` entries are ANDed; a scalar means strict equality, an array means IN; filtering runs before `orderBy`/`limit`. Deliberately a data filter over already materialised rows, not a predicate language — aggregates stay aggregates.
|
|
52
|
+
- **@voltro/client** — **Two ergonomics that remove the two most-repeated shapes in a Voltro frontend.** - **`mutate(input, options)` — `onSuccess` / `onError` / `notify`.** Every write site used to re-type `try { await mutate() } catch { toast.error(…) } finally { setSubmitting(false) }`. `pending` already replaced the `finally`; these replace the rest. Load-bearing semantic: supplying an error handler (`onError` or `notify.error`) marks the failure HANDLED — `mutate` resolves with `undefined` instead of rejecting, which is what actually deletes the try/catch. With no handler it rejects exactly as before, so unhandled failures stay loud. `notify` routes to an app-registered sink (`setMutationNotifier`) — the framework stays unbound to any toast library. - **`useSubscription` now returns `loading` and `isEmpty`, and accepts `fallback`.** Call sites branched on `data === undefined`, conflating "no snapshot yet" with "zero rows" — the cause of a flash of empty-state before the first snapshot. They are now distinct, derived once. `fallback` fills `data` while loading without lying about `loading`. Also documented: `error` carries a COLD-START stream failure (check it to avoid an infinite skeleton); a failure after data arrived deliberately does not blank good data — those reach the error bus (`useOnRpcError`).
|
|
53
|
+
- **@voltro/cli** — **Make the client hook surface discoverable — and keep it that way.** A downstream app built its whole frontend on three transport hooks (`useSubscription` / `useMutation` / `useAction`) and hand-rolled forms, tables, upload, permission gates, debounce and pagination, because the agent guide documented ~none of the ~40 client hooks that already ship. Three changes: - **The always-loaded core now carries a "Pick the CLIENT primitive" rubric** — the same decision-rubric treatment the backend primitives get, plus a "you're about to write X → reach for Y" table (per-field `useState` → `useFormBinding`, hand-rolled table → `useDataTable`, `FileReader` → `useUpload`, `useMemo` fan-in → `useDerived`, …). - **The hook reference is complete.** `reference/hooks-overview.md` presented itself as "the client-side hook surface" while documenting 13 of 44 hooks; it now enumerates the schema-driven-UI, files/permissions/utilities, and AI families (en + de). - **A coverage GATE** (`agentsMdTemplate.test.ts`): every exported `use*` hook of `@voltro/client` must appear in the composed agent-docs corpus, or the test fails with the list. Docs can no longer silently fall behind the export surface. Plus **`voltro doctor` now flags hand-rolls**: it scans an app's source for the patterns a shipped primitive covers (hand-rolled form/table, `FileReader` upload, `setTimeout` debounce, `useMemo` fan-in, local Next.js compat shims, hand-rolled presence) and names the primitive to use. Advisory, never fatal — a false-positive lecture must not fail a build.
|
|
54
|
+
- **@voltro/cli** — **Generated `matchError` / `AppError` / `AppErrorTag`.** Codegen now emits a per-app exhaustive error matcher into `rpcGroup.generated.ts`, derived BY REFERENCE from every descriptor's `error:` schema plus the cross-cutting plugin errors. `matchError(err, { [tag]: handler }, fallback?)` dispatches on `_tag` with the handler keys constrained to the app's ACTUAL error tags — so a hand-maintained tag list that silently drifts (dead/renamed tags) is gone; a stale tag is a compile error. Browser-safe (reads `_tag` structurally, no runtime dep). Workflows keep their existing `WorkflowErrors`.
|
|
55
|
+
- **@voltro/cli** — **`voltro doctor` + `voltro update --codemods-only`/`--from`** — two upgrade-path ergonomics from AWB adoption feedback: - **`voltro doctor [app]`** (and **`voltro serve --preflight`**) preflights an API app for production serve: it verifies the precompiled serve bundle exists and, if not, prints the exact fix and exits 1 — so a Dockerfile / CI step catches "prod `voltro serve` with no `voltro build`" at BUILD time instead of at cold-start (0.3.0 made an unbuilt production serve fatal). - **`voltro update --codemods-only` (alias `--run-codemods`) + `--from <version>`** — re-apply the codemods + manual notes for an explicit `[from, to]` delta WITHOUT bumping `package.json` or installing. The recovery path for a hand-edited version bump (`bump package.json` + install first), which otherwise makes `voltro update` report "already on X — nothing to do" and silently skip the codemods. `--from` also overrides the auto-detected source version on a normal update.
|
|
56
|
+
- **@voltro/runtime** — **`encryptField` / `decryptField` — standalone field cipher for raw-SQL paths.** `.encrypted()` columns encrypt/decrypt transparently inside the `ctx.store` middleware, so a code path that reaches the DB by RAW SQL (an auth strategy with no store handle reading/writing a session token, a one-off backfill) bypasses it. `@voltro/runtime` now exposes the SAME registered cipher standalone: `encryptField` encrypts a value; `decryptField` decrypts an `enc:v1:…` value and passes a non-ciphertext value through unchanged (so a raw-SQL path can adopt encryption while pre-existing plaintext rows keep working). Both throw a clear error when no cipher is registered. Encryption stops being all-or-nothing tied to going through `ctx.store`.
|
|
57
|
+
- **@voltro/testing** — **`makeVoltroTestClient` — the missing half of the test story.** The framework shipped a strong BACKEND test story (`makeTestContext`, `mockStore`, `mockAi`, the workflow runner) and nothing for the client. A Voltro frontend is a reactive-data app: with no way to render a component against mocked `useSubscription` / `useMutation` — and to inject a loading state, a stream error, or a failing write — it is structurally untestable, which is why real apps end up with zero frontend tests. `import { makeVoltroTestClient } from '@voltro/testing/client'` returns a `Provider` plus `setSubscription` / `failSubscription` / `resetSubscription` and a `calls` log of every write with its input. A tag ABSENT from the fixture stays in the loading state — the distinction from an empty result is exactly what you want to assert. Deliberately NOT a renderer: it hands you a Provider, so it works with `react-dom/client` + `act`, testing-library, or your own harness, and locks you into none of them.
|
|
58
|
+
- **@voltro/i18n** — **Plurals, Intl formatters, and catalogs a bundler can actually split.** - **`plural(locale, count, forms)` + `usePlural`** — real CLDR categories via `Intl.PluralRules`. Replaces the hardcoded `"{count} epic(s)"` pattern, whose literal `(s)` is simply wrong outside English, and which cannot express languages that distinguish few from many (Polish 2–4 vs 5+). An explicit `zero` form is honoured for exactly 0; ordinals via `{ type: 'ordinal' }`. - **`useFormatDate` / `useRelativeTime` / `useFormatNumber` / `useFormatCurrency` / `useFormatters`** — locale-bound `Intl` wrappers, so an app stops carrying several divergent hand-rolled "X minutes ago" helpers that each round differently. - **`defineCatalogs` + `LazyI18nProvider`** — code-split catalogs. `pickCatalog({ en, de })` is a STATIC import map: every locale is a value-level import, so the bundler must emit them in one chunk and a German-only visitor downloads English too, growing linearly per locale. A map of `() => import()` loaders is the only shape a bundler treats as a chunk boundary. `preload(locale)` before hydration keeps first paint synchronous via `peek()`; the provider's `fallback` is for a locale SWITCH, not first paint. Unknown locales degrade to the base catalog, and a failed chunk load stays retryable.
|
|
59
|
+
- **@voltro/protocol, @voltro/client** — **Nested optimistic: `shapeItem` + bulk (multi-item) targets.** Two additive improvements to path-targeted auto-optimistic: - **`shapeItem`** — the nested counterpart of `shape`, typed to the ITEM of the nested array (not the mutation's output), so a path-target patch reads/returns the item without casting `current`. (Previously the nested shaper was the flat `shape`, pinned to the output row — every adopter had to cast. `shape` stays bound to the output for flat targets; a single field can't be both, so the nested shaper is its own.) - **Bulk `identify`** — a target's `identify` may now return `string[]` to patch/delete MANY items (or top-level rows) in one mutation — the group-drag / batch-edit case where per-item parallel writes used to race. Each patched item keeps its own key. Runtime-compatible: a nested target that still uses `shape` falls back transparently. No codemod — additive.
|
|
60
|
+
- **@voltro/web, @voltro/client** — **Two hooks apps kept hand-rolling, because nothing shipped them.** - **`useTheme` (`@voltro/web`)** — returns `{ theme, resolvedTheme, setTheme }` over the framework's own `voltro:theme` cookie and the `html.dark` class. The bug it removes is specific: an app that hand-rolls a second theme store ends up with two writers on one class, so the toggle's state and the rendered theme can disagree, and the pre-paint script flashes the wrong one. `'system'` resolves through `matchMedia` and tracks live OS changes. - **`useConnectionStatus` (`@voltro/client`)** — `'connected' | 'degraded' | 'offline'`, derived from the only two signals the client honestly has: `navigator.onLine` (browser says the network is gone) and the rpc error bus (a call actually failed). `degraded` means "we saw a failure and no success since"; coming back online clears it, because the failures counted during an offline window ARE that window. Deliberately no polling ping just to colour an indicator.
|
|
61
|
+
- **@voltro/database** — **`.uniqueActive([cols])` now works on mysql / mariadb** (it previously failed loud at migrate — those engines have no partial index). It lowers automatically to a STORED generated column per key column — `CASE WHEN <predicate> THEN CAST(<col> AS CHAR(255)) ELSE NULL END` — plus a UNIQUE over them. NULL-distinct uniqueness means a soft-deleted row's generated columns are all NULL and never collide, so re-creating the key just works — the same resurrection-safe semantics the partial index gives on postgres/sqlite/mssql. The lowering is applied identically on the emit side and the declared-snapshot side, so it round-trips through the declarative differ (verified live on MariaDB: declare→migrate→introspect→re-plan is a no-diff, and a duplicate among active rows is rejected while soft-deleting one frees the key). You write the same `.uniqueActive([...])` on every dialect.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- **@voltro/cli** — **Standalone `voltro codegen` now emits the SAME `rpcGroup.generated.ts` as `voltro dev` / `voltro build`.** Previously the standalone command skipped the app's plugins, so it dropped every plugin's cross-cutting error union AND its client RPC routes — an inconsistent generated file (typed plugin errors gone, plugin route tags unresolved) vs the dev/serve boot path. The command now loads `app.config` and gathers the plugins' codegen inputs through the SAME `gatherCodegenPluginImports` the boot path uses (single-sourced, so the three paths can't drift).
|
|
66
|
+
- **@voltro/cli** — **The hook-coverage gate was too weak, and it let dead documentation through.** It asked only whether a hook's NAME appeared in the agent-docs corpus — which a one-line row in a link table satisfies. Ten client hooks had exactly that, and six of those rows linked to a page that never mentions the hook, so a reader following the link learned nothing. Two sharper checks replace it: - **Substance** (`agentsMdTemplate.test.ts`): a hook must appear at least once OUTSIDE a table row, so a bare row no longer counts as documentation. - **Dead references** (`gen-agent-docs.mjs`): a row promising `[`useX`](/docs/…)` must point at a page that actually mentions `useX`. This lives in the generator because that is the only thing which reads the docs tree — and it walks BOTH language trees, since an en-only check leaves every German dead link invisible.
|
|
67
|
+
- **@voltro/cli** — - **Serve bundle: inline the pure-JS SQL drivers (`pg`/`mysql2`/`tedious`) instead of runtime-shimming them.** A production `voltro serve` with a postgres/mysql/mssql store crashed at boot with `Pg.Pool is not a constructor` (and the mysql/mssql equivalents): the driver's `@effect/sql-*` consumer links its leaf as a namespace (`import * as Pg from 'pg'; new Pg.Pool()`), but the native-leaf CJS shim (`module.exports = <leaf>`) did not surface the named members through esbuild's `import * as` interop, so `Pg.Pool` was `undefined`. These drivers are pure JS (no `.node`), so they are now inlined into the serve bundle like the rest of the framework — esbuild links the real module and the namespace resolves correctly. Only genuinely-native leaves (`better-sqlite3`, the turso/libsql addons, `pg-native`) and the dynamically-imported `ioredis`/`nodemailer` still resolve via the runtime shim. Verified end-to-end from a relocated prod tree (a real `POST /rpc` through the inlined `pg`).
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
42
71
|
## [0.3.0] — 2026-07-18
|
|
43
72
|
|
|
44
73
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -180,7 +180,7 @@ export declare interface AutoApplyTarget {
|
|
|
180
180
|
readonly op: 'insert' | 'update' | 'delete';
|
|
181
181
|
readonly order?: 'prepend' | 'append' | undefined;
|
|
182
182
|
readonly shape?: ((input: Record<string, unknown>, optimisticIdOrCurrent?: unknown) => Record<string, unknown>) | undefined;
|
|
183
|
-
readonly identify?: ((input: Record<string, unknown>) => string) | undefined;
|
|
183
|
+
readonly identify?: ((input: Record<string, unknown>) => string | ReadonlyArray<string>) | undefined;
|
|
184
184
|
/** Dot-path to a nested array WITHIN the query value (e.g.
|
|
185
185
|
* `'snapshot.projects'`). When set, the reducer patches that nested array at
|
|
186
186
|
* ITEM granularity instead of the top-level row array — for JSON array
|
|
@@ -192,6 +192,10 @@ export declare interface AutoApplyTarget {
|
|
|
192
192
|
* value satisfies this. Stops a patch bleeding across sibling subscriptions
|
|
193
193
|
* sharing a source table. */
|
|
194
194
|
readonly match?: ((value: unknown, input: Record<string, unknown>) => boolean) | undefined;
|
|
195
|
+
/** Nested-item shaper — the client uses this (not `shape`) to build/patch a
|
|
196
|
+
* nested-array item when `path` is set. Typed to the item, not the output.
|
|
197
|
+
* 2nd arg is the optimistic id (insert) or the current item (update). */
|
|
198
|
+
readonly shapeItem?: ((input: Record<string, unknown>, currentOrOptimisticId: unknown) => Record<string, unknown>) | undefined;
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
declare interface CacheEntry {
|
|
@@ -268,6 +272,10 @@ export declare interface CacheSnapshot<T> {
|
|
|
268
272
|
* an empty requirement is always allowed. Pure — the matcher core. */
|
|
269
273
|
export declare const canCall: (subjectScopes: ReadonlyArray<string>, required: string | ReadonlyArray<string>) => boolean;
|
|
270
274
|
|
|
275
|
+
/** OR variant of `canCall` — satisfied by holding AT LEAST ONE of `required`.
|
|
276
|
+
* An empty requirement is allowed (nothing is being demanded). Pure. */
|
|
277
|
+
export declare const canCallAny: (subjectScopes: ReadonlyArray<string>, required: ReadonlyArray<string>) => boolean;
|
|
278
|
+
|
|
271
279
|
/** Structural mirror of @voltro/cli's `CapabilityManifestTable` column — the
|
|
272
280
|
* client stays decoupled from the (node-only) cli package. */
|
|
273
281
|
export declare interface CapabilityColumn {
|
|
@@ -370,6 +378,18 @@ export declare const computeWindow: (args: {
|
|
|
370
378
|
readonly total?: number;
|
|
371
379
|
}) => WindowSpec;
|
|
372
380
|
|
|
381
|
+
export declare interface ConnectionState {
|
|
382
|
+
readonly status: ConnectionStatus;
|
|
383
|
+
/** `navigator.onLine` (true during SSR — nothing better is knowable there). */
|
|
384
|
+
readonly online: boolean;
|
|
385
|
+
/** Consecutive rpc failures with no success in between. 0 when healthy. */
|
|
386
|
+
readonly failureCount: number;
|
|
387
|
+
/** When the most recent failure was observed. */
|
|
388
|
+
readonly lastFailureAt: number | undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export declare type ConnectionStatus = 'connected' | 'degraded' | 'offline';
|
|
392
|
+
|
|
373
393
|
/**
|
|
374
394
|
* The wire shape a data-copilot action returns. Mirror it in the action's
|
|
375
395
|
* `output` schema — a refusal carries the human-readable `reason` (derived
|
|
@@ -599,6 +619,9 @@ export declare interface FrameworkRuntimesProviderProps {
|
|
|
599
619
|
readonly children: ReactNode;
|
|
600
620
|
}
|
|
601
621
|
|
|
622
|
+
/** The registered notifier, if any. */
|
|
623
|
+
export declare const getMutationNotifier: () => MutationNotifier | undefined;
|
|
624
|
+
|
|
602
625
|
/** Read the current feed. Newest-first; up to MAX_MUTATIONS entries. */
|
|
603
626
|
export declare const getMutations: () => ReadonlyArray<MutationEvent>;
|
|
604
627
|
|
|
@@ -637,6 +660,21 @@ export declare const markFailed: (queue: Outbox, id: string, error: unknown) =>
|
|
|
637
660
|
|
|
638
661
|
export declare const markSent: (queue: Outbox, id: string) => Outbox;
|
|
639
662
|
|
|
663
|
+
export declare interface MutateOptions<Input, Output> {
|
|
664
|
+
/** Ran after the server confirms the write. */
|
|
665
|
+
readonly onSuccess?: (output: Output, input: Input) => void;
|
|
666
|
+
/** Ran on failure. Providing it marks the error handled → `mutate` resolves
|
|
667
|
+
* with `undefined` rather than rejecting. */
|
|
668
|
+
readonly onError?: (error: unknown, input: Input) => void;
|
|
669
|
+
/** Declarative sugar over the app-wide notifier (see `setMutationNotifier`).
|
|
670
|
+
* Values are passed through verbatim — resolve i18n yourself. Setting
|
|
671
|
+
* `error` also marks the failure handled. */
|
|
672
|
+
readonly notify?: {
|
|
673
|
+
readonly success?: string | ((output: Output, input: Input) => string);
|
|
674
|
+
readonly error?: string | ((error: unknown, input: Input) => string);
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
640
678
|
export declare interface MutationBuilder<Input, Output> extends MutationState<Input, Output> {
|
|
641
679
|
/** Override auto-optimistic with a custom reducer. */
|
|
642
680
|
readonly withOptimistic: (optimistic: OptimisticFn<Input>) => MutationBuilder<Input, Output>;
|
|
@@ -670,8 +708,21 @@ export declare interface MutationEvent {
|
|
|
670
708
|
readonly durationMs?: number;
|
|
671
709
|
}
|
|
672
710
|
|
|
711
|
+
/** The app's toast/notification sink. Deliberately NOT baked to any library —
|
|
712
|
+
* register your own once at boot (sonner, react-hot-toast, a custom banner). */
|
|
713
|
+
export declare interface MutationNotifier {
|
|
714
|
+
readonly success: (message: string) => void;
|
|
715
|
+
readonly error: (message: string) => void;
|
|
716
|
+
}
|
|
717
|
+
|
|
673
718
|
export declare interface MutationState<Input, Output> {
|
|
674
|
-
|
|
719
|
+
/** Run the mutation. With no options it rejects on failure (unchanged).
|
|
720
|
+
* With an error handler (`onError` / `notify.error`) the failure is handled
|
|
721
|
+
* and the promise resolves with `undefined` instead. */
|
|
722
|
+
readonly mutate: {
|
|
723
|
+
(input: Input): Promise<Output>;
|
|
724
|
+
(input: Input, options: MutateOptions<Input, Output>): Promise<Output | undefined>;
|
|
725
|
+
};
|
|
675
726
|
readonly pending: boolean;
|
|
676
727
|
readonly error: unknown | undefined;
|
|
677
728
|
readonly data: Output | undefined;
|
|
@@ -1008,6 +1059,10 @@ export declare interface SeqElement<E> {
|
|
|
1008
1059
|
readonly event: E;
|
|
1009
1060
|
}
|
|
1010
1061
|
|
|
1062
|
+
/** Register (or clear) the app-wide notifier that `notify:` routes to. Call once
|
|
1063
|
+
* at boot, next to your toast provider. */
|
|
1064
|
+
export declare const setMutationNotifier: (notifier: MutationNotifier | undefined) => void;
|
|
1065
|
+
|
|
1011
1066
|
/**
|
|
1012
1067
|
* Transition a pending mutation to 'success' or 'error'. Updates the
|
|
1013
1068
|
* existing entry in-place (preserves order in the buffer) so the
|
|
@@ -1234,8 +1289,16 @@ export declare interface SubscriptionCacheOptions {
|
|
|
1234
1289
|
* Convex's `"skip"` sentinel without overloading the
|
|
1235
1290
|
* input argument.
|
|
1236
1291
|
*/
|
|
1237
|
-
export declare interface SubscriptionOptions {
|
|
1292
|
+
export declare interface SubscriptionOptions<T = unknown> {
|
|
1238
1293
|
readonly skip?: boolean;
|
|
1294
|
+
/**
|
|
1295
|
+
* Value exposed as `data` until the first server snapshot arrives. With it,
|
|
1296
|
+
* `data` is never `undefined`, so a list page can render its real (empty)
|
|
1297
|
+
* shell immediately instead of branching on `undefined`. `loading` still
|
|
1298
|
+
* reports the true state — the fallback is a rendering convenience, never a
|
|
1299
|
+
* claim that data arrived.
|
|
1300
|
+
*/
|
|
1301
|
+
readonly fallback?: T;
|
|
1239
1302
|
}
|
|
1240
1303
|
|
|
1241
1304
|
export declare interface SubscriptionState<T> {
|
|
@@ -1246,11 +1309,22 @@ export declare interface SubscriptionState<T> {
|
|
|
1246
1309
|
readonly revision: number;
|
|
1247
1310
|
/** Wall-clock ms when the most recent delta was emitted; undefined for snapshots / optimistic. */
|
|
1248
1311
|
readonly emittedAt: number | undefined;
|
|
1249
|
-
/** Stream error
|
|
1312
|
+
/** Stream error — surfaced ONLY when no snapshot has ever arrived (a cold-start
|
|
1313
|
+
* failure). A failure AFTER data arrived deliberately does NOT replace good
|
|
1314
|
+
* data with an error: a transient ws hiccup would otherwise blank a working
|
|
1315
|
+
* screen. Those still reach the api's error bus — subscribe with
|
|
1316
|
+
* `useOnRpcError` for connection-level UX. */
|
|
1250
1317
|
readonly error: unknown | undefined;
|
|
1251
1318
|
/** Number of live optimistic patches currently applied. Useful for
|
|
1252
1319
|
* rendering a faint "syncing…" indicator when > 0. */
|
|
1253
1320
|
readonly pendingPatches: number;
|
|
1321
|
+
/** No snapshot yet — render a skeleton. Equivalent to `revision < 0`, but says
|
|
1322
|
+
* what it means, so call sites stop hand-writing `data === undefined ? …`. */
|
|
1323
|
+
readonly loading: boolean;
|
|
1324
|
+
/** Data HAS arrived and is empty (empty array, or a null/undefined value).
|
|
1325
|
+
* `loading` and `isEmpty` are DIFFERENT states — conflating them is what
|
|
1326
|
+
* causes a flash of empty-state before the first snapshot. */
|
|
1327
|
+
readonly isEmpty: boolean;
|
|
1254
1328
|
}
|
|
1255
1329
|
|
|
1256
1330
|
/** A map entry: a bare event name, or a function of the component's props. */
|
|
@@ -1464,6 +1538,10 @@ export declare interface UseAsyncValidationOptions {
|
|
|
1464
1538
|
* scope(s)? Use to gate UI. Defaults to false (deny) with no provider. */
|
|
1465
1539
|
export declare const useCan: (required: string | ReadonlyArray<string>) => boolean;
|
|
1466
1540
|
|
|
1541
|
+
/** Reactive OR check — true if the subject holds at least one of `required`.
|
|
1542
|
+
* Use for "this section is visible to editors OR reviewers" affordances. */
|
|
1543
|
+
export declare const useCanAny: (required: ReadonlyArray<string>) => boolean;
|
|
1544
|
+
|
|
1467
1545
|
/**
|
|
1468
1546
|
* Fetch the api's capability manifest (procedures + tables + schemas) once.
|
|
1469
1547
|
* Browser-safe — imports only React + the runtime context, exactly like
|
|
@@ -1474,6 +1552,24 @@ export declare const useCan: (required: string | ReadonlyArray<string>) => boole
|
|
|
1474
1552
|
*/
|
|
1475
1553
|
export declare const useCapabilityManifest: (apiName: string) => CapabilityManifestState;
|
|
1476
1554
|
|
|
1555
|
+
/**
|
|
1556
|
+
* Observe connection health for one mounted api.
|
|
1557
|
+
*
|
|
1558
|
+
* ```tsx
|
|
1559
|
+
* const { status } = useConnectionStatus('app')
|
|
1560
|
+
* {status !== 'connected' && <OfflineBanner status={status} />}
|
|
1561
|
+
* ```
|
|
1562
|
+
*
|
|
1563
|
+
* `degraded` clears as soon as any rpc on that api succeeds again — call
|
|
1564
|
+
* `reportSuccess()` from a place that knows a call went through if you want to
|
|
1565
|
+
* clear it eagerly (the hook already clears it when the browser comes back
|
|
1566
|
+
* online).
|
|
1567
|
+
*/
|
|
1568
|
+
export declare const useConnectionStatus: (apiName: string) => ConnectionState & {
|
|
1569
|
+
/** Clear the degraded state — call after a known-good round trip. */
|
|
1570
|
+
readonly reportSuccess: () => void;
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1477
1573
|
/**
|
|
1478
1574
|
* Bind a data-copilot action by api name + tag. The action's input is
|
|
1479
1575
|
* `{ question }` and its output is a {@link CopilotAnswer}. Generic over the
|