@voltro/cli 0.53.0 → 0.55.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 +335 -0
- package/dist/{agentsMd-0l980yhL.js → agentsMd-BaLC10Na.js} +110 -82
- package/dist/agentsMd-DCY1RSs8.js +2 -0
- package/dist/{apiBuild-CaPfoWku.js → apiBuild-CMvLJM_K.js} +2 -2
- package/dist/apiBuild-Cl0IDx8c.js +2 -0
- package/dist/bin.js +1 -1
- package/dist/{build-D-OnvNMf.js → build-S0QOzqPT.js} +115 -115
- package/dist/{checkCommand-D2ZduVlh.js → checkCommand-DNkY5kwF.js} +1 -1
- package/dist/{checkCommand-C5elt0tW.js → checkCommand-fbj9GDjN.js} +6 -6
- package/dist/{cloudCmd-QUXh-b5w.js → cloudCmd-DzKcSYuy.js} +1 -1
- package/dist/codegen-CN6vMM4J.js +2 -0
- package/dist/{codegen-FEk8AZHb.js → codegen-SIepQtUl.js} +76 -65
- package/dist/codegenCommand-3TDJezom.js +42 -0
- package/dist/{codemodRunner-BjtB2lq6.js → codemodRunner-C2zxZUIw.js} +64 -9
- package/dist/{commands-DyxAmhP0.js → commands-BBYJ7Q3B.js} +96 -73
- package/dist/{dashboardCommand-BdKTyT13.js → dashboardCommand-D2kmyCLL.js} +3 -3
- package/dist/{dataCommand-Bab9X7s8.js → dataCommand-BEPPQiTl.js} +267 -195
- package/dist/dbCommand-BTyBGhIA.js +2 -0
- package/dist/{dbCommand-06O2finM.js → dbCommand-DZTmOFT4.js} +3 -3
- package/dist/{dev-C6LGF4iY.js → dev-Ca_A_S9v.js} +2439 -2397
- package/dist/{dev-GjJWAYo2.js → dev-DfVZaoys.js} +1 -1
- package/dist/{doctorCommand-etMkflRc.js → doctorCommand-CGZJK_4o.js} +21 -21
- package/dist/doctorCommand-djmqEcDC.js +2 -0
- package/dist/{dormancyCommand-UwZ1AZzB.js → dormancyCommand-DY2rYpTa.js} +1 -1
- package/dist/{embeddingsCommand-C70zWHwo.js → embeddingsCommand-BoCqZsgp.js} +1 -1
- package/dist/{envCommand-dSyKvRkM.js → envCommand-Bxy2fOjc.js} +15 -15
- package/dist/{evolveCommand-CG0_ebO5.js → evolveCommand-BsbZ-XDg.js} +2 -2
- package/dist/fileConventions-l-RIXbx8.js +36 -0
- package/dist/{fileTaxonomy-B7uxipWS.js → fileTaxonomy-CbyMQYx_.js} +37 -37
- package/dist/frameworkTableAssembly-Df2Ymp2f.js +2 -0
- package/dist/{frameworkTableAssembly-DKx3ba3S.js → frameworkTableAssembly-Do-cf6RJ.js} +96 -102
- package/dist/index.js +2 -2
- package/dist/{infoCommand-_53iOc_j.js → infoCommand-EmM3jPKD.js} +1 -1
- package/dist/{inspect-Bd8-9wsi.js → inspect-DCqILJ1G.js} +4 -0
- package/dist/inspect-DGJwpOAb.js +2 -0
- package/dist/interruptedReplace-CwnkBb2X.js +41 -0
- package/dist/interruptedReplace-qzmFI020.js +2 -0
- package/dist/manifestBuild-CJ2zvPvT.js +2 -0
- package/dist/{manifestBuild-Cqgsx2bM.js → manifestBuild-DjX5MoXy.js} +1 -1
- package/dist/{metaCommands-Cn2oboG4.js → metaCommands-x7RCi2AF.js} +2 -2
- package/dist/{migrate-Cko9rswM.js → migrate-CGFZS-1a.js} +2 -2
- package/dist/mobileCommand-D9O6iq3D.js +428 -0
- package/dist/mobileCommand-DAum7tsG.js +2 -0
- package/dist/{pageConvention-C938S8oC.js → pageConvention-CMpfDN6r.js} +1 -1
- package/dist/{privacyCommand-DWTQMC6R.js → privacyCommand-BCa2OoZG.js} +2 -2
- package/dist/{probeCommand-DkGGLknv.js → probeCommand-Bs3iVBSL.js} +1 -1
- package/dist/{projectScaffold-EzlErR4E.js → projectScaffold-CJfP-xbT.js} +1 -1
- package/dist/{projectScaffold-B4dmTlwT.js → projectScaffold-CSN0OzBV.js} +2 -2
- package/dist/renderModeScan-43yQ2opo.js +147 -0
- package/dist/{renderProfile-CskIgAfn.js → renderProfile-DvrhVJHa.js} +2 -2
- package/dist/{runtimeTrace-c0APJz7E.js → runtimeTrace-C1BTpHGQ.js} +1 -1
- package/dist/{sdkgen-BiQCgIEr.js → sdkgen-CXMwLg9n.js} +1 -1
- package/dist/{serveCommand-CueKQgzl.js → serveCommand-C7IrCD58.js} +899 -897
- package/dist/serveCommand-Cjt5S9hD.js +2 -0
- package/dist/serveEntry.js +1 -1
- package/dist/start-DH7cat4-.js +3 -0
- package/dist/{start-ekPan8BT.js → start-EOV7s1NZ.js} +544 -527
- package/dist/startEntry.js +1 -1
- package/dist/{staticCommand-xlSL-IWk.js → staticCommand-ey0kYmOT.js} +1 -1
- package/dist/{subcommandNames-DpYs3DXr.js → subcommandNames-CDzfEtKV.js} +3 -3
- package/dist/{templates-BR-fb4SP.js → templates-BTWZkJJT.js} +41 -9
- package/dist/{test-BWPQcRoB.js → test-DO27-x2P.js} +1 -1
- package/dist/{updateCommand-C_8I8Rzo.js → updateCommand-C_jN1w18.js} +1 -1
- package/dist/updateCommand-nnFjDbl4.js +2 -0
- package/dist/{webDev-C7jWJ5dX.js → webDev-1XpVnYkW.js} +1 -1
- package/dist/{webDev-oczpugbx.js → webDev-B7vNj4Bq.js} +1231 -1186
- package/dist/{webhooksCommand-4SVPDjKg.js → webhooksCommand-B1LVcyO3.js} +1 -1
- package/dist/workspaceDeps-RKEkX92S.js +45 -0
- package/package.json +31 -19
- package/templates/AGENTS.core.md +2 -0
- package/templates/AGENTS.md +4 -2
- package/templates/agent-docs/_index.md +2 -2
- package/templates/agent-docs/_manifest.json +1 -1
- package/templates/agent-docs/ai.md +4 -4
- package/templates/agent-docs/authentication.md +115 -0
- package/templates/agent-docs/cli.md +98 -12
- package/templates/agent-docs/data.md +121 -21
- package/templates/agent-docs/database/advancedqueries.md +1 -1
- package/templates/agent-docs/database/migrations.md +1 -1
- package/templates/agent-docs/database/seedsdialects.md +64 -2
- package/templates/agent-docs/internationalization.md +2 -0
- package/templates/agent-docs/introduction.md +25 -0
- package/templates/agent-docs/local-first-mobile.md +139 -41
- package/templates/agent-docs/observability.md +4 -2
- package/templates/agent-docs/plugins/atlassian.md +2 -2
- package/templates/agent-docs/plugins/audit.md +2 -2
- package/templates/agent-docs/plugins/billing.md +1 -1
- package/templates/agent-docs/plugins/cdc-out.md +8 -3
- package/templates/agent-docs/plugins/comments.md +22 -0
- package/templates/agent-docs/plugins/presence.md +32 -3
- package/templates/agent-docs/plugins/prometheus.md +2 -0
- package/templates/agent-docs/plugins/queue.md +47 -4
- package/templates/agent-docs/plugins.md +52 -14
- package/templates/agent-docs/reference.md +25 -4
- package/templates/agent-docs/routing.md +81 -9
- package/templates/agent-docs/scheduling.md +1 -1
- package/templates/agent-docs/schema-driven-ui.md +137 -1
- package/templates/agent-docs/templates/appshells.md +36 -4
- package/templates/agent-docs/whats-new.md +75 -158
- 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/README.md +3 -3
- package/templates/apps/api-collab/app.config.ts +1 -1
- package/templates/apps/api-collab/database/schema.ts +12 -8
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +1 -1
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +1 -1
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +1 -1
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-collab/template.json +1 -1
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +10 -2
- 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 +11 -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/package.json +7 -6
- 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/README.md +43 -24
- package/templates/apps/frontend-collab/app.config.ts +3 -3
- package/templates/apps/frontend-collab/package.json +14 -10
- package/templates/apps/frontend-collab/src/locales/de.ts +1 -2
- package/templates/apps/frontend-collab/src/locales/en.ts +1 -2
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +72 -76
- package/templates/apps/frontend-collab/src/pages/page.tsx +45 -22
- package/templates/apps/frontend-collab/template.json +2 -2
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +8 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/README.md +48 -0
- package/templates/apps/frontend-landing/app.config.ts +28 -0
- package/templates/apps/frontend-landing/package.json +7 -6
- package/templates/apps/frontend-landing/src/assets/hero.jpg +0 -0
- package/templates/apps/frontend-landing/src/fonts/Geist-Variable.woff2 +0 -0
- package/templates/apps/frontend-landing/src/fonts/LICENSE-Geist.txt +92 -0
- package/templates/apps/frontend-landing/src/globals.css +15 -0
- package/templates/apps/frontend-landing/src/globals.d.ts +17 -0
- package/templates/apps/frontend-landing/src/locales/de.ts +3 -2
- package/templates/apps/frontend-landing/src/locales/en.ts +5 -2
- package/templates/apps/frontend-landing/src/pages/page.test.tsx +79 -0
- package/templates/apps/frontend-landing/src/pages/page.tsx +26 -3
- package/templates/apps/frontend-landing/template.json +2 -2
- 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/package.json +8 -7
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +12 -11
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +29 -17
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +16 -0
- package/dist/agentsMd-SDDSkyl4.js +0 -2
- package/dist/apiBuild-DHtLXYx9.js +0 -2
- package/dist/codegen-BWpt3VgF.js +0 -2
- package/dist/codegenCommand-BOiWQ5hz.js +0 -137
- package/dist/dbCommand-B1EXBC6f.js +0 -2
- package/dist/doctorCommand-B0hX0tdz.js +0 -2
- package/dist/fileConventions-DASGEmj-.js +0 -35
- package/dist/frameworkTableAssembly-C_7Z-rMs.js +0 -2
- package/dist/inspect-CuoDInfZ.js +0 -2
- package/dist/interruptedReplace-C3O3M1MM.js +0 -28
- package/dist/interruptedReplace-CvmiAM9K.js +0 -2
- package/dist/manifestBuild-C4-J1-m_.js +0 -2
- package/dist/renderModeScan-CUbOeOAg.js +0 -122
- package/dist/serveCommand-DsnrVN3U.js +0 -2
- package/dist/start-BJzZLbt8.js +0 -3
- package/dist/updateCommand-Bqql_rsQ.js +0 -2
|
@@ -431,6 +431,15 @@ between acting on it and learning to skim it:
|
|
|
431
431
|
recorder resolves it through the relation registry instead, target and (for a
|
|
432
432
|
many-to-many) junction alike. A write to the junction changes membership,
|
|
433
433
|
which is exactly the change a user makes.
|
|
434
|
+
- **A `crud.*` executor is watched exactly like a hand-written one** — and it is
|
|
435
|
+
the case that needs it most. `crud.list('tasks', { include: { subTasks: true } })`
|
|
436
|
+
reads a table your own file never names, so there is nothing in front of you to
|
|
437
|
+
check `source:` against. The descriptor stays yours either way: `crud.*` supplies
|
|
438
|
+
only the executor, you write the `source:` beside it. `crud.count` counts as a
|
|
439
|
+
read too — it returns a number rather than rows, but an insert changes that
|
|
440
|
+
number, so the counted table belongs in `source:` or "page 3 of 12" stops moving.
|
|
441
|
+
`crud.create` / `update` / `remove` issue no read at all and never produce a
|
|
442
|
+
finding.
|
|
434
443
|
- **A table read only to NARROW a result is not counted** — a parent reached
|
|
435
444
|
through `inSubquery(...)`, or a read the framework made to resolve your row
|
|
436
445
|
filter. Those decide which rows come back rather than contributing rows, and
|
|
@@ -733,9 +742,12 @@ is always present (the fallback stands in until the first snapshot) and `loading
|
|
|
733
742
|
is a plain boolean reporting the true state. There is nothing to narrow.
|
|
734
743
|
|
|
735
744
|
**Errors.** `loading` means **no data has arrived yet** — it is not a claim that
|
|
736
|
-
the subscription is healthy. A **cold-start** failure (nothing ever arrived)
|
|
737
|
-
|
|
738
|
-
|
|
745
|
+
the subscription is healthy. A **cold-start** failure (nothing ever arrived) is
|
|
746
|
+
its own state: `loading` is `false`, `failed` is `true`, and `error` is
|
|
747
|
+
non-optional there, so branching on `loading` alone can no longer render a
|
|
748
|
+
skeleton forever. (It used to leave `loading` true, and the type's own comment
|
|
749
|
+
predicted the consequence — the fix was to stop making `loading` mean two
|
|
750
|
+
things rather than to keep warning about it.) A
|
|
739
751
|
failure AFTER data arrived deliberately does NOT replace good data with an error
|
|
740
752
|
banner (a transient websocket hiccup would blank a working screen); those reach
|
|
741
753
|
the api's error bus instead — subscribe with `useOnRpcError` for
|
|
@@ -1119,7 +1131,13 @@ export const employeesUpdate = defineMutation({
|
|
|
1119
1131
|
target: {
|
|
1120
1132
|
table: 'employees',
|
|
1121
1133
|
op: 'update',
|
|
1122
|
-
relations: {
|
|
1134
|
+
relations: {
|
|
1135
|
+
assignedStores: {
|
|
1136
|
+
junction: 'employee_assigned_stores',
|
|
1137
|
+
anchorColumn: 'employeeId', // the junction reference() pointing at `employees`
|
|
1138
|
+
targetColumn: 'storeId', // the junction's other reference()
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1123
1141
|
},
|
|
1124
1142
|
})
|
|
1125
1143
|
```
|
|
@@ -1127,9 +1145,7 @@ export const employeesUpdate = defineMutation({
|
|
|
1127
1145
|
After the executor succeeds, `input.assignedStores` is reconciled against the
|
|
1128
1146
|
junction via the diff-based link writer (`store.relationLinks`): missing rows
|
|
1129
1147
|
inserted, surplus rows deleted, unchanged rows untouched — so reactive
|
|
1130
|
-
subscriptions on the junction see one change per changed row.
|
|
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.
|
|
1148
|
+
subscriptions on the junction see one change per changed row.
|
|
1133
1149
|
|
|
1134
1150
|
The semantics worth knowing: an ABSENT input field leaves the links
|
|
1135
1151
|
untouched — absent is not empty; an empty array is the explicit "clear them
|
|
@@ -1137,6 +1153,34 @@ all". The row id comes from the executor's `output.id`, falling back to
|
|
|
1137
1153
|
`input.id`. The link writes go through `ctx.store`, so undo capture and
|
|
1138
1154
|
cross-table rules see them like any other write.
|
|
1139
1155
|
|
|
1156
|
+
### The same declaration drives the optimistic update
|
|
1157
|
+
|
|
1158
|
+
A junction change used to reach the browser only with the server delta — so on
|
|
1159
|
+
one submit the renamed title flipped immediately and the assigned stores sat on
|
|
1160
|
+
their old value until the roundtrip landed. It does not any more: `useMutation`
|
|
1161
|
+
reconciles the junction rows of every subscription sourced on `junction` the
|
|
1162
|
+
moment the mutation is sent, against the same `input[field]` the server will
|
|
1163
|
+
write.
|
|
1164
|
+
|
|
1165
|
+
It is a diff, not a redraw: a surviving link keeps its own row (and its real
|
|
1166
|
+
id), a surplus link disappears, and only a genuinely new link is a staged
|
|
1167
|
+
optimistic row. The patches ride the ordinary optimistic lane — reverted if the
|
|
1168
|
+
mutation fails, kept after it succeeds until the server data actually moves.
|
|
1169
|
+
Nothing is on a timer.
|
|
1170
|
+
|
|
1171
|
+
Client-side the anchor id is `input.id`; for an `insert` it is the same
|
|
1172
|
+
optimistic id the new row was stamped with, since the server's `output.id` is
|
|
1173
|
+
not knowable before the response arrives.
|
|
1174
|
+
|
|
1175
|
+
**Why you state the two columns.** The optimistic patch runs in the BROWSER, and
|
|
1176
|
+
the browser cannot import your `db/` schema — `@voltro/database` is server-only
|
|
1177
|
+
by construction — so the junction's two `reference()` columns cannot be derived
|
|
1178
|
+
there. `anchorColumn` is the one pointing at the target's own table;
|
|
1179
|
+
`targetColumn` is the other. They are not taken on trust: before it writes, the
|
|
1180
|
+
server compares your declaration against the junction's real reference columns
|
|
1181
|
+
and refuses, naming the correct pair, if they disagree. A self-junction (both
|
|
1182
|
+
columns referencing one table) is still refused by name, never guessed.
|
|
1183
|
+
|
|
1140
1184
|
## Typed Errors
|
|
1141
1185
|
|
|
1142
1186
|
```ts
|
|
@@ -1717,7 +1761,7 @@ created by every migration and diffed on every boot.
|
|
|
1717
1761
|
|
|
1718
1762
|
The other tempting option is to point `source:` at a name that resolves to
|
|
1719
1763
|
nothing. That is worse than the empty table: the [stale-`source` boot
|
|
1720
|
-
warning](#fan-out
|
|
1764
|
+
warning](#fan-out-how-many-subscribers-may-one-change-wake) is the only signal
|
|
1721
1765
|
for a subscription that has gone permanently quiet, and an exemption for a name
|
|
1722
1766
|
you invented disables it for the one case it was built for.
|
|
1723
1767
|
|
|
@@ -1847,7 +1891,7 @@ Two consequences worth knowing:
|
|
|
1847
1891
|
- **Not free per subscriber.** A publish wakes every subscriber of that channel
|
|
1848
1892
|
and re-runs each one's executor; the channel is one routing key, so
|
|
1849
1893
|
subscribers looking at different slices of the state are woken too. Publish on
|
|
1850
|
-
a real change, not on a timer — see [Fan-out](#fan-out
|
|
1894
|
+
a real change, not on a timer — see [Fan-out](#fan-out-how-many-subscribers-may-one-change-wake).
|
|
1851
1895
|
|
|
1852
1896
|
## Query Executor
|
|
1853
1897
|
|
|
@@ -2065,9 +2109,10 @@ the resume window (`reactive.resume.windowMs`, default 60 s) the server replays
|
|
|
2065
2109
|
**only the deltas the client missed** — the re-subscribe presents the last
|
|
2066
2110
|
materialised revision and the stream continues on the same revision line, so a
|
|
2067
2111
|
short offline gap costs a handful of patches instead of every row. Outside the
|
|
2068
|
-
window, for computed queries, for
|
|
2112
|
+
window, for computed queries, for a subscription whose source table a
|
|
2113
|
+
registered row filter may narrow, or whenever anything is in
|
|
2069
2114
|
doubt, the query answers with a fresh snapshot — the delta-resume wire contract
|
|
2070
|
-
lives in [the wire protocol](/docs/data/wire-protocol#reconnect
|
|
2115
|
+
lives in [the wire protocol](/docs/data/wire-protocol#reconnect-delta-resume).
|
|
2071
2116
|
|
|
2072
2117
|
**What is on screen while that happens is your last-known-good data, not a
|
|
2073
2118
|
skeleton.** The replacement cache is seeded from the one it retires, so `data`
|
|
@@ -2172,6 +2217,15 @@ subscriber on every delivery, on purpose — a role revoked or a share withdrawn
|
|
|
2172
2217
|
to end the stream on the very NEXT delivery, not whenever a cache happens to
|
|
2173
2218
|
expire — and each of them can be a database round-trip.
|
|
2174
2219
|
|
|
2220
|
+
**On every transport.** A live query can leave the server three ways — the
|
|
2221
|
+
WebSocket the browser client uses, an [SSE
|
|
2222
|
+
stream](/docs/data/rest-routes#live-updates-over-http-stream-sse), and a
|
|
2223
|
+
[gRPC](/docs/data/grpc) server-streaming rpc — and all three resolve per
|
|
2224
|
+
delivery through the same code: guards re-checked before each frame, row
|
|
2225
|
+
visibility re-derived from the unfiltered base descriptor for each frame, and a
|
|
2226
|
+
revoked scope ending the stream. The transport decides how the frame is
|
|
2227
|
+
framed, never what the subject may see.
|
|
2228
|
+
|
|
2175
2229
|
So deliveries run **concurrently, up to a bound**. The default is 8 in flight.
|
|
2176
2230
|
Measured with 50 subscribers behind a 5 ms guard: 517 ms to serve all of them
|
|
2177
2231
|
serially, 72 ms at 8 lanes.
|
|
@@ -3205,7 +3259,9 @@ es.addEventListener('delta', (e) => applyDelta(JSON.parse(e.data)))
|
|
|
3205
3259
|
|
|
3206
3260
|
Each event's `_tag` becomes the SSE `event:` name, so a client listens per kind instead of switching on a payload field. The framing handles the details that bite otherwise: embedded newlines are split across `data:` lines (a raw `\n` would truncate the event), a `retry:` hint is sent, and a keep-alive comment goes out every 15s so proxies don't drop an idle stream.
|
|
3207
3261
|
|
|
3208
|
-
Same guarantees as the WebSocket path, because it is the same code: the declarative `guards:`, the row filter and tenant scoping all run before anything is emitted, and the client's disconnect tears the subscription down (including a disconnect *during* setup). A guard denial arrives as one `error` event rather than an HTTP status — by then the response headers are already sent.
|
|
3262
|
+
Same guarantees as the WebSocket path, because it is the same code: the declarative `guards:`, the [row filter](/docs/authentication/row-level-security) and tenant scoping all run before anything is emitted, and the client's disconnect tears the subscription down (including a disconnect *during* setup). A guard denial arrives as one `error` event rather than an HTTP status — by then the response headers are already sent.
|
|
3263
|
+
|
|
3264
|
+
And they run before **every** event, not only before the first one: the guards are re-checked and the subject's row visibility is re-resolved from the unfiltered base descriptor per delivery, so a scope revoked while the `EventSource` is open ends the stream on the next event, and a membership that ends stops carrying those rows in the next `delta`. An open SSE stream is not a cheaper read path than a fresh `GET`.
|
|
3209
3265
|
|
|
3210
3266
|
`stream: 'sse'` on a mutation or action is ignored: there is nothing to subscribe to.
|
|
3211
3267
|
|
|
@@ -4999,11 +5055,29 @@ A resume is declined — always with a fresh snapshot — when:
|
|
|
4999
5055
|
- the resuming caller is a different subject or tenant (a login, logout or
|
|
5000
5056
|
tenant switch between disconnect and resume) — the retained history is keyed
|
|
5001
5057
|
by subject AND tenant, so a changed identity simply never finds it;
|
|
5002
|
-
- the
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5058
|
+
- the query is a **computed** query — it re-runs a handler, so there is no
|
|
5059
|
+
delta chain to replay;
|
|
5060
|
+
- a registered row filter (`setRowFilter`) can narrow THIS subscription's
|
|
5061
|
+
source table, or the query declares an eager `.with(...)`. A row-filtered
|
|
5062
|
+
subscription's visible row set exists only per delivery, so replaying it
|
|
5063
|
+
could serve rows the subject has since lost.
|
|
5064
|
+
|
|
5065
|
+
**This is per table, not per app.** A filter that declares
|
|
5066
|
+
`tables: [...]` (see [row-level security](/docs/authentication/row-level-security))
|
|
5067
|
+
keeps delta-resume on every subscription whose source is not in that set —
|
|
5068
|
+
the common case, since most filters narrow a handful of tables. Without the
|
|
5069
|
+
declaration the framework cannot know which tables the predicate may reach
|
|
5070
|
+
and excludes them all. Eager loads are excluded wholesale because a relation
|
|
5071
|
+
resolves below the seam that narrows. They reconnect with a fresh snapshot,
|
|
5072
|
+
exactly as before.
|
|
5073
|
+
|
|
5074
|
+
**Which of your queries actually got a ring** is recorded per label, since the
|
|
5075
|
+
excluded and the never-eligible look identical on the wire:
|
|
5076
|
+
`/_voltro/inspect/subscriptions` returns a `resume` array of
|
|
5077
|
+
`{ label, resumable, excluded }`, and `voltro dev` logs each verdict once under
|
|
5078
|
+
the `voltro:resume` scope. A `computed` verdict is the one worth reading first:
|
|
5079
|
+
it means the executor returns a value rather than a descriptor, so no row-filter
|
|
5080
|
+
declaration can ever change it.
|
|
5007
5081
|
|
|
5008
5082
|
**Author a live-subscribed getter to return, not throw.** A subscription is a
|
|
5009
5083
|
long-lived stream, so a getter that throws on every re-evaluation is a broken
|
|
@@ -6101,6 +6175,8 @@ export default {
|
|
|
6101
6175
|
port: 50051,
|
|
6102
6176
|
procedures: ['orders.get', 'orders.list', 'orders.create'],
|
|
6103
6177
|
// tls: { certPath, keyPath, caPath? } — plaintext without it (dev / mesh).
|
|
6178
|
+
// drainMs: 5000, // shutdown drain budget — see below
|
|
6179
|
+
// maxMessageBytes, maxMetadataBytes — grpc-js frame limits
|
|
6104
6180
|
},
|
|
6105
6181
|
}
|
|
6106
6182
|
```
|
|
@@ -6112,6 +6188,21 @@ works out of the box). The gRPC packages ship as script-free optional
|
|
|
6112
6188
|
dependencies of `@voltro/cli`; a configured `grpc:` block with them missing
|
|
6113
6189
|
refuses the boot by name.
|
|
6114
6190
|
|
|
6191
|
+
## Shutdown drains, then forces — `drainMs`
|
|
6192
|
+
|
|
6193
|
+
On SIGTERM the surface flips its health status to `NOT_SERVING` (so a load
|
|
6194
|
+
balancer stops sending it work) and gives open calls **`drainMs`** to finish
|
|
6195
|
+
before force-closing them. Default `5000`; `0` forces immediately; the env
|
|
6196
|
+
override is `VOLTRO_GRPC_DRAIN_MS`.
|
|
6197
|
+
|
|
6198
|
+
Pick it from two numbers only you have. Keep it **below** your orchestrator's
|
|
6199
|
+
termination grace (`terminationGracePeriodSeconds`, `docker stop -t`) — past
|
|
6200
|
+
that point SIGKILL arrives and the drain never completes, so a larger budget
|
|
6201
|
+
buys nothing. Keep it **above** your longest legitimately in-flight unary
|
|
6202
|
+
call, or every rolling deploy force-closes work that would have finished. When
|
|
6203
|
+
the budget is exceeded the surface says so in a warning naming the budget,
|
|
6204
|
+
rather than leaking the port into the next boot.
|
|
6205
|
+
|
|
6115
6206
|
## Field numbers are managed — `grpc.manifest.json`
|
|
6116
6207
|
|
|
6117
6208
|
Field numbers are the proto wire identity, so they may never depend on
|
|
@@ -6169,10 +6260,19 @@ sleeping action whose post-sleep write never lands).
|
|
|
6169
6260
|
A `query` becomes a **server-streaming** rpc: each frame is the CURRENT full
|
|
6170
6261
|
snapshot, re-pushed live when the query's `source:` changes — subscribe,
|
|
6171
6262
|
mutate from anywhere, and the open stream receives the new frame with no
|
|
6172
|
-
re-request.
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6263
|
+
re-request.
|
|
6264
|
+
|
|
6265
|
+
**Authorization is re-derived per FRAME, not frozen at open.** Before every
|
|
6266
|
+
delivery the framework re-runs the query's `guards:` and re-resolves the
|
|
6267
|
+
subject's [row-level visibility](/docs/authentication/row-level-security)
|
|
6268
|
+
from the unfiltered base descriptor. A revoked scope ends the stream with the
|
|
6269
|
+
mapped status; a membership that ends mid-stream stops carrying those rows in
|
|
6270
|
+
the next frame, with the stream itself untouched. This is the same code the
|
|
6271
|
+
WebSocket and SSE transports run — an open gRPC stream is not a cheaper read
|
|
6272
|
+
path than a fresh call.
|
|
6273
|
+
|
|
6274
|
+
Slow consumers are handled through grpc-js write backpressure — frames
|
|
6275
|
+
coalesce to the latest snapshot rather than buffering unboundedly.
|
|
6176
6276
|
|
|
6177
6277
|
## Declared limits (v1)
|
|
6178
6278
|
|
|
@@ -627,7 +627,7 @@ await ctx.store.update('notes', id, {
|
|
|
627
627
|
|
|
628
628
|
Two layers of validation apply:
|
|
629
629
|
|
|
630
|
-
1. **JSON validity** — that the stored bytes are well-formed JSON — is enforced automatically by the database on every dialect (see [Storage + validation per dialect](#storage
|
|
630
|
+
1. **JSON validity** — that the stored bytes are well-formed JSON — is enforced automatically by the database on every dialect (see [Storage + validation per dialect](#storage-validation-per-dialect)). You don't declare anything.
|
|
631
631
|
2. **JSON *shape*** — that the value matches your expected structure — is up to you: enforce it at the table level with `table().validate(Schema)`:
|
|
632
632
|
|
|
633
633
|
```ts
|
|
@@ -700,7 +700,7 @@ A handler that a `voltro dev` session or a test actually ran is reported with wh
|
|
|
700
700
|
|
|
701
701
|
### What the codemod does per kind
|
|
702
702
|
|
|
703
|
-
- **`rename-column`** gets a real `transform`: it renames the field in the `*.entity.ts` AND chains **`.renamedFrom('old')`** (so the differ plans a catalog RENAME, not the lossy drop+create described [above](#
|
|
703
|
+
- **`rename-column`** gets a real `transform`: it renames the field in the `*.entity.ts` AND chains **`.renamedFrom('old')`** (so the differ plans a catalog RENAME, not the lossy drop+create described [above](#renamedfrom-oldname)), then annotates the handler sites the blast radius found.
|
|
704
704
|
- **`retype-column` / `split-column` / `drop-column` / `rename-table`** are reshaping changes with no single mechanical rewrite, so they get a **`manual`** codemod: a generated, numbered checklist of the edits + the annotation to add, printed for you to apply.
|
|
705
705
|
|
|
706
706
|
`voltro evolve` produces the plan; it does not apply the schema change. **`voltro check` is the gate on the result**, and `voltro db apply` lands it — after `--write`, review the annotated handlers, then run those two.
|
|
@@ -391,7 +391,7 @@ What the framework hides for you vs what's worth knowing. Per-dialect pages dril
|
|
|
391
391
|
| `RETURNING *` on DELETE | yes | no | yes (10.0+) | OUTPUT DELETED.* | yes | yes |
|
|
392
392
|
| Parameterized `LIMIT ?` | yes | no — integer-literal inlined | yes | no — integer-literal inlined | yes | yes |
|
|
393
393
|
| `LIMIT N OFFSET N` syntax | yes | yes | yes | no — `OFFSET … ROWS FETCH NEXT … ROWS ONLY` | yes | yes |
|
|
394
|
-
| DEFAULT on TEXT columns | yes | **NO** —
|
|
394
|
+
| DEFAULT on TEXT columns | yes | **NO** — framework emits VARCHAR(255) | yes — framework still emits VARCHAR(255) (engine parity) | yes — framework emits NVARCHAR(450) (indexable) | yes | yes |
|
|
395
395
|
| Native JSON column type | JSONB | JSON | JSON | NVARCHAR(MAX) | TEXT | TEXT |
|
|
396
396
|
| JSON columns returned as objects | yes | yes | yes | **no — strings** — framework auto-parses | **no — strings** — framework auto-parses | **no — strings** — framework auto-parses |
|
|
397
397
|
| Booleans | proper booleans | 0/1 (TINYINT) | 0/1 | BIT (proper bool) | 0/1 (INTEGER) | 0/1 (INTEGER) |
|
|
@@ -796,11 +796,48 @@ The framework's DDL emitter detects the case and switches to `VARCHAR(255)`:
|
|
|
796
796
|
```typescript
|
|
797
797
|
text().default('json') // → VARCHAR(255) DEFAULT 'json'
|
|
798
798
|
text().oneOf(['a', 'b', 'c']).default('a') // → VARCHAR(255) DEFAULT 'a' CHECK (col IN ('a','b','c'))
|
|
799
|
-
text().nullable() // →
|
|
799
|
+
text().nullable() // → LONGTEXT (unchanged — no default to trip up)
|
|
800
800
|
```
|
|
801
801
|
|
|
802
802
|
VARCHAR(255) is the framework's heuristic — enough for typical enum-like values, short status strings, format identifiers. If you need longer defaulted text, declare the column as `text().nullable()` + handle the missing-default case in application code, OR drop down to `unsafe()`.
|
|
803
803
|
|
|
804
|
+
### Adding a default to an existing column
|
|
805
|
+
|
|
806
|
+
The rule holds for a migration too, not only for `CREATE TABLE`. Adding
|
|
807
|
+
`.default(…)` to a `text()` column that already exists **reshapes** the column
|
|
808
|
+
rather than setting a default on it:
|
|
809
|
+
|
|
810
|
+
```sql
|
|
811
|
+
ALTER TABLE tickets MODIFY COLUMN `status` VARCHAR(255) NOT NULL DEFAULT 'active'
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
That is deliberate, and it is what makes the change appliable at all: a plain
|
|
815
|
+
`ALTER TABLE … ALTER COLUMN status SET DEFAULT 'active'` is answered by MySQL
|
|
816
|
+
with `BLOB, TEXT, GEOMETRY or JSON column 'status' can't have a default value`,
|
|
817
|
+
so the migration would stop half-applied. Reshaping means the column has the same
|
|
818
|
+
type whether the default was declared before or after the table existed.
|
|
819
|
+
|
|
820
|
+
Two consequences worth knowing before you run it:
|
|
821
|
+
|
|
822
|
+
- **It is a narrowing.** If a row already holds more than 255 characters, the
|
|
823
|
+
ALTER fails (`Data too long for column 'status'`) and the migration stops
|
|
824
|
+
before it. Check first, and pick the width yourself with
|
|
825
|
+
`text().maxLength(n).default(…)` if 255 is too small:
|
|
826
|
+
|
|
827
|
+
```sql
|
|
828
|
+
SELECT COUNT(*) FROM tickets WHERE CHAR_LENGTH(status) > 255
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
- **Removing a default does not reshape back.** `DROP DEFAULT` is legal on any
|
|
832
|
+
mysql type, and widening a `VARCHAR(255)` back to `LONGTEXT` would fail for an
|
|
833
|
+
indexed column — so the column keeps its bounded type. Declare
|
|
834
|
+
`text().maxLength(255)` if you want that to be visible in the schema.
|
|
835
|
+
|
|
836
|
+
SQL Server does the same thing for its own reason (an `NVARCHAR(MAX)` column
|
|
837
|
+
cannot be indexed, so a defaulted text column is `NVARCHAR(450)`). On postgres a
|
|
838
|
+
`TEXT` column takes a `DEFAULT` directly, and SQLite rebuilds the table to the
|
|
839
|
+
declared shape — neither reshapes anything.
|
|
840
|
+
|
|
804
841
|
## JSON columns
|
|
805
842
|
|
|
806
843
|
`json()` columns emit `JSON` (mysql's native binary JSON type since 5.7+). The driver auto-parses on read; same shape as postgres. No coercion overhead.
|
|
@@ -1187,6 +1224,31 @@ When the caller didn't supply an `orderBy` but did set `skip` (uncommon but lega
|
|
|
1187
1224
|
|
|
1188
1225
|
The compiler inlines integer literals for TOP/OFFSET/FETCH NEXT values rather than parameter binding. Same rationale as MySQL — tedious has bind-as-INT issues with large or unexpected-typed numeric params.
|
|
1189
1226
|
|
|
1227
|
+
## Text columns with a DEFAULT — NVARCHAR(450)
|
|
1228
|
+
|
|
1229
|
+
A plain `text()` column is `NVARCHAR(MAX)`, which SQL Server cannot index. A text
|
|
1230
|
+
column that carries a literal default or a closed value set is therefore emitted
|
|
1231
|
+
bounded, at `NVARCHAR(450)` — under the 900-byte single-column key limit, so it
|
|
1232
|
+
stays indexable:
|
|
1233
|
+
|
|
1234
|
+
```typescript
|
|
1235
|
+
text().default('open') // → NVARCHAR(450) + a DEFAULT constraint
|
|
1236
|
+
text().oneOf(['open', 'closed']) // → NVARCHAR(450) + a CHECK constraint
|
|
1237
|
+
text().nullable() // → NVARCHAR(MAX)
|
|
1238
|
+
```
|
|
1239
|
+
|
|
1240
|
+
This holds for migrations as well as for `CREATE TABLE`: adding `.default(…)` to
|
|
1241
|
+
an existing text column retypes it to `NVARCHAR(450)` and then adds the default
|
|
1242
|
+
constraint, so the column has the same type whether the default was declared
|
|
1243
|
+
before or after the table existed. It is a narrowing — if a row already holds
|
|
1244
|
+
more than 450 characters, the `ALTER COLUMN` fails ("String or binary data would
|
|
1245
|
+
be truncated") and the migration stops there. Check first, and use
|
|
1246
|
+
`text().maxLength(n).default(…)` to choose a different width:
|
|
1247
|
+
|
|
1248
|
+
```sql
|
|
1249
|
+
SELECT COUNT(*) FROM tickets WHERE LEN(status) > 450
|
|
1250
|
+
```
|
|
1251
|
+
|
|
1190
1252
|
## JSON columns — NVARCHAR(MAX) + auto-parse
|
|
1191
1253
|
|
|
1192
1254
|
`json()` columns emit `NVARCHAR(MAX)` in DDL — mssql has no native JSON type pre-2025. Validation goes through `ISJSON(col) = 1` CHECK constraints; serialization is application-side.
|
|
@@ -78,6 +78,8 @@ Server-side, the active locale is determined by, in priority order:
|
|
|
78
78
|
2. **`Accept-Language` header** — the browser/OS preference, q-weighted and sorted per RFC 4647.
|
|
79
79
|
3. **`defaultLocale`** — last-resort fallback.
|
|
80
80
|
|
|
81
|
+
**One resolver decides, and everything the server renders for that request reads its answer** — the page render and its `<I18nProvider>`, the `<html lang>` attribute, the ISR cache key (so a language switch cannot re-serve the previous locale's cached HTML), and the validation errors an [`<AutoForm>` renders on the no-JavaScript form-POST path](/docs/ui/forms-and-tables#forms-without-javascript). That last one is worth naming because a server has no `<html lang>` to read yet at the time it validates; deriving the locale a second way there would answer `en` for every request.
|
|
82
|
+
|
|
81
83
|
The resolved locale is **guaranteed** to be one of the codes in `locales`. Any unsupported value (a cookie pointing at a code you no longer ship, a browser asking for `xx-YY`) falls through to the next signal. RFC 4647 lookup strips subtags one segment at a time — `de-CH-1996` → `de-CH` → `de` — so a `de` catalog serves a `de-CH` browser.
|
|
82
84
|
|
|
83
85
|
The client **adopts what the server resolved**, reading it from the `<html lang>` attribute the server render sets, then falling back to the cookie and the default. `Accept-Language` is never read in the browser: `navigator.languages` can diverge from what the server saw.
|
|
@@ -544,6 +544,16 @@ export const searchParams = Schema.Struct({
|
|
|
544
544
|
|
|
545
545
|
- `searchParams` (an `effect/Schema` struct — every field optional or with a default) types the page's query string: `useSearchParams(searchParams)` returns the decoded shape, and links built with `withQuery` type-check against it. Details: [Pages → Query strings](/docs/routing/pages#query-strings).
|
|
546
546
|
|
|
547
|
+
Two more page exports change what the framework produces for a route:
|
|
548
|
+
|
|
549
|
+
```tsx
|
|
550
|
+
export const ogImage = ({ params, loaderData, locale }) => ({ type: 'div', props: { /* satori JSX */ } })
|
|
551
|
+
export const intercept = { from: '/photos' }
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
- `ogImage` declares the page's `og:image` as a satori JSX template. `static` pages render the PNG at BUILD time into `dist/assets/og/`; `ssr` pages render it on demand over a signed route. The `og:image` / `twitter:image` / `twitter:card` tags are injected automatically unless your own `meta` already sets them. A declared font is REQUIRED (there is no bundled default), and an `ssr` route exporting it needs `VOLTRO_OG_SECRET` — `voltro start` refuses the boot otherwise. Details: [Loaders and meta → OG images](/docs/routing/loaders-and-meta#og-images-from-a-template-ogimage).
|
|
555
|
+
- `intercept` makes the page an **intercepting route**: `from` names one or more ROUTE PATTERNS (`'/photos'`, `['/photos', '/albums/[id]']`), and a soft navigation arriving from one of them renders this page as an overlay above the still-mounted origin. Every hard load — and a soft navigation from anywhere else — renders it standalone. Details: [Intercepting routes](/docs/routing/intercepting-routes).
|
|
556
|
+
|
|
547
557
|
## Discovery in practice
|
|
548
558
|
|
|
549
559
|
```text
|
|
@@ -605,11 +615,26 @@ If yes, the promise belongs in the name — you cannot see a contract before you
|
|
|
605
615
|
| `*.store.ts` | exactly one `defineStore`, no server state | `store/one-per-file`, `store/mirrors-server-state` |
|
|
606
616
|
| `*.collection.ts` | declares content collections (`defineCollection`); frontmatter schema violations fail the build naming the file | the build's collection decode + reference validation |
|
|
607
617
|
| `*.consumer.ts` | declares queue consumers (`defineQueueConsumer`, @voltro/plugin-queue); loading registers, the plugin's activation starts them | the queue runner (decode→DLQ, retry→DLQ, commit-per-message) |
|
|
618
|
+
| `*.ws.ts` | default-exports one raw WebSocket gateway (`defineWebSocket`), mounting its own upgrade path beside the rpc socket | boot discovery on BOTH paths (`voltro dev` and `voltro serve`); two gateways on one path refuse the boot |
|
|
608
619
|
|
|
609
620
|
A `*.component.tsx` promises exactly ONE component. It does not promise to export nothing else: types, and plain module-local values a `const COLUMNS = […]` beside the table that renders them, are fine and always were. What the rule counts is components — a declaration that renders — so an object, an array, a string or a `new` beside your component is not a second one, and neither is `export default Card` next to `export const Card`.
|
|
610
621
|
|
|
611
622
|
The BOUNDARY rules (`internal/foreign-import`, `fixture/production-import`, `ui/unlinked`) are assertions about your import graph, so it is worth knowing which edges they follow: relative specifiers, your tsconfig `paths` aliases (read from the nearest `tsconfig.json`, so a per-app `@/*` works when you run `voltro doctor` at the repo root), `export … from` re-exports, and dynamic `import()`. A package import is a leaf — the walk stops at the edge of your app.
|
|
612
623
|
|
|
624
|
+
## Contracts that are not suffixes
|
|
625
|
+
|
|
626
|
+
The admission test above is about the PROMISE, not about the spelling — and three of the framework's conventions carry one without being a suffix on a filename. They are listed here because a reader looking for "what does the framework read out of my tree" would otherwise stop at the table:
|
|
627
|
+
|
|
628
|
+
| Convention | Promise | Read by |
|
|
629
|
+
|---|---|---|
|
|
630
|
+
| `searchParams` page export | the page's query string decodes through this `effect/Schema` struct — every field optional or with a default | `useSearchParams(searchParams)`, `withQuery` link typing, and the render-mode scan (a page declaring BOTH `renderMode: 'isr'` and `searchParams` is refused) |
|
|
631
|
+
| `ogImage` page export | this route's `og:image` is a satori JSX template, not a file you ship | the build (`static` → a hashed PNG in `dist/assets/og/`) and `voltro start` (`ssr` → a signed on-demand route, which needs `VOLTRO_OG_SECRET`) |
|
|
632
|
+
| `intercept` page export | `from` names the routes a soft navigation may arrive from for this page to render as an overlay above them | the client router; a hard load renders the page standalone regardless |
|
|
633
|
+
| `grpc.manifest.json` (app root) | field numbers are checked in and append-only — a deleted field goes `reserved`, never re-used | `voltro grpc proto` and the gRPC surface wiring, which derive wire identity from it rather than from declaration order |
|
|
634
|
+
| `content/<name>/**` | the files a `*.collection.ts` declares — markdown with frontmatter, or `.json` for a data collection | `getCollection` / `getEntry`, the build's collection artifacts, and the dev server's watcher |
|
|
635
|
+
|
|
636
|
+
The page exports are per-ROUTE and the last two are per-APP, which is the only reason they cannot be spellings: there is nothing to rename.
|
|
637
|
+
|
|
613
638
|
## `*.component.ui.tsx` — reads, never writes
|
|
614
639
|
|
|
615
640
|
```tsx
|