@voltro/cli 0.52.0 → 0.53.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 +229 -0
- package/THIRD-PARTY-NOTICES.md +8311 -3318
- package/dist/{apiBuild-CSFI8QGq.js → apiBuild-CaPfoWku.js} +11 -5
- package/dist/apiBuild-DHtLXYx9.js +2 -0
- package/dist/bin.js +1 -1
- package/dist/build-D-OnvNMf.js +843 -0
- package/dist/{checkCommand-COmqc2cB.js → checkCommand-C5elt0tW.js} +6 -6
- package/dist/{checkCommand-2SbqzukH.js → checkCommand-D2ZduVlh.js} +1 -1
- package/dist/codegen-BWpt3VgF.js +2 -0
- package/dist/{codegen-VF479Cnb.js → codegen-FEk8AZHb.js} +2 -2
- package/dist/{codegenCommand-DCdG2JN-.js → codegenCommand-BOiWQ5hz.js} +4 -4
- package/dist/{codemodRunner-r7J9lIa7.js → codemodRunner-BjtB2lq6.js} +109 -11
- package/dist/{commands-Cc_nV8WI.js → commands-DyxAmhP0.js} +30 -30
- package/dist/{dashboardCommand-C-gKvwqh.js → dashboardCommand-BdKTyT13.js} +5 -5
- package/dist/{dataCommand-BgpBHnlB.js → dataCommand-Bab9X7s8.js} +2 -2
- package/dist/{dbCommand-sHedr-NJ.js → dbCommand-06O2finM.js} +277 -236
- package/dist/dbCommand-B1EXBC6f.js +2 -0
- package/dist/{dev-CRHoCEiy.js → dev-C6LGF4iY.js} +3006 -2426
- package/dist/{dev--A3nsxA3.js → dev-GjJWAYo2.js} +1 -1
- package/dist/doctorCommand-B0hX0tdz.js +2 -0
- package/dist/{doctorCommand-DtfJ3FA6.js → doctorCommand-etMkflRc.js} +101 -69
- package/dist/{dormancyCommand-Drn7o0No.js → dormancyCommand-UwZ1AZzB.js} +1 -1
- package/dist/{embeddingsCommand-Z-jO1fWN.js → embeddingsCommand-C70zWHwo.js} +1 -1
- package/dist/{envCommand-D4gCrrTZ.js → envCommand-dSyKvRkM.js} +18 -18
- package/dist/{evolveCommand-CMROeKeA.js → evolveCommand-CG0_ebO5.js} +2 -2
- package/dist/fileConventions-DASGEmj-.js +35 -0
- package/dist/{fileTaxonomy-DvDUV9wq.js → fileTaxonomy-B7uxipWS.js} +55 -55
- package/dist/fontPipeline-LxIHa1vo.js +2 -0
- package/dist/fontPipeline-Tsh8kZfA.js +152 -0
- package/dist/frameworkTableAssembly-C_7Z-rMs.js +2 -0
- package/dist/{frameworkTableAssembly-w-XnLa3q.js → frameworkTableAssembly-DKx3ba3S.js} +5 -5
- package/dist/imagePipeline-B_GVJgm6.js +2 -0
- package/dist/imagePipeline-CBZmjT4i.js +127 -0
- package/dist/index.js +1 -1
- package/dist/{infoCommand-DXM868o_.js → infoCommand-_53iOc_j.js} +1 -1
- package/dist/{inspect-CtL_xTbu.js → inspect-Bd8-9wsi.js} +1 -1
- package/dist/inspect-CuoDInfZ.js +2 -0
- package/dist/{inspectGateHint-BjnFubmH.js → inspectGateHint-4LxkNtrz.js} +1 -1
- package/dist/manifestBuild-C4-J1-m_.js +2 -0
- package/dist/{manifestBuild-CuU1VrSm.js → manifestBuild-Cqgsx2bM.js} +1 -1
- package/dist/{metaCommands-C6RFmF1r.js → metaCommands-Cn2oboG4.js} +8 -2
- package/dist/{migrate-D0F-eTlK.js → migrate-Cko9rswM.js} +2 -2
- package/dist/{pageConvention-CzUiSbtU.js → pageConvention-C938S8oC.js} +1 -1
- package/dist/{privacyCommand-DGdopOI6.js → privacyCommand-DWTQMC6R.js} +2 -2
- package/dist/{probeCommand-C9gazU0H.js → probeCommand-DkGGLknv.js} +83 -24
- package/dist/{renderModeScan-CcH2X1_D.js → renderModeScan-CUbOeOAg.js} +3 -1
- package/dist/{renderProfile-Ck32Fzxr.js → renderProfile-CskIgAfn.js} +2 -2
- package/dist/{runtimeTrace-BPQyCmC5.js → runtimeTrace-c0APJz7E.js} +1 -1
- package/dist/{sdkgen-Se88ifTd.js → sdkgen-BiQCgIEr.js} +1 -1
- package/dist/{serveCommand-DkP3OT0W.js → serveCommand-CueKQgzl.js} +869 -805
- package/dist/serveCommand-DsnrVN3U.js +2 -0
- package/dist/serveEntry.js +1 -1
- package/dist/start-BJzZLbt8.js +3 -0
- package/dist/start-ekPan8BT.js +1510 -0
- package/dist/startEntry.js +1 -1
- package/dist/{staticCommand-BwNEDlSU.js → staticCommand-xlSL-IWk.js} +1 -1
- package/dist/{test-f3amja6a.js → test-BWPQcRoB.js} +1 -1
- package/dist/updateCommand-Bqql_rsQ.js +2 -0
- package/dist/{updateCommand-BMk2e4ky.js → updateCommand-C_8I8Rzo.js} +139 -115
- package/dist/webDev-C7jWJ5dX.js +2 -0
- package/dist/{webDev-BgWL9gKV.js → webDev-oczpugbx.js} +1455 -922
- package/dist/{webhooksCommand-CoIO3jbj.js → webhooksCommand-4SVPDjKg.js} +1 -1
- package/package.json +60 -19
- package/templates/AGENTS.md +5 -3
- package/templates/agent-docs/_index.md +5 -3
- package/templates/agent-docs/_manifest.json +20 -4
- package/templates/agent-docs/ai.md +2 -2
- package/templates/agent-docs/authentication.md +1 -1
- package/templates/agent-docs/cli.md +2 -2
- package/templates/agent-docs/configuration.md +17 -0
- package/templates/agent-docs/data.md +470 -26
- package/templates/agent-docs/database/advancedqueries.md +7 -7
- package/templates/agent-docs/database/columntypes.md +2 -2
- package/templates/agent-docs/database/querying.md +1 -1
- package/templates/agent-docs/database/schema.md +1 -1
- package/templates/agent-docs/database/seedsdialects.md +1 -1
- package/templates/agent-docs/database/transactions.md +3 -3
- package/templates/agent-docs/deployment.md +8 -0
- package/templates/agent-docs/internationalization.md +2 -2
- package/templates/agent-docs/introduction.md +7 -1
- package/templates/agent-docs/local-first-mobile.md +102 -4
- package/templates/agent-docs/observability.md +2 -0
- package/templates/agent-docs/plugins/auth.md +1 -1
- package/templates/agent-docs/plugins/comments.md +142 -0
- package/templates/agent-docs/plugins/notifications.md +47 -4
- package/templates/agent-docs/plugins/presence.md +16 -3
- package/templates/agent-docs/plugins/prometheus.md +1 -1
- package/templates/agent-docs/plugins/queue.md +129 -0
- package/templates/agent-docs/plugins.md +11 -7
- package/templates/agent-docs/reference.md +16 -4
- package/templates/agent-docs/routing.md +527 -3
- package/templates/agent-docs/schema-driven-ui.md +214 -3
- package/templates/agent-docs/security.md +3 -3
- package/templates/agent-docs/whats-new.md +160 -66
- package/templates/apps/api-ai/package.json +6 -6
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +9 -9
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-row-history/package.json +8 -8
- package/templates/apps/api-saas/package.json +10 -10
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/app.config.ts +26 -2
- package/templates/apps/changelog/content/releases/{0.1.0.mdx → v0-1-0.mdx} +0 -1
- package/templates/apps/changelog/content/releases/{0.2.0.mdx → v0-2-0.mdx} +0 -1
- package/templates/apps/changelog/package.json +8 -8
- package/templates/apps/changelog/src/collections/releases.collection.ts +48 -0
- package/templates/apps/changelog/src/globals.d.ts +1 -1
- package/templates/apps/changelog/src/locales/de.ts +1 -1
- package/templates/apps/changelog/src/locales/en.ts +1 -1
- package/templates/apps/changelog/src/pages/[locale]/[slug]/page.tsx +7 -4
- package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +13 -41
- package/templates/apps/changelog/src/pages/[slug]/page.test.tsx +26 -51
- package/templates/apps/changelog/src/pages/[slug]/page.tsx +21 -19
- package/templates/apps/changelog/src/pages/page.test.tsx +14 -37
- package/templates/apps/changelog/src/pages/page.tsx +18 -12
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +10 -10
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/content/docs/de/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/de/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/guides/first-page.md +4 -0
- package/templates/apps/frontend-docs/content/docs/en/intro/getting-started.md +4 -0
- package/templates/apps/frontend-docs/package.json +8 -6
- package/templates/apps/frontend-docs/src/collections/docs.collection.ts +21 -0
- package/templates/apps/frontend-docs/src/locales/de.ts +0 -5
- package/templates/apps/frontend-docs/src/locales/en.ts +0 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/docs/[...slug]/page.tsx +17 -9
- package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +10 -5
- package/templates/apps/frontend-docs/src/pages/[locale]/page.tsx +8 -0
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.test.tsx +53 -15
- package/templates/apps/frontend-docs/src/pages/docs/[...slug]/page.tsx +27 -33
- package/templates/apps/frontend-docs/src/pages/page.test.tsx +17 -3
- package/templates/apps/frontend-docs/src/pages/page.tsx +16 -12
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +6 -6
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/content/posts/cms-to-ssg.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/hello-static.md +12 -0
- package/templates/apps/frontend-static-blog/content/posts/islands-not-hydration.md +14 -0
- package/templates/apps/frontend-static-blog/package.json +8 -6
- package/templates/apps/frontend-static-blog/src/collections/posts.collection.ts +27 -0
- package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/[slug]/page.tsx +7 -4
- package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +7 -1
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.test.tsx +19 -9
- package/templates/apps/frontend-static-blog/src/pages/blog/[slug]/page.tsx +19 -21
- package/templates/apps/frontend-static-blog/src/pages/page.test.tsx +18 -5
- package/templates/apps/frontend-static-blog/src/pages/page.tsx +21 -14
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/apiBuild-BYBpL7Pz.js +0 -2
- package/dist/build-CPgcMQug.js +0 -793
- package/dist/codegen-CctkDO-1.js +0 -2
- package/dist/dbCommand-DNb6yeOG.js +0 -2
- package/dist/doctorCommand-CqoWA2p5.js +0 -2
- package/dist/fileConventions-DOqD3lPS.js +0 -34
- package/dist/frameworkTableAssembly-C6ETawPR.js +0 -2
- package/dist/inspect-CuGDYES0.js +0 -2
- package/dist/manifestBuild-CPjhvM62.js +0 -2
- package/dist/serveCommand-DLc-BznW.js +0 -2
- package/dist/start-DfL3fOiN.js +0 -3
- package/dist/start-jw89Xbqy.js +0 -1339
- package/dist/updateCommand-5gFVfK5q.js +0 -2
- package/dist/webDev-CZbTsDcH.js +0 -2
- package/templates/apps/changelog/scripts/generate-rss.mjs +0 -38
- package/templates/apps/changelog/src/lib/releases.ts +0 -21
- package/templates/apps/frontend-static-blog/src/content/posts.ts +0 -64
|
@@ -1103,6 +1103,40 @@ target: {
|
|
|
1103
1103
|
|
|
1104
1104
|
`path`, `by`, `match`, and `shapeItem` are browser-safe descriptor data (a dot-path string + pure functions) — the same discipline as `identify`/`shape`.
|
|
1105
1105
|
|
|
1106
|
+
## Declared relations — a junction saved in the same mutation
|
|
1107
|
+
|
|
1108
|
+
A form with a multi-reference field (assigned stores, tags, members) writes a
|
|
1109
|
+
JUNCTION table beside the row. Declare that on the write target and the
|
|
1110
|
+
framework reconciles the links INSIDE the mutation's transaction — no
|
|
1111
|
+
hand-written junction code in the executor, and a failure rolls the whole
|
|
1112
|
+
write back:
|
|
1113
|
+
|
|
1114
|
+
```ts
|
|
1115
|
+
export const employeesUpdate = defineMutation({
|
|
1116
|
+
name: 'employees.update',
|
|
1117
|
+
input: EmployeesUpdateInput, // carries assignedStores: string[]
|
|
1118
|
+
output: Employee,
|
|
1119
|
+
target: {
|
|
1120
|
+
table: 'employees',
|
|
1121
|
+
op: 'update',
|
|
1122
|
+
relations: { assignedStores: 'employee_assigned_stores' },
|
|
1123
|
+
},
|
|
1124
|
+
})
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
After the executor succeeds, `input.assignedStores` is reconciled against the
|
|
1128
|
+
junction via the diff-based link writer (`store.relationLinks`): missing rows
|
|
1129
|
+
inserted, surplus rows deleted, unchanged rows untouched — so reactive
|
|
1130
|
+
subscriptions on the junction see one change per changed row. The anchor
|
|
1131
|
+
column is derived from the junction's `reference()` targets; a self-junction
|
|
1132
|
+
(both columns referencing one table) is refused by name, never guessed.
|
|
1133
|
+
|
|
1134
|
+
The semantics worth knowing: an ABSENT input field leaves the links
|
|
1135
|
+
untouched — absent is not empty; an empty array is the explicit "clear them
|
|
1136
|
+
all". The row id comes from the executor's `output.id`, falling back to
|
|
1137
|
+
`input.id`. The link writes go through `ctx.store`, so undo capture and
|
|
1138
|
+
cross-table rules see them like any other write.
|
|
1139
|
+
|
|
1106
1140
|
## Typed Errors
|
|
1107
1141
|
|
|
1108
1142
|
```ts
|
|
@@ -2026,8 +2060,14 @@ Queries/subscriptions are for live state. Streams are for one-shot element flows
|
|
|
2026
2060
|
## Reconnect
|
|
2027
2061
|
|
|
2028
2062
|
A dropped WebSocket rebuilds the whole client stack — new socket, new RPC
|
|
2029
|
-
client, new subscription cache — and re-subscribes every active query
|
|
2030
|
-
|
|
2063
|
+
client, new subscription cache — and re-subscribes every active query. Inside
|
|
2064
|
+
the resume window (`reactive.resume.windowMs`, default 60 s) the server replays
|
|
2065
|
+
**only the deltas the client missed** — the re-subscribe presents the last
|
|
2066
|
+
materialised revision and the stream continues on the same revision line, so a
|
|
2067
|
+
short offline gap costs a handful of patches instead of every row. Outside the
|
|
2068
|
+
window, for computed queries, for row-filtered apps, or whenever anything is in
|
|
2069
|
+
doubt, the query answers with a fresh snapshot — the delta-resume wire contract
|
|
2070
|
+
lives in [the wire protocol](/docs/data/wire-protocol#reconnect--delta-resume).
|
|
2031
2071
|
|
|
2032
2072
|
**What is on screen while that happens is your last-known-good data, not a
|
|
2033
2073
|
skeleton.** The replacement cache is seeded from the one it retires, so `data`
|
|
@@ -2054,6 +2094,15 @@ Three things are deliberately NOT carried across:
|
|
|
2054
2094
|
- **Entries nothing re-subscribes to.** A screen that unmounted during the
|
|
2055
2095
|
reconnect does not pin its rows; the seed evicts on the normal inactive TTL.
|
|
2056
2096
|
|
|
2097
|
+
### The local-first mirror partitions by subject — the carve-out
|
|
2098
|
+
|
|
2099
|
+
An app using `@voltro/local-first`'s query mirror keeps rows on the DEVICE
|
|
2100
|
+
across reloads. The blank-on-auth rule extends there structurally: every
|
|
2101
|
+
mirrored key carries the subject AND tenant, so the next subject's binding
|
|
2102
|
+
simply never finds the predecessor's rows, and a logout or membership
|
|
2103
|
+
revocation calls `purge()` on the departing partition. Nothing about the
|
|
2104
|
+
in-memory blanking above changes.
|
|
2105
|
+
|
|
2057
2106
|
### An auth change still blanks — on purpose
|
|
2058
2107
|
|
|
2059
2108
|
When the rebuild happens because the connection's *subject* changed — a cookie
|
|
@@ -2158,32 +2207,40 @@ once. Order BETWEEN subscribers was never guaranteed.
|
|
|
2158
2207
|
There is a number, it is not a constant, and which number you get depends on a
|
|
2159
2208
|
property of your **queries** rather than of your scale. Re-derive it on your own
|
|
2160
2209
|
hardware with `node packages/runtime/scripts/fanout-ceiling.mjs`; the figures
|
|
2161
|
-
below are the spread across three runs on a busy developer machine
|
|
2162
|
-
writes per second, against a budget of 100 ms of
|
|
2163
|
-
of one core).
|
|
2210
|
+
below are the spread across three runs on a busy developer machine (12-core,
|
|
2211
|
+
macOS) at 10 matched writes per second, against a budget of 100 ms of
|
|
2212
|
+
event-loop time per second (10% of one core).
|
|
2164
2213
|
|
|
2165
2214
|
| Subscriber population | Marginal CPU per subscriber | Subscribers per node |
|
|
2166
2215
|
| --- | --- | --- |
|
|
2167
|
-
| **Shared** — N clients on the SAME query (a leaderboard, a shared board) | 0.
|
|
2168
|
-
| **Distinct** — N clients each on their OWN query (`where userId = me`) |
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2216
|
+
| **Shared** — N clients on the SAME query (a leaderboard, a shared board), every write matches all of them | 0.37–0.41 µs | ≈ 25 000–27 000 |
|
|
2217
|
+
| **Distinct** — N clients each on their OWN query (`where userId = me`), a write matches ONE | flat — per-write cost does not grow with resident subscribers | not set by subscriber count |
|
|
2218
|
+
|
|
2219
|
+
The shared case fans out by design — one read and one diff (the memoisation
|
|
2220
|
+
above), then N emits — and it is the shape that sets the ceiling. The distinct
|
|
2221
|
+
case changed shape entirely with **matcher authority**: a subscription whose
|
|
2222
|
+
query is a plain predicate read is woken by the predicate index alone, so a
|
|
2223
|
+
write to *somebody else's* row is a non-event, not a wake. Per write it costs a
|
|
2224
|
+
bucket lookup plus one delivery, regardless of how many thousands of distinct
|
|
2225
|
+
subscribers are resident. Measured: **0 of 200** subscribers whose predicate
|
|
2226
|
+
matched nothing were woken by a write on their table — **a selective `where`
|
|
2227
|
+
buys real headroom** now.
|
|
2228
|
+
|
|
2229
|
+
What still wakes conservatively (any change on the table), and deliberately —
|
|
2230
|
+
this list is exhaustive:
|
|
2231
|
+
|
|
2232
|
+
- queries with an **eager `.with()` spec**, a setOp (`union`/…) or a CTE — the
|
|
2233
|
+
handler reads rows the root predicate does not describe;
|
|
2234
|
+
- **`dependsOn`** raw reads (the dispatcher can only re-run the descriptor,
|
|
2235
|
+
never the handler);
|
|
2236
|
+
- **computed** queries and **`reactivityChannel`** queries (their own
|
|
2237
|
+
recompute paths, unchanged);
|
|
2238
|
+
- **oversized change events** (`tombstone` / `unrecovered` — row images the
|
|
2239
|
+
matcher cannot see wake the whole table for that one event; `rehydrated`
|
|
2240
|
+
events are judged normally).
|
|
2241
|
+
|
|
2242
|
+
One thing that is easy to assume and is not true:
|
|
2243
|
+
|
|
2187
2244
|
- **It is not 512.** That constant bounds `onChange` LISTENERS — one per declared
|
|
2188
2245
|
subscription file, reaction or aggregate, bound once at boot. Every client
|
|
2189
2246
|
subscription in a process shares the dispatcher's single listener, so ten
|
|
@@ -2195,6 +2252,17 @@ mysql/mariadb (binlog), so a second node needs no extra wiring — the cost bein
|
|
|
2195
2252
|
budgeted here is the matcher and re-query CPU each node spends on ITS OWN
|
|
2196
2253
|
clients.
|
|
2197
2254
|
|
|
2255
|
+
### Deltas are per-query — two queries can briefly diverge
|
|
2256
|
+
|
|
2257
|
+
Every subscription has its own revision line and its own delivery moment. After
|
|
2258
|
+
one write that affects two queries you hold open, the deltas arrive as two
|
|
2259
|
+
independent pushes — usually microseconds apart, but there is no cross-query
|
|
2260
|
+
transaction on the wire, and a render between the two pushes can see query A
|
|
2261
|
+
after the write and query B before it. Within ONE query you never see a partial
|
|
2262
|
+
write (a delta is computed from a committed row set); across queries, design for
|
|
2263
|
+
eventual agreement rather than instantaneous consistency — derive values that
|
|
2264
|
+
must agree atomically inside one query instead of joining two on the client.
|
|
2265
|
+
|
|
2198
2266
|
## Raw WebSocket gateways — `defineWebSocket`
|
|
2199
2267
|
|
|
2200
2268
|
Everything above rides the framework's subscription protocol, and that stays the answer for app realtime — live queries, optimistic patches, reconnect. A **gateway** exists for the other case: a FOREIGN protocol that needs a socket the framework does not speak — a Yjs provider, a legacy device fleet, an MQTT-over-WS bridge. It mounts its own upgrade path beside the rpc socket, in a `*.ws.ts` file discovered on **both** boot paths:
|
|
@@ -4854,7 +4922,7 @@ A streaming query (what `useSubscription` opens) emits a sequence of **subscript
|
|
|
4854
4922
|
{ _tag: 'error', error: { _tag?: string, message: string, ...fields }, revision?: number }
|
|
4855
4923
|
```
|
|
4856
4924
|
|
|
4857
|
-
- **`revision`** — monotonically increasing; lets the client order events
|
|
4925
|
+
- **`revision`** — monotonically increasing; lets the client order events. **Revisions may JUMP forward** — under socket backpressure the server coalesces updates a slow consumer hasn't read yet into one event whose patch is computed against the row set of the last event it actually handed over, so patch continuity holds across the jump. A jump is therefore normal, never a gap; only a *regressing* or repeating revision would be a protocol violation.
|
|
4858
4926
|
- **`emittedAt`** — epoch milliseconds, present on `delta` only.
|
|
4859
4927
|
- **`data`** (snapshot) — the full payload, typed by the query's `output` schema (a row, an array of rows, a computed value — whatever the handler returns).
|
|
4860
4928
|
- **`patch`** (delta) — an id-keyed RFC-6902-style patch against the row set the client last held.
|
|
@@ -4873,6 +4941,70 @@ propagate to the shared connection and stall every *other* subscription on it
|
|
|
4873
4941
|
client surfaces it as `useSubscription(...).error` for that one query key;
|
|
4874
4942
|
siblings keep delivering their snapshots and deltas.
|
|
4875
4943
|
|
|
4944
|
+
### Slow consumers — coalescing and `SubscriptionOverrun`
|
|
4945
|
+
|
|
4946
|
+
A consumer that stops reading (a backgrounded tab, a saturated link) does not
|
|
4947
|
+
grow the server without bound. While its socket is blocked, updates
|
|
4948
|
+
**coalesce**: the server keeps only the newest state per subscription and, when
|
|
4949
|
+
the socket accepts again, sends ONE event — a patch against the last state the
|
|
4950
|
+
consumer was actually handed (`revision` jumps accordingly, see above). Memory
|
|
4951
|
+
per blocked subscription is bounded by construction: one pending state,
|
|
4952
|
+
regardless of how far behind the consumer is.
|
|
4953
|
+
|
|
4954
|
+
A consumer that stays more than `reactive.socket.maxBufferedBytes` (default
|
|
4955
|
+
1 MiB, env `VOLTRO_REACTIVE_MAX_BUFFERED_BYTES`) behind for
|
|
4956
|
+
`reactive.socket.overrunAfterMs` (default 10 s) is closed **loudly**: it
|
|
4957
|
+
receives an `error` event with `error._tag: 'SubscriptionOverrun'` (carrying
|
|
4958
|
+
`bufferedBytes` + `maxBufferedBytes`) and the stream ends — never a silent
|
|
4959
|
+
drop. The client re-subscribes and starts from a fresh snapshot.
|
|
4960
|
+
|
|
4961
|
+
Oversized events are telemetry, not a cap: an event over
|
|
4962
|
+
`reactive.socket.oversizedEventBytes` (default 256 KiB) is delivered normally
|
|
4963
|
+
and counted (`voltro_subscription_oversized_total`) with a WARN naming the
|
|
4964
|
+
query — alongside `voltro_subscription_buffered_bytes`,
|
|
4965
|
+
`voltro_subscription_coalesced_total` and `voltro_subscription_overrun_total`
|
|
4966
|
+
in the Prometheus exporter and the inspect Metrics panel.
|
|
4967
|
+
|
|
4968
|
+
### Reconnect — delta-resume
|
|
4969
|
+
|
|
4970
|
+
A client that reconnects inside the **resume window** does not have to pay for
|
|
4971
|
+
a full snapshot: it sends the last `revision` it materialised in the per-call
|
|
4972
|
+
`voltro-resume-from` request header (the same header surface the idempotency
|
|
4973
|
+
key rides), and the server — which kept the subscription alive server-side for
|
|
4974
|
+
the window after the disconnect — **replays only the deltas that were missed**
|
|
4975
|
+
and re-attaches the stream on the SAME revision line.
|
|
4976
|
+
|
|
4977
|
+
The signal is the first event's tag, not a schema field:
|
|
4978
|
+
|
|
4979
|
+
- **first event `delta`** — the resume was honoured; apply the patch onto the
|
|
4980
|
+
rows you already hold and continue.
|
|
4981
|
+
- **first event `snapshot`** — the resume was declined; reset to the snapshot.
|
|
4982
|
+
This is the answer whenever anything is in doubt, because a wrong snapshot
|
|
4983
|
+
costs bytes while a wrong replay would leak rows.
|
|
4984
|
+
|
|
4985
|
+
`@voltro/client` does both automatically — the reconnect-seeded cache keeps its
|
|
4986
|
+
rows and revision, presents the header, and treats a snapshot-first stream as
|
|
4987
|
+
the reset it already knows how to do. Replayed deltas may **coalesce** exactly
|
|
4988
|
+
as slow-consumer updates do (revisions jump; patch continuity holds).
|
|
4989
|
+
|
|
4990
|
+
A resume is declined — always with a fresh snapshot — when:
|
|
4991
|
+
|
|
4992
|
+
- the window expired (`reactive.resume.windowMs`, default 60 s, env
|
|
4993
|
+
`VOLTRO_REACTIVE_RESUME_WINDOW_MS`), or more deltas were missed than the ring
|
|
4994
|
+
retains (`reactive.resume.maxDeltas`, default 256, env
|
|
4995
|
+
`VOLTRO_REACTIVE_RESUME_MAX_DELTAS`);
|
|
4996
|
+
- the query's `guards:` were revoked while the client was away — the
|
|
4997
|
+
per-delivery re-check keeps running on the detached subscription, and a
|
|
4998
|
+
revocation drops the retained history outright;
|
|
4999
|
+
- the resuming caller is a different subject or tenant (a login, logout or
|
|
5000
|
+
tenant switch between disconnect and resume) — the retained history is keyed
|
|
5001
|
+
by subject AND tenant, so a changed identity simply never finds it;
|
|
5002
|
+
- the app registers a row filter (`setRowFilter`), or the query is a
|
|
5003
|
+
**computed** query — both are excluded from resume by design: a row-filtered
|
|
5004
|
+
subscription's visible row set exists only per delivery, and a computed query
|
|
5005
|
+
re-runs a handler with no delta chain to replay. They reconnect with a fresh
|
|
5006
|
+
snapshot, exactly as before.
|
|
5007
|
+
|
|
4876
5008
|
**Author a live-subscribed getter to return, not throw.** A subscription is a
|
|
4877
5009
|
long-lived stream, so a getter that throws on every re-evaluation is a broken
|
|
4878
5010
|
stream. For an expected-absent row, make the query `output: Schema.NullOr(...)`
|
|
@@ -5205,6 +5337,203 @@ try {
|
|
|
5205
5337
|
|
|
5206
5338
|
|
|
5207
5339
|
|
|
5340
|
+
---
|
|
5341
|
+
|
|
5342
|
+
<!-- source: en/data/content-collections.md -->
|
|
5343
|
+
## Content collections
|
|
5344
|
+
|
|
5345
|
+
_File-based, schema-typed markdown content — defineCollection over content/<name>/**/*.md, an isomorphic getCollection/getEntry, locale trees with fallback, headings for TOCs, data collections, references, and RSS feeds — without installing a markdown dependency._
|
|
5346
|
+
|
|
5347
|
+
A content collection turns a folder of markdown files into typed, rendered
|
|
5348
|
+
content: you declare the frontmatter schema in code, and `getCollection()` /
|
|
5349
|
+
`getEntry()` hand you decoded data plus server-rendered HTML with
|
|
5350
|
+
syntax-highlighted code fences. The markdown engine lives in the framework —
|
|
5351
|
+
**do not install your own `marked` / `remark` / `shiki`**; a second pipeline
|
|
5352
|
+
drifts from the one your artifacts, feeds and templates already use.
|
|
5353
|
+
|
|
5354
|
+
## A blog in 20 lines
|
|
5355
|
+
|
|
5356
|
+
One collection file, one markdown file, one page:
|
|
5357
|
+
|
|
5358
|
+
```ts
|
|
5359
|
+
// src/collections/posts.collection.ts
|
|
5360
|
+
import { Schema } from 'effect'
|
|
5361
|
+
import { defineCollection } from '@voltro/content'
|
|
5362
|
+
|
|
5363
|
+
export const posts = defineCollection({
|
|
5364
|
+
name: 'posts',
|
|
5365
|
+
directory: 'content/posts',
|
|
5366
|
+
schema: Schema.Struct({ title: Schema.String, date: Schema.String }),
|
|
5367
|
+
})
|
|
5368
|
+
export type Post = Schema.Schema.Type<typeof posts.schema>
|
|
5369
|
+
```
|
|
5370
|
+
|
|
5371
|
+
```ts
|
|
5372
|
+
// src/pages/blog/[slug]/page.tsx
|
|
5373
|
+
import { getCollection, getEntry, type ContentEntry } from '@voltro/content'
|
|
5374
|
+
import { useLoaderData } from '@voltro/web'
|
|
5375
|
+
import { posts, type Post } from '../../../collections/posts.collection'
|
|
5376
|
+
|
|
5377
|
+
export const renderMode = 'static' as const
|
|
5378
|
+
export const getStaticPaths = async () =>
|
|
5379
|
+
(await getCollection(posts.name)).map((e) => ({ params: { slug: e.slug } }))
|
|
5380
|
+
export const loader = async ({ params }: { params: { slug: string } }) =>
|
|
5381
|
+
await getEntry<Post>(posts.name, params.slug)
|
|
5382
|
+
|
|
5383
|
+
export default function Post() {
|
|
5384
|
+
const post = useLoaderData<ContentEntry<Post> | null>()
|
|
5385
|
+
if (!post) return <main>Not found</main>
|
|
5386
|
+
return <article dangerouslySetInnerHTML={{ __html: post.html ?? '' }} />
|
|
5387
|
+
}
|
|
5388
|
+
```
|
|
5389
|
+
|
|
5390
|
+
Drop `content/posts/hello.md` with `title:` + `date:` frontmatter and the
|
|
5391
|
+
build pre-renders `/blog/hello` — highlighted code fences included.
|
|
5392
|
+
|
|
5393
|
+
## How it stays out of your bundle
|
|
5394
|
+
|
|
5395
|
+
The loader is **isomorphic**. At build/SSR time it reads the filesystem and
|
|
5396
|
+
renders markdown (shiki runs on the server only). The build also emits JSON
|
|
5397
|
+
artifacts under `dist/assets/content/<name>[.<locale>]/…` — an index (slugs +
|
|
5398
|
+
frontmatter, no bodies) and one file per entry (rendered HTML + headings). On
|
|
5399
|
+
an SPA navigation, the CLIENT branch of `getCollection`/`getEntry` fetches
|
|
5400
|
+
those artifacts. The result: no markdown engine, no highlighter, and no
|
|
5401
|
+
content bodies in your JavaScript bundle. `voltro dev` serves the same
|
|
5402
|
+
artifact shapes on demand and invalidates them when a `content/**` file
|
|
5403
|
+
changes.
|
|
5404
|
+
|
|
5405
|
+
## Frontmatter is a schema, and violations fail the build
|
|
5406
|
+
|
|
5407
|
+
The `schema` is an `effect/Schema` struct decoded per file. A missing or
|
|
5408
|
+
mistyped field is a **build error naming the file** — not a page that renders
|
|
5409
|
+
`undefined`. Numbers in frontmatter arrive as strings; use
|
|
5410
|
+
`Schema.Union(Schema.NumberFromString, Schema.Number)` for numeric fields.
|
|
5411
|
+
`getCollection<A>` returns entries whose `data` is the schema's inferred
|
|
5412
|
+
type — no casts.
|
|
5413
|
+
|
|
5414
|
+
## Slugs come from the path
|
|
5415
|
+
|
|
5416
|
+
`content/posts/hello.md` → `hello`; nested folders stay in the slug
|
|
5417
|
+
(`database/joins.md` → `database/joins`). Two files resolving to one slug
|
|
5418
|
+
(a rename that left both) is a build error.
|
|
5419
|
+
|
|
5420
|
+
## Locale trees + fallback
|
|
5421
|
+
|
|
5422
|
+
A collection with `i18n` treats the first path segment as the locale:
|
|
5423
|
+
|
|
5424
|
+
```ts
|
|
5425
|
+
export const docs = defineCollection({
|
|
5426
|
+
name: 'docs',
|
|
5427
|
+
directory: 'content/docs',
|
|
5428
|
+
schema: Schema.Struct({ title: Schema.String }),
|
|
5429
|
+
i18n: { locales: ['en', 'de'], defaultLocale: 'en', missing: 'fallback' },
|
|
5430
|
+
})
|
|
5431
|
+
```
|
|
5432
|
+
|
|
5433
|
+
`getCollection('docs', { locale: 'de' })` reads the `de/` tree. A slug missing
|
|
5434
|
+
in the requested locale is served from the default tree with
|
|
5435
|
+
`fallback: true` on the entry (render an "untranslated" banner off it) — or
|
|
5436
|
+
omitted entirely with `missing: 'missing'`. Incomplete translations are the
|
|
5437
|
+
normal case; decide the policy per collection instead of improvising per page.
|
|
5438
|
+
|
|
5439
|
+
## Headings as data
|
|
5440
|
+
|
|
5441
|
+
Every rendered entry carries `headings: [{ depth, slug, text }]` — the TOC
|
|
5442
|
+
input. The slugs are the SAME ids stamped on the rendered `<h2 id="…">`
|
|
5443
|
+
elements, so sidebar anchors never drift from the body. For a TOC without a
|
|
5444
|
+
render pass, `extractHeadings(markdown)` (from `@voltro/content/markdown`)
|
|
5445
|
+
computes the same data synchronously.
|
|
5446
|
+
|
|
5447
|
+
## Data collections
|
|
5448
|
+
|
|
5449
|
+
`kind: 'data'` reads `.json` files instead of markdown — the `authors.json`
|
|
5450
|
+
case. Each file decodes whole against the schema; there is no render path:
|
|
5451
|
+
|
|
5452
|
+
```ts
|
|
5453
|
+
export const authors = defineCollection({
|
|
5454
|
+
name: 'authors',
|
|
5455
|
+
directory: 'content/authors',
|
|
5456
|
+
kind: 'data',
|
|
5457
|
+
schema: Schema.Struct({ name: Schema.String, url: Schema.String }),
|
|
5458
|
+
})
|
|
5459
|
+
```
|
|
5460
|
+
|
|
5461
|
+
## References between collections
|
|
5462
|
+
|
|
5463
|
+
`reference('<collection>')` declares a frontmatter field that names an entry
|
|
5464
|
+
of another collection by slug:
|
|
5465
|
+
|
|
5466
|
+
```ts
|
|
5467
|
+
schema: Schema.Struct({
|
|
5468
|
+
title: Schema.String,
|
|
5469
|
+
author: reference('authors'),
|
|
5470
|
+
})
|
|
5471
|
+
```
|
|
5472
|
+
|
|
5473
|
+
The build validates every reference — a dangling one (`author: nobody`) fails
|
|
5474
|
+
the build naming the collection, entry, field and target. Resolve it with
|
|
5475
|
+
`getEntry('authors', entry.data.author)`.
|
|
5476
|
+
|
|
5477
|
+
## RSS feeds from a collection
|
|
5478
|
+
|
|
5479
|
+
Declare feeds in `app.config.ts`; the build writes them next to
|
|
5480
|
+
`sitemap.xml`, and `voltro dev` serves the same XML live:
|
|
5481
|
+
|
|
5482
|
+
```ts
|
|
5483
|
+
export default {
|
|
5484
|
+
// …
|
|
5485
|
+
seo: { siteUrl: 'https://example.com' },
|
|
5486
|
+
feeds: [{
|
|
5487
|
+
path: '/rss.xml',
|
|
5488
|
+
collection: 'posts',
|
|
5489
|
+
title: 'My blog',
|
|
5490
|
+
item: (e) => e.data.draft === 'true' ? null : ({
|
|
5491
|
+
title: e.data.title, link: `/blog/${e.slug}`, date: e.data.date,
|
|
5492
|
+
}),
|
|
5493
|
+
}],
|
|
5494
|
+
}
|
|
5495
|
+
```
|
|
5496
|
+
|
|
5497
|
+
Returning `null` from `item` excludes an entry — that is the **draft filter**:
|
|
5498
|
+
keep a `draft: true` field in your schema and filter it in `item` and in your
|
|
5499
|
+
page loaders (the changelog template's `visibleReleases` helper is the worked
|
|
5500
|
+
example, including future-dated staging).
|
|
5501
|
+
|
|
5502
|
+
## No MDX — islands carry the interactivity
|
|
5503
|
+
|
|
5504
|
+
Collection bodies are **markdown, not MDX**: JSX, `import`s and
|
|
5505
|
+
`{expressions}` in a body are not executed. When a content page needs a live
|
|
5506
|
+
widget, the surrounding PAGE provides it via the islands mechanism — the
|
|
5507
|
+
content stays inert HTML and the widget hydrates alone:
|
|
5508
|
+
|
|
5509
|
+
```tsx
|
|
5510
|
+
// src/pages/blog/[slug]/page.tsx
|
|
5511
|
+
export const interactive = 'islands' as const
|
|
5512
|
+
|
|
5513
|
+
export default function Post() {
|
|
5514
|
+
const post = useLoaderData<ContentEntry<Post>>()
|
|
5515
|
+
return (
|
|
5516
|
+
<main>
|
|
5517
|
+
<ReadingProgress /> {/* an island() component — the ONLY hydrated JS */}
|
|
5518
|
+
<article dangerouslySetInnerHTML={{ __html: post.html ?? '' }} />
|
|
5519
|
+
</main>
|
|
5520
|
+
)
|
|
5521
|
+
}
|
|
5522
|
+
```
|
|
5523
|
+
|
|
5524
|
+
## Limits + neighbors
|
|
5525
|
+
|
|
5526
|
+
- **Images referenced from markdown bodies** are copied as-is (no transform):
|
|
5527
|
+
the [image pipeline](/docs/routing/assets) covers `?image` imports from
|
|
5528
|
+
code. Put content images under `public/` and reference them absolutely.
|
|
5529
|
+
- **Files are DEVELOPER content** — versioned with the code, deployed by the
|
|
5530
|
+
build. Editorial content with drafts, roles and a save/publish pipeline is
|
|
5531
|
+
[`@voltro/cms`](/docs/data/cms). Astro's remote "Content Layer loaders"
|
|
5532
|
+
map to `@voltro/cms` here: remote/editorial sources go through the CMS,
|
|
5533
|
+
not through file collections.
|
|
5534
|
+
|
|
5535
|
+
|
|
5536
|
+
|
|
5208
5537
|
---
|
|
5209
5538
|
|
|
5210
5539
|
<!-- source: en/data/cms.md -->
|
|
@@ -5745,3 +6074,118 @@ app's public origin comes from `VOLTRO_PUBLIC_URL`.
|
|
|
5745
6074
|
a secret, not a connection.
|
|
5746
6075
|
- **`redirectTo` is a same-origin path only.** An absolute URL is rejected —
|
|
5747
6076
|
otherwise every app declaring a connection would ship an open redirector.
|
|
6077
|
+
|
|
6078
|
+
|
|
6079
|
+
|
|
6080
|
+
---
|
|
6081
|
+
|
|
6082
|
+
<!-- source: en/data/grpc.md -->
|
|
6083
|
+
## gRPC surface
|
|
6084
|
+
|
|
6085
|
+
_Serve opt-in procedures to generated gRPC clients — proto emitted from your effect/Schema with checked-in field-number stability, unary for mutations/actions, server-streaming for live queries, guards + interceptors + typed errors identical to the socket._
|
|
6086
|
+
|
|
6087
|
+
The gRPC surface serves a NAMED list of your procedures to external gRPC
|
|
6088
|
+
clients — the polyglot-microservice door. The `.proto` is generated from the
|
|
6089
|
+
same `effect/Schema` your procedures already declare, so there is no second
|
|
6090
|
+
contract to maintain; the wire semantics are the framework's own: guards,
|
|
6091
|
+
plugin interceptors and typed errors behave **identically** to the rpc
|
|
6092
|
+
socket, because a gRPC call runs the *same bound runner* every other surface
|
|
6093
|
+
uses (the e2e proves interceptor order side by side).
|
|
6094
|
+
|
|
6095
|
+
```ts
|
|
6096
|
+
// app.config.ts
|
|
6097
|
+
export default {
|
|
6098
|
+
type: 'api' as const,
|
|
6099
|
+
name: 'api',
|
|
6100
|
+
grpc: {
|
|
6101
|
+
port: 50051,
|
|
6102
|
+
procedures: ['orders.get', 'orders.list', 'orders.create'],
|
|
6103
|
+
// tls: { certPath, keyPath, caPath? } — plaintext without it (dev / mesh).
|
|
6104
|
+
},
|
|
6105
|
+
}
|
|
6106
|
+
```
|
|
6107
|
+
|
|
6108
|
+
NOTHING is exposed by default — every tag is named. Booting writes
|
|
6109
|
+
`.framework/grpc.proto` (hand it to any proto codegen) and mounts
|
|
6110
|
+
`grpc.health.v1` health checking plus server reflection (`grpcurl … list`
|
|
6111
|
+
works out of the box). The gRPC packages ship as script-free optional
|
|
6112
|
+
dependencies of `@voltro/cli`; a configured `grpc:` block with them missing
|
|
6113
|
+
refuses the boot by name.
|
|
6114
|
+
|
|
6115
|
+
## Field numbers are managed — `grpc.manifest.json`
|
|
6116
|
+
|
|
6117
|
+
Field numbers are the proto wire identity, so they may never depend on
|
|
6118
|
+
property order. They come from a checked-in manifest in your app root:
|
|
6119
|
+
|
|
6120
|
+
- a **new** field gets the next never-used number — an **inserted** field
|
|
6121
|
+
never renumbers its neighbours;
|
|
6122
|
+
- a **deleted** field's number becomes `reserved` (emitted into the proto,
|
|
6123
|
+
so `protoc` refuses a colliding hand-edit too);
|
|
6124
|
+
- **reusing** a reserved number is a codegen error, never a warning — an old
|
|
6125
|
+
client would silently read the wrong field.
|
|
6126
|
+
|
|
6127
|
+
Commit the manifest with the schema change that moved it: the diff review IS
|
|
6128
|
+
the wire-contract review.
|
|
6129
|
+
|
|
6130
|
+
## The mapping table
|
|
6131
|
+
|
|
6132
|
+
| Schema | proto3 |
|
|
6133
|
+
|---|---|
|
|
6134
|
+
| `Schema.String` / `Number` / `Boolean` | `string` / `double` / `bool` |
|
|
6135
|
+
| integer schemas | `int64` |
|
|
6136
|
+
| `Schema.Array(T)` | `repeated T` |
|
|
6137
|
+
| nested `Schema.Struct` | nested message |
|
|
6138
|
+
| `Schema.Record({ key: String, value: T })` | `map<string, T>` |
|
|
6139
|
+
| `Schema.optional(T)` **and** `Schema.NullOr(T)` | `optional T` — absent and `null` are ONE wire state (proto3 presence) |
|
|
6140
|
+
| string-literal unions | `string` (validated server-side on decode) |
|
|
6141
|
+
| unions of shapes, tuples, recursion, free-form objects | a LOUD per-procedure codegen error naming the schema path |
|
|
6142
|
+
|
|
6143
|
+
Requests are decoded against the descriptor's input schema before the
|
|
6144
|
+
executor runs — proto3 suppresses default values on the wire, and without
|
|
6145
|
+
that decode an empty string would arrive as an absent field and fail
|
|
6146
|
+
somewhere much later.
|
|
6147
|
+
|
|
6148
|
+
## Status codes — complete against the wire error union
|
|
6149
|
+
|
|
6150
|
+
| outcome | gRPC status | trailers |
|
|
6151
|
+
|---|---|---|
|
|
6152
|
+
| no credential on a guarded call | `UNAUTHENTICATED` | |
|
|
6153
|
+
| presented-and-rejected credential | `UNAUTHENTICATED` | |
|
|
6154
|
+
| authenticated, missing scope (`ScopeError`) | `PERMISSION_DENIED` | `voltro-error: scope` |
|
|
6155
|
+
| input fails the schema | `INVALID_ARGUMENT` | `voltro-error: input` |
|
|
6156
|
+
| `BusinessRuleViolation` | `FAILED_PRECONDITION` | `voltro-error: rule` |
|
|
6157
|
+
| `requiresApproval` pending — a FLOW OUTCOME, not a failure | `FAILED_PRECONDITION` | `voltro-pending: approval` + `voltro-approval-id` |
|
|
6158
|
+
| your declared typed error | `FAILED_PRECONDITION` | `voltro-error: <tag>` |
|
|
6159
|
+
| deadline exceeded | `DEADLINE_EXCEEDED` | |
|
|
6160
|
+
| anything else | `INTERNAL` | |
|
|
6161
|
+
|
|
6162
|
+
**Deadlines interrupt the work.** A client deadline (`grpc-timeout`) aborts
|
|
6163
|
+
the executor's fiber through the request signal — the server stops doing the
|
|
6164
|
+
work, it does not merely suppress the response (the e2e pins this with a
|
|
6165
|
+
sleeping action whose post-sleep write never lands).
|
|
6166
|
+
|
|
6167
|
+
## Streaming queries
|
|
6168
|
+
|
|
6169
|
+
A `query` becomes a **server-streaming** rpc: each frame is the CURRENT full
|
|
6170
|
+
snapshot, re-pushed live when the query's `source:` changes — subscribe,
|
|
6171
|
+
mutate from anywhere, and the open stream receives the new frame with no
|
|
6172
|
+
re-request. The per-delivery guard re-check applies (a revoked scope ends
|
|
6173
|
+
the stream with the mapped status), and slow consumers are handled through
|
|
6174
|
+
grpc-js write backpressure — frames coalesce to the latest snapshot rather
|
|
6175
|
+
than buffering unboundedly.
|
|
6176
|
+
|
|
6177
|
+
## Declared limits (v1)
|
|
6178
|
+
|
|
6179
|
+
- **No client- or bidi-streaming**, and `*.stream.ts` procedures are NOT
|
|
6180
|
+
exposable — the fourth kind is a one-shot element stream with its own
|
|
6181
|
+
semantics; put it behind a query or keep it on the socket.
|
|
6182
|
+
- **No gRPC-Web** — a browser talks the framework's own subscription
|
|
6183
|
+
protocol (that is the better browser transport in every dimension we care
|
|
6184
|
+
about); gRPC is for backends.
|
|
6185
|
+
- **No Connect protocol** — connectrpc is NOT gRPC-Web; a connect consumer's
|
|
6186
|
+
alternative today is the [REST/OpenAPI projection](/docs/data/rest-routes).
|
|
6187
|
+
- App realtime stays on the framework's subscription protocol, and gateways
|
|
6188
|
+
exist for the other case: a FOREIGN protocol that needs a socket the
|
|
6189
|
+
framework does not speak — the same boundary
|
|
6190
|
+
[data/subscriptions](/docs/data/subscriptions) draws for raw WebSocket
|
|
6191
|
+
gateways, one sentence, two doors.
|
|
@@ -379,7 +379,7 @@ Emits `CONSTRAINT <name> UNIQUE (col1, col2, ...)` inline in CREATE
|
|
|
379
379
|
TABLE on every dialect. Standard SQL.
|
|
380
380
|
|
|
381
381
|
This is what backs `ctx.store.upsert(..., { conflictColumns: ['a', 'b'] })`
|
|
382
|
-
— see [Bulk operations](/docs/database/bulk-operations#upsert).
|
|
382
|
+
— see [Bulk operations](/docs/database/bulk-operations#upsert-insert-or-update-on-conflict).
|
|
383
383
|
|
|
384
384
|
## GiST indexes (PostGIS spatial)
|
|
385
385
|
|
|
@@ -689,7 +689,7 @@ mssql, sqlite 3.8+).
|
|
|
689
689
|
depend on?"
|
|
690
690
|
|
|
691
691
|
When you don't have a recursive structure, plain
|
|
692
|
-
[`withCte()`](/docs/database/query-builder#ctes) is enough.
|
|
692
|
+
[`withCte()`](/docs/database/query-builder#ctes-common-table-expressions) is enough.
|
|
693
693
|
|
|
694
694
|
## Shape
|
|
695
695
|
|
|
@@ -820,7 +820,7 @@ eager-load with `.with({...})` to get the per-field pre-filter.
|
|
|
820
820
|
|
|
821
821
|
## See also
|
|
822
822
|
|
|
823
|
-
- [Plain CTEs](/docs/database/query-builder#ctes) — `withCte()` for
|
|
823
|
+
- [Plain CTEs](/docs/database/query-builder#ctes-common-table-expressions) — `withCte()` for
|
|
824
824
|
non-recursive named sub-queries
|
|
825
825
|
- [Self-joins](/docs/database/self-joins) — for single-level
|
|
826
826
|
parent/child queries
|
|
@@ -830,7 +830,7 @@ eager-load with `.with({...})` to get the per-field pre-filter.
|
|
|
830
830
|
`unionAll`, the mechanism a recursive CTE is built on
|
|
831
831
|
- [Joins](/docs/database/joins) — relation-based traversal when the
|
|
832
832
|
graph depth is fixed (e.g. parent + immediate children)
|
|
833
|
-
- [Aggregations](/docs/database/query-builder#
|
|
833
|
+
- [Aggregations](/docs/database/query-builder#aggregates) —
|
|
834
834
|
COUNT/SUM/AVG over a recursive CTE's result set
|
|
835
835
|
|
|
836
836
|
|
|
@@ -940,7 +940,7 @@ keep the branches' result sets bounded.
|
|
|
940
940
|
the column-level "in A but not in B" case
|
|
941
941
|
- [Aggregations](/docs/database/aggregations) — `count()` etc. on
|
|
942
942
|
a set-op result is a common pattern
|
|
943
|
-
- [CTEs](/docs/database/query-builder#ctes) — name a complex set-op
|
|
943
|
+
- [CTEs](/docs/database/query-builder#ctes-common-table-expressions) — name a complex set-op
|
|
944
944
|
result so you can reference it in a larger query
|
|
945
945
|
|
|
946
946
|
|
|
@@ -1044,7 +1044,7 @@ Non-correlated only. The inner query can NOT reference outer-row
|
|
|
1044
1044
|
columns like `WHERE inner.userId = users.id`. For correlated
|
|
1045
1045
|
sub-queries (a common shape: "user who has at least one post created
|
|
1046
1046
|
in the last hour") use a [Self-join](/docs/database/self-joins) or
|
|
1047
|
-
an [Eager-load](/docs/database/joins#eager-loading) — both can
|
|
1047
|
+
an [Eager-load](/docs/database/joins#eager-loading-via-with-spec) — both can
|
|
1048
1048
|
express the same query without the correlation reference.
|
|
1049
1049
|
|
|
1050
1050
|
## Reactivity
|
|
@@ -1069,7 +1069,7 @@ every dialect we ship. No per-dialect dispatch.
|
|
|
1069
1069
|
with `count()` etc. for "count of X where Y belongs to Z"
|
|
1070
1070
|
- [Self-joins](/docs/database/self-joins) — when the relationship
|
|
1071
1071
|
can be expressed as a join instead
|
|
1072
|
-
- [CTEs](/docs/database/query-builder#ctes) — for naming a
|
|
1072
|
+
- [CTEs](/docs/database/query-builder#ctes-common-table-expressions) — for naming a
|
|
1073
1073
|
sub-query you reuse multiple times in the same outer query
|
|
1074
1074
|
|
|
1075
1075
|
|
|
@@ -350,7 +350,7 @@ on it explicitly via `.expressionIndex(name, [...], { ... })`.
|
|
|
350
350
|
|
|
351
351
|
- [Columns](/docs/database/columns) — `.computed(row => ...)` and
|
|
352
352
|
`.default(() => ...)` for the app-side variants
|
|
353
|
-
- [Indexes](/docs/database/indexes#expression) — `.expressionIndex()`
|
|
353
|
+
- [Indexes](/docs/database/indexes#expression-indexes) — `.expressionIndex()`
|
|
354
354
|
for indexing a generated column
|
|
355
355
|
- [Full-text search](/docs/database/full-text-search) — the FTS
|
|
356
356
|
pattern uses STORED tsvector generated columns
|
|
@@ -989,7 +989,7 @@ behavioural gaps are too large to paper over.
|
|
|
989
989
|
## See also
|
|
990
990
|
|
|
991
991
|
- [Columns](/docs/database/columns) — the regular schema-DSL types
|
|
992
|
-
- [Expression indexes](/docs/database/indexes#expression) — the
|
|
992
|
+
- [Expression indexes](/docs/database/indexes#expression-indexes) — the
|
|
993
993
|
framework's index API (`kind: 'gist' | 'gin'` on postgres)
|
|
994
994
|
- [PostGIS docs](https://postgis.net/docs/) — the official manual,
|
|
995
995
|
authoritative for every spatial function the framework re-exports
|
|
@@ -309,7 +309,7 @@ The query builder has dedicated pages for the deeper topics:
|
|
|
309
309
|
- **[Self-joins](/docs/database/self-joins)** — `.as(alias)` +
|
|
310
310
|
`.innerJoin(table, alias, on)` + `.selectJoined({...})` for parent/
|
|
311
311
|
child trees and CTE references.
|
|
312
|
-
- **[CTEs](/docs/database/query-builder#ctes)** — `.withCte(name, sub)`
|
|
312
|
+
- **[CTEs](/docs/database/query-builder#ctes-common-table-expressions)** — `.withCte(name, sub)`
|
|
313
313
|
for named sub-queries reusable inside the outer SELECT.
|
|
314
314
|
- **[Recursive CTEs](/docs/database/recursive-cte)** — `.recursiveCte`
|
|
315
315
|
for tree walks (org hierarchy, comment threads, file folders).
|
|
@@ -445,7 +445,7 @@ framework runs on the post-commit change channel.
|
|
|
445
445
|
**Reach for `reference()` first.** A real foreign key across a plugin boundary
|
|
446
446
|
works and survives the plugin renaming its table, because `reference()` takes
|
|
447
447
|
the table as a VALUE — see
|
|
448
|
-
[plugins/overview](/docs/plugins/overview
|
|
448
|
+
[plugins/overview](/docs/plugins/overview).
|
|
449
449
|
`pluginRef` is for the case where you have deliberately chosen NOT to have a
|
|
450
450
|
key: it enforces nothing at the database level.
|
|
451
451
|
|