@voltro/runtime 0.78.0 → 0.79.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 +58 -0
- package/dist/index.d.ts +175 -16
- package/dist/index.js +2586 -2423
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,64 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.79.0] — 2026-09-23
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **Probes answer on the app port from the first moment, on every command** — `@voltro/cli`, `@voltro/runtime`, `@voltro/voltro`
|
|
47
|
+
|
|
48
|
+
`voltro dev`, `voltro serve` and `voltro start` now bind their app port as the FIRST thing they do once they know it, and answer `/internal/liveness` (200), `/internal/readiness` (503 until the app serves, then 200 with the dependency ping) and `/internal/startup` (JSON `{ phase, ready, elapsedMs, … }`, 503 → 200) there through the whole boot — a slow database connect, a fingerprint check or a cold migrate included. Every other request and every WebSocket upgrade gets `503 + Retry-After: 1` until ready, then the app serves on the same socket and the probe paths keep answering before routing. There is ONE server and ONE port, so a probe path means the same thing on every command; `voltro dev` no longer binds a second probe server on app port + 1 and no longer reads `VOLTRO_DEV_HEALTH_PORT`. Migration: point every probe of a pod running `voltro dev` at the app port and delete the variable — the codemod prints the exact steps and finds the variable in compose, Helm and CI files.
|
|
49
|
+
|
|
50
|
+
**`voltro update` carries you across this** — codemod `0.79.0/01_probes-on-the-app-port`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.79.0).
|
|
51
|
+
- **A rejected credential is refused on the default tenant policy too** — `@voltro/cli`, `@voltro/protocol`, `@voltro/voltro`
|
|
52
|
+
|
|
53
|
+
A request whose credential a strategy rejects (an expired or malformed token, a revoked session or key) now ends with `Unauthenticated` (`credential rejected: <reason>`) on every anonymous-tenant policy. Until now that held under `auth: { anonymousTenantId }` and `auth: { anonymousTenantRequired: true }`, but not for an app that sets neither. There, a procedure without a guard served the caller as an anonymous one, and an open query answered public data to a token that had just been refused. A caller who sends no credential is unaffected: it still resolves to the tenant its `x-tenant` header names, or to the development tenant. `composeAuthStrategies` gains `anonymousTenantDefault` for that default tenant. Unlike a `fallback`, it does not take over the rejected-credential decision. A client that kept sending a stale cookie or token and relied on the anonymous answer now receives `Unauthenticated`, which the error bus reports and `wireAuthRefresh` acts on.
|
|
54
|
+
|
|
55
|
+
**`voltro update` carries you across this** — codemod `0.79.0/03_rejected-credentials-refused-on-the-default-policy`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.79.0).
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **A 1366 over a 4-byte character says which config field fixes it** — `@voltro/database`
|
|
60
|
+
|
|
61
|
+
When MySQL or MariaDB refuse a write with `Incorrect string value: '\xF0…'` (errno 1366 over a four-byte UTF-8 lead byte — an emoji into a `utf8mb3` column), the store's `detail:` line now carries a `hint:` naming the column and the remedy: declare `store: { dialect, charset: 'utf8mb4' }` in `app.config.ts` and run `voltro db plan`. A 1366 for any other reason gets no advice about character sets.
|
|
62
|
+
- **The store's character set is declared once and every table follows it** — `@voltro/database`, `@voltro/cli`, `@voltro/devtools-ui`
|
|
63
|
+
|
|
64
|
+
On MySQL and MariaDB the framework's `CREATE TABLE` named no character set, so a `text()` column inherited whatever the database was created with while a `json()` column did not (MariaDB's `JSON` is `LONGTEXT CHARACTER SET utf8mb4`, MySQL's has none) — and on a `utf8mb3` database one table refused an emoji in its text column with errno 1366 and took it in its json column. `app.config.ts` now takes `store: { dialect: 'mariadb' | 'mysql', charset: 'utf8mb4' | 'utf8mb3' }`: every declared table is created with it as its default and, when a live table's string columns are stored otherwise, converted to it by a new `convert-charset` plan operation — a table rewrite that drops and re-creates the foreign keys touching the table, so a pair bound by a constraint converts together instead of one side failing with errno 150. Widening to `utf8mb4` is `safe`; narrowing to `utf8mb3` is `lossy` and waits for the destructive opt-in. json columns keep the engine's own charset (neither engine lets it be declared). Unset, nothing changes. A charset on any other dialect is refused at boot, and a `DB_DIALECT` override off the mysql family drops it with a logged line rather than silently.
|
|
65
|
+
|
|
66
|
+
The public signatures that changed only gain optional trailing parameters (`declaredSnapshot`, `emitNamespacedSchemaSql`, `emitNamespaceProvisionDdl`, the schema-emit options) and one union member (`convert-charset` in the migration operation kind), so existing calls compile unchanged. Only an exhaustive `switch` over that operation kind would need a new case.
|
|
67
|
+
- **The plan and the doctor name tables whose text cannot hold what json can** — `@voltro/cli`, `@voltro/database`
|
|
68
|
+
|
|
69
|
+
Without a declared store charset, `voltro db plan` prints — under the plan — the MySQL / MariaDB tables whose text columns are stored in a character set narrower than their json columns' `utf8mb4`, and the tables whose string columns are not all in one character set (a column converted by hand). `voltro doctor` has the same section, reading `information_schema`, with three outcomes that never print alike: not applicable off the mysql family, not checked with the reason when no database is reachable, and the findings — against the declaration when one is set. A healthy `utf8mb4` database reports nothing; in `--json` the section is `storeCharset`.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- **A handler's own `Unauthenticated` reaches the client typed, on any procedure** — `@voltro/protocol`, `@voltro/cli`, `@voltro/runtime`
|
|
74
|
+
|
|
75
|
+
`Unauthenticated` and `ScopeError` are now members of every procedure's wire error union — queries, mutations, actions, streams and workflow starts, whether or not the descriptor declares a guard — and `Unauthenticated` of every event's. They used to ride only a procedure with an enforced `guards:` entry, on the reasoning that an open procedure cannot produce them. It can: the auth middleware refuses an open procedure with a rejected credential exactly as it refuses a guarded one, and a handler may refuse on its own with `assertAuthenticated` or `requireScope`. On a procedure without a guard the server judged that handler-side refusal undeclared and collapsed it, so the browser received `{ _tag: 'InternalError', message: 'Unauthenticated: …' }` — the typed error's name inside a defect that no `_tag` match and no credential-refresh policy could see — and the generated client had no schema to decode the middleware's own refusal with. Both ends now carry the same union, so the value arrives as `{ _tag: 'Unauthenticated' }` on the failure channel over `POST /rpc` and over the WebSocket, and a gRPC surface maps it to `UNAUTHENTICATED` rather than `FAILED_PRECONDITION`. Measured under `voltro dev` and `voltro serve` with a validly signed, expired JWT driven through the real client over the socket.
|
|
76
|
+
- **`voltro build` walks an api app's module closure once, not once per handler file** — `@voltro/cli`
|
|
77
|
+
|
|
78
|
+
Each api artefact (`apiEntry.js`, the serve bundle) is preceded by a closure-discovery pass that only reads esbuild's metafile. It used to hand esbuild every discovery file as its own entry point, and without code splitting esbuild links every entry point separately, each on its own copy of the module graph, all at once. On a synthetic api app of 6,747 modules (4,514 handler files) that probe alone peaked at 4.3 GiB in the esbuild service — ten times the bundle it precedes — and it ran twice per build, so the whole build peaked at 5–6 GiB and could be refused by a shared host with less free memory than that. The probe now links one generated entry that imports every discovery file; the closure it finds is the same set. Measured on the same app the build's peak is under 2 GiB (esbuild 1.9 GiB, Node 0.5 GiB), at 3,048 modules 1.0 GiB, at 1,047 modules 0.9 GiB. The build page documents the expectation.
|
|
79
|
+
- **The file-migration `phase` rewrite reaches aliased, held and nested specs** — `@voltro/cli`
|
|
80
|
+
|
|
81
|
+
For a project crossing 0.78.0, the `0.78.0/01_file-migration-phase` rewrite now also finds `migration` imported under another name, a spec held in a local `const`, and migrations in subdirectories of `migrations/`. All three are shapes the runner discovers, and the rewrite used to skip them without a word. A spec built outside the call (by a helper, or imported) is not rewritten, because the change would land in code other callers share. Instead the rewrite names it by file and line. `migration()` still refuses a spec without a phase when the file loads.
|
|
82
|
+
- **A subscription refused in-band reaches the error bus and the refresh policy** — `@voltro/client`
|
|
83
|
+
|
|
84
|
+
A guard's refusal on a subscription travels in-band — a `{ _tag: 'error' }` event on that one stream, so a sibling subscription on the shared socket is not stalled. The subscription cache set the snapshot's error from it and told nobody else: the api's error bus, which `wireAuthRefresh` and `useOnRpcError` listen on, heard only streams that failed outright. So an expired credential on a read never asked the host for a fresh token and never reached an error reporter. The in-band value is now emitted to the bus with `source: 'subscription'` and `kind: 'handler'`, the same event a failed stream produces.
|
|
85
|
+
- **`voltro support collect` no longer fails on a temporary directory under src/** — `@voltro/cli`
|
|
86
|
+
|
|
87
|
+
The conflict-key inventory in `voltro support collect` found its files with a glob that skips dot-directories, then loaded them through a call that walked the directories again. So a temporary directory that an editor, a test run or a build tool creates and removes under `src/` could fail the whole collection with `Directory not found`, and an unreadable dot-directory failed it with `EACCES`. Only the files the glob returned are loaded now. A file removed between the two steps is skipped.
|
|
88
|
+
- **Tenant namespaces on MySQL / MariaDB provision without USE and take the charset** — `@voltro/database`, `@voltro/cli`
|
|
89
|
+
|
|
90
|
+
Provisioning a tenant namespace on the mysql family emitted `USE <database>` followed by unqualified DDL. MySQL 8.4 refuses `USE` in the prepared protocol (errno 1295), so under `tenancy.isolation: 'namespace'` no tenant could be provisioned on MySQL at all; on MariaDB the `USE` outlived its transaction on the pooled connection, which then served its next unqualified statement in the tenant database. The tenant DDL is now database-qualified, as the store's namespace view already was. The declared store charset reaches the tenant too: a new tenant database is created with it as its default and its tables carry it. A tenant provisioned before the declaration is named per namespace by `voltro db plan` and `voltro doctor` rather than silently left as it was.
|
|
91
|
+
- **`voltro update` moves the `@voltro/*` pins in `pnpm-workspace.yaml` too** — `@voltro/cli`
|
|
92
|
+
|
|
93
|
+
`voltro update` used to bump only the package.json files. A pnpm workspace that also names framework versions in `pnpm-workspace.yaml` was left behind in two ways. `overrides:` entries held the tree on the old release. And `minimumReleaseAgeExclude:` entries (`'@voltro/<pkg>@<version>'`, which admit a release younger than pnpm's release-age window) went stale, so pnpm refused the new release for that whole window. Both blocks now move with the bump. An entry moves only when it names exactly the version being left; an entry at any other version is printed as `kept`. A block written as a flow list is named rather than rewritten. The packages named there count for `--wait`, and the file is restored along with the package.json files when an install fails before anything lands.
|
|
94
|
+
- **`voltro update --wait` waits up to 45 minutes, longer than a release holds** — `@voltro/cli`
|
|
95
|
+
|
|
96
|
+
`voltro update --wait` polls the registry until every package it bumps is published at the target version, `@voltro/dashboard` included. It used to give up after 20 minutes. A release, though, holds `latest` for up to 30 minutes after its other packages finish uploading while `@voltro/dashboard` is still being built. So an update that started polling at the first upload could fail after a release that went on to complete normally. The ceiling is now 45 minutes, which covers the release's hold plus its upload phase. The update still reports the late packages while it waits, and on timeout it still restores the bumps byte for byte.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
42
100
|
## [0.78.0] — 2026-09-22
|
|
43
101
|
|
|
44
102
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { DataStore } from '@voltro/database';
|
|
|
24
24
|
import { DeclaredAccess } from '@voltro/protocol';
|
|
25
25
|
import { DialectId } from '@voltro/database';
|
|
26
26
|
import { DialectReplicationAdapter } from '@voltro/database';
|
|
27
|
+
import { Duplex } from 'node:stream';
|
|
27
28
|
import { Duration } from 'effect';
|
|
28
29
|
import { Effect } from 'effect';
|
|
29
30
|
import { EventDeliverySemantics } from '@voltro/protocol';
|
|
@@ -44,6 +45,7 @@ import { HttpRequestInterceptor } from '@voltro/protocol';
|
|
|
44
45
|
import { HttpRouteObserver } from '@voltro/protocol';
|
|
45
46
|
import { IdempotencyStore } from '@voltro/protocol';
|
|
46
47
|
import { IdScheme } from '@voltro/database';
|
|
48
|
+
import { IncomingMessage } from 'node:http';
|
|
47
49
|
import { InferInsertRow } from '@voltro/database';
|
|
48
50
|
import { InferRow } from '@voltro/database';
|
|
49
51
|
import { InMemoryMetricExporter } from '@opentelemetry/sdk-metrics';
|
|
@@ -94,6 +96,7 @@ import { Schema } from 'effect';
|
|
|
94
96
|
import { ScopeError } from '@voltro/protocol';
|
|
95
97
|
import { Server } from 'node:http';
|
|
96
98
|
import { ServerOnlyColumnWrite } from '@voltro/protocol';
|
|
99
|
+
import { ServerResponse } from 'node:http';
|
|
97
100
|
import { SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
98
101
|
import { spawn } from 'node:child_process';
|
|
99
102
|
import { SqlClient } from '@effect/sql';
|
|
@@ -868,6 +871,23 @@ export declare interface AnalyticsTopEntry {
|
|
|
868
871
|
readonly value: number;
|
|
869
872
|
}
|
|
870
873
|
|
|
874
|
+
/**
|
|
875
|
+
* Answer one probe.
|
|
876
|
+
*
|
|
877
|
+
* A THROWN readiness check is `not-ready`, not a 500: an app's own probe
|
|
878
|
+
* throwing is exactly the state the probe exists to report, and a 500 would be
|
|
879
|
+
* read by some ingress configurations as an error page rather than a failed
|
|
880
|
+
* check.
|
|
881
|
+
*
|
|
882
|
+
* `startup` answers as JSON when the boot path can describe its phase (every
|
|
883
|
+
* path that binds the early listener), `503` until the boot finished and `200`
|
|
884
|
+
* from then on — so a `startupProbe` pointed at it holds liveness back for
|
|
885
|
+
* exactly as long as the boot takes, and a human reading it sees WHICH step it
|
|
886
|
+
* is on. A path with no phase to report (the web dev server, whose socket Vite
|
|
887
|
+
* owns) answers it as the readiness question.
|
|
888
|
+
*/
|
|
889
|
+
export declare const answerProbe: (health: ResolvedHealth, kind: ProbeKind, startup?: () => StartupSnapshot) => Promise<ProbeAnswer>;
|
|
890
|
+
|
|
871
891
|
/**
|
|
872
892
|
* Answer a `resumeFrom` for this identity. `fromRevision` is the last
|
|
873
893
|
* revision the CLIENT materialised. Only revisions inside
|
|
@@ -1670,6 +1690,28 @@ export declare interface BeginOAuthResult {
|
|
|
1670
1690
|
readonly state: string;
|
|
1671
1691
|
}
|
|
1672
1692
|
|
|
1693
|
+
/**
|
|
1694
|
+
* Bind the app port NOW and answer probes on it until the boot path hands
|
|
1695
|
+
* over. Rejects when the bind fails (`EADDRINUSE`, `EACCES`): a serving
|
|
1696
|
+
* process that cannot hold its port has nothing to do, and the caller
|
|
1697
|
+
* reports the failure the same way it reported a late bind failure before.
|
|
1698
|
+
*/
|
|
1699
|
+
export declare const bindBootListener: (opts: BindBootListenerOptions) => Promise<BootListener>;
|
|
1700
|
+
|
|
1701
|
+
export declare interface BindBootListenerOptions {
|
|
1702
|
+
readonly port: number;
|
|
1703
|
+
/** Absent → wildcard (dual-stack `::` on Linux), the production default and
|
|
1704
|
+
* what the rpc listener binds. */
|
|
1705
|
+
readonly host?: string;
|
|
1706
|
+
/** The app's `health` block — the prefix and the app's own answers. */
|
|
1707
|
+
readonly health?: HealthConfigInput;
|
|
1708
|
+
/** Deterministic "now" for `elapsedMs`; tests inject. */
|
|
1709
|
+
readonly now?: () => number;
|
|
1710
|
+
readonly log?: {
|
|
1711
|
+
readonly info: (message: string, fields?: Record<string, unknown>) => void;
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1673
1715
|
/** Exported for the backpressure suite — the production caller is `bindSubscription`. */
|
|
1674
1716
|
export declare const bindComputedUntyped: (computed: ComputedQuery, dispatcher: Dispatcher, context: RuntimeContext, label: string,
|
|
1675
1717
|
/** Tunables override (tests); production reads the process-wide slot. */
|
|
@@ -1847,11 +1889,51 @@ export declare const bindSubscriptionUntyped: (descriptor: QueryDescriptor, disp
|
|
|
1847
1889
|
|
|
1848
1890
|
export declare const BOOT_REFUSAL_NAME = "VoltroBootRefusal";
|
|
1849
1891
|
|
|
1892
|
+
export declare interface BootListener {
|
|
1893
|
+
/** The bound server — the ONE socket this process serves on. */
|
|
1894
|
+
readonly server: Server;
|
|
1895
|
+
/** The port actually bound (differs from the requested one for `port: 0`). */
|
|
1896
|
+
readonly port: number;
|
|
1897
|
+
readonly state: BootState;
|
|
1898
|
+
/** Advance the phase; the next probe reflects it immediately. */
|
|
1899
|
+
readonly set: (patch: Partial<Omit<BootState, 'startedAt'>>) => void;
|
|
1900
|
+
/** Boot complete — the phase alone, never the dependency ping. */
|
|
1901
|
+
readonly isReady: () => boolean;
|
|
1902
|
+
/** Hand over the real handlers. Consulted only once the phase is `ready`
|
|
1903
|
+
* (or `draining`); until then every non-probe request answers 503. */
|
|
1904
|
+
readonly delegate: (to: BootListenerDelegate) => void;
|
|
1905
|
+
/** The dependency probe readiness consults once ready — a DB `SELECT 1`.
|
|
1906
|
+
* Installed by the boot path when its store exists, which is after the
|
|
1907
|
+
* bind; an app-declared `health.readiness` replaces the whole check. */
|
|
1908
|
+
readonly setDependencyPing: (ping: () => Promise<boolean>) => void;
|
|
1909
|
+
/** Close the socket. Resolves once node reports the listener closed. */
|
|
1910
|
+
readonly close: () => Promise<void>;
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/** The real handlers, once the boot path has them. */
|
|
1914
|
+
export declare interface BootListenerDelegate {
|
|
1915
|
+
readonly request: (request: IncomingMessage, response: ServerResponse) => void;
|
|
1916
|
+
readonly upgrade: (request: IncomingMessage, socket: Duplex, head: Buffer) => void;
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
export declare type BootPhase = 'booting' | 'migrating' | 'ready' | 'draining' | 'error';
|
|
1920
|
+
|
|
1850
1921
|
/** An error the boot path raised on purpose. The launcher prints its message
|
|
1851
1922
|
* once and exits 1 instead of degrading to a fallback path or printing a
|
|
1852
1923
|
* stack. */
|
|
1853
1924
|
export declare const bootRefusal: (message: string) => Error;
|
|
1854
1925
|
|
|
1926
|
+
export declare interface BootState {
|
|
1927
|
+
readonly phase: BootPhase;
|
|
1928
|
+
/** Epoch ms when the listener bound — the base of `elapsedMs`. */
|
|
1929
|
+
readonly startedAt: number;
|
|
1930
|
+
/** Declared table count, set when a migrate phase begins, so a watcher can
|
|
1931
|
+
* gauge how much schema work a slow boot is doing. */
|
|
1932
|
+
readonly tablesTotal?: number;
|
|
1933
|
+
/** Present only in `error` — the boot failure message. */
|
|
1934
|
+
readonly error?: string;
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1855
1937
|
export declare interface BrandedScheduleDefinition extends ScheduleDefinition {
|
|
1856
1938
|
readonly [SCHEDULE_BRAND]: true;
|
|
1857
1939
|
}
|
|
@@ -5508,6 +5590,37 @@ export declare const hashUnitInterval: (s: string) => number;
|
|
|
5508
5590
|
/** How much longer than `keepAliveTimeout` the headers deadline runs. */
|
|
5509
5591
|
export declare const HEADERS_TIMEOUT_MARGIN_MS = 5000;
|
|
5510
5592
|
|
|
5593
|
+
/** The `health` block of `app.config.ts`. Every field optional. */
|
|
5594
|
+
export declare interface HealthConfigInput {
|
|
5595
|
+
/**
|
|
5596
|
+
* Path prefix for the probes. Default `/internal`, giving
|
|
5597
|
+
* `/internal/liveness`, `/internal/readiness` and `/internal/startup`.
|
|
5598
|
+
*
|
|
5599
|
+
* Move it when your app owns a route there — two segments collide sooner than
|
|
5600
|
+
* you would think, and on a web app the ROUTE wins today.
|
|
5601
|
+
*/
|
|
5602
|
+
readonly path?: string;
|
|
5603
|
+
/**
|
|
5604
|
+
* "Is the process up at all" — restart me if not. Default: reaching this code
|
|
5605
|
+
* IS the answer, so `true`.
|
|
5606
|
+
*
|
|
5607
|
+
* Keep it cheap and keep it independent of dependencies. A liveness probe that
|
|
5608
|
+
* consults a database restarts pods when the database is slow, which is the
|
|
5609
|
+
* one thing that makes an outage worse.
|
|
5610
|
+
*/
|
|
5611
|
+
readonly liveness?: () => boolean | Promise<boolean>;
|
|
5612
|
+
/**
|
|
5613
|
+
* "Should I receive traffic" — pulled from the Service endpoints until this
|
|
5614
|
+
* says yes. Default: the boot-complete flag the boot path supplies.
|
|
5615
|
+
*
|
|
5616
|
+
* This is the one worth writing yourself, and the answers genuinely differ: a
|
|
5617
|
+
* dependency ping for an api, "the catalog is loaded" for an app that renders
|
|
5618
|
+
* from one, `true` for a display that must keep showing its last frame while
|
|
5619
|
+
* its upstream wobbles.
|
|
5620
|
+
*/
|
|
5621
|
+
readonly readiness?: () => boolean | Promise<boolean>;
|
|
5622
|
+
}
|
|
5623
|
+
|
|
5511
5624
|
/** Define a histogram. `boundaries` default to the framework duration buckets. */
|
|
5512
5625
|
export declare const histogramMetric: (name: string, boundaries?: MetricBoundaries.MetricBoundaries, description?: string) => Metric.Metric.Histogram<number>;
|
|
5513
5626
|
|
|
@@ -7844,6 +7957,25 @@ export declare interface PreviewStore {
|
|
|
7844
7957
|
transactional: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T>;
|
|
7845
7958
|
}
|
|
7846
7959
|
|
|
7960
|
+
export declare interface ProbeAnswer {
|
|
7961
|
+
readonly status: number;
|
|
7962
|
+
readonly body: string;
|
|
7963
|
+
/** `text/plain` for the two yes/no probes; `application/json` for a startup
|
|
7964
|
+
* snapshot. */
|
|
7965
|
+
readonly contentType: 'text/plain' | 'application/json';
|
|
7966
|
+
}
|
|
7967
|
+
|
|
7968
|
+
/**
|
|
7969
|
+
* Which probe a URL is, or `undefined`.
|
|
7970
|
+
*
|
|
7971
|
+
* `undefined` for anything else — including a path UNDER the prefix that names
|
|
7972
|
+
* no probe. `/internal/nonsense` must not answer, or the surface stops being a
|
|
7973
|
+
* set of three known questions.
|
|
7974
|
+
*/
|
|
7975
|
+
export declare const probeFor: (health: ResolvedHealth, cleanUrl: string) => ProbeKind | undefined;
|
|
7976
|
+
|
|
7977
|
+
export declare type ProbeKind = 'liveness' | 'readiness' | 'startup';
|
|
7978
|
+
|
|
7847
7979
|
/** Project a stored row into the wire shape. Deliberately total over the
|
|
7848
7980
|
* columns the descriptor declares — and deliberately silent about the token
|
|
7849
7981
|
* columns, which have no wire representation at all. */
|
|
@@ -9211,6 +9343,12 @@ export declare interface ResolvedDevice {
|
|
|
9211
9343
|
*/
|
|
9212
9344
|
export declare const resolveDependentTables: (descriptor: QueryDescriptor) => ReadonlySet<string>;
|
|
9213
9345
|
|
|
9346
|
+
export declare interface ResolvedHealth {
|
|
9347
|
+
readonly path: string;
|
|
9348
|
+
readonly liveness: () => boolean | Promise<boolean>;
|
|
9349
|
+
readonly readiness: () => boolean | Promise<boolean>;
|
|
9350
|
+
}
|
|
9351
|
+
|
|
9214
9352
|
export declare interface ResolvedReactiveConfig {
|
|
9215
9353
|
readonly deliveryConcurrency: number;
|
|
9216
9354
|
readonly rawReadTrackingLimit: number;
|
|
@@ -9258,6 +9396,16 @@ export declare interface ResolvedWorkflowCallerContext extends WorkflowCallerCon
|
|
|
9258
9396
|
readonly rowFilter: RowFilterScope;
|
|
9259
9397
|
}
|
|
9260
9398
|
|
|
9399
|
+
/**
|
|
9400
|
+
* Resolve the app's health block against the boot path's own readiness flag.
|
|
9401
|
+
*
|
|
9402
|
+
* `isReady` is what the boot knows and the app cannot: the server is listening
|
|
9403
|
+
* and the modules are loaded. An app-supplied `readiness` REPLACES it — the app
|
|
9404
|
+
* is the more specific authority, and an app that wants to answer `true` while
|
|
9405
|
+
* its upstream is down must be able to.
|
|
9406
|
+
*/
|
|
9407
|
+
export declare const resolveHealth: (config: HealthConfigInput | undefined, isReady: () => boolean) => ResolvedHealth;
|
|
9408
|
+
|
|
9261
9409
|
export declare const resolveKeepAlive: (config: KeepAliveConfig | null | undefined) => KeepAliveTimeouts;
|
|
9262
9410
|
|
|
9263
9411
|
/**
|
|
@@ -9914,22 +10062,19 @@ export declare interface RpcServerOptions<Rpcs extends Rpc.Any> {
|
|
|
9914
10062
|
*/
|
|
9915
10063
|
readonly pluginHttpInterceptor?: HttpRequestInterceptor;
|
|
9916
10064
|
/**
|
|
9917
|
-
*
|
|
9918
|
-
*
|
|
9919
|
-
*
|
|
9920
|
-
*
|
|
9921
|
-
*
|
|
9922
|
-
*
|
|
9923
|
-
*
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
* stops taking traffic instead of erroring every request. */
|
|
9931
|
-
readonly ping?: () => Promise<boolean>;
|
|
9932
|
-
};
|
|
10065
|
+
* The early listener (`bindBootListener`) that already holds the app port.
|
|
10066
|
+
*
|
|
10067
|
+
* Every serving command binds one as the first thing it does once it knows
|
|
10068
|
+
* its port, so `/internal/…` answers from the first moment of the process
|
|
10069
|
+
* and everything else gets `503 + Retry-After` until the boot path flips it
|
|
10070
|
+
* to `ready`. This server's handlers are then reached THROUGH it, on the same
|
|
10071
|
+
* socket: the platform never listens itself (`PlatformHandlerHost` below).
|
|
10072
|
+
*
|
|
10073
|
+
* Absent → this call binds `port`/`host` itself, with no probe surface — the
|
|
10074
|
+
* shape a test harness wants when it boots a bare rpc listener. A boot path
|
|
10075
|
+
* never omits it; `earlyListenerParity.test.ts` reads them.
|
|
10076
|
+
*/
|
|
10077
|
+
readonly listener?: BootListener;
|
|
9933
10078
|
/**
|
|
9934
10079
|
* Max body size (bytes) accepted on the buffered `POST /rpc` JSON endpoint. A
|
|
9935
10080
|
* JSON rpc mutation/action/query envelope is never legitimately large, so an
|
|
@@ -11280,6 +11425,20 @@ export declare interface StartupContext {
|
|
|
11280
11425
|
/** The default export shape every `*.startup.ts` must provide. */
|
|
11281
11426
|
export declare type StartupFn = (ctx: StartupContext) => void | Promise<void>;
|
|
11282
11427
|
|
|
11428
|
+
/**
|
|
11429
|
+
* What `/internal/startup` reports when the boot path keeps a phase — the
|
|
11430
|
+
* early listener's state (`bootListener.ts`). `ready` is the boot flag alone,
|
|
11431
|
+
* never the dependency ping: a startup probe asks whether the boot FINISHED,
|
|
11432
|
+
* and a database that wobbles afterwards is readiness's question.
|
|
11433
|
+
*/
|
|
11434
|
+
export declare interface StartupSnapshot {
|
|
11435
|
+
readonly phase: string;
|
|
11436
|
+
readonly ready: boolean;
|
|
11437
|
+
readonly elapsedMs: number;
|
|
11438
|
+
readonly tablesTotal?: number;
|
|
11439
|
+
readonly error?: string;
|
|
11440
|
+
}
|
|
11441
|
+
|
|
11283
11442
|
/**
|
|
11284
11443
|
* Write (or replace) the calling subject's credential, encrypted.
|
|
11285
11444
|
*
|