@voltro/cli 0.8.0 → 0.10.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 +329 -7
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/dist/apiBuild-BbR1nYcE.js +2 -0
- package/dist/{apiBuild-OrDkXGFX.js → apiBuild-DTLPSlt9.js} +8 -8
- package/dist/bin.js +2 -2
- package/dist/{commands-DT4PipBz.js → commands-B0QT8dUY.js} +2080 -1638
- package/dist/dev-BJvbwjSz.js +2 -0
- package/dist/{dev-Ga65K4jc.js → dev-CK9fbuAM.js} +1433 -1367
- package/dist/index.js +1 -1
- package/dist/{inspectMetrics-EK0CcHyu.js → inspectMetrics-BeMjgt8x.js} +1239 -957
- package/dist/inspectState.d.ts +6 -2
- package/dist/{serveCommand-CR5xnYZt.js → serveCommand-YR3mKjvf.js} +367 -384
- package/dist/serveEntry.js +3 -3
- package/dist/{start-DuyBQxW3.js → start-C_M4fGS2.js} +391 -340
- package/dist/startEntry.js +2 -2
- package/package.json +18 -17
- package/templates/agent-docs/cli.md +94 -18
- package/templates/agent-docs/data.md +109 -1
- package/templates/agent-docs/database/seedsdialects.md +20 -4
- package/templates/agent-docs/routing.md +77 -14
- package/templates/agent-docs/scheduling.md +32 -0
- package/templates/apps/api-ai/package.json +7 -7
- 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-storage/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-durable/schedules/nightlyReport.cron.tsx +5 -0
- package/templates/apps/api-durable/subscribers/orderChanges.subscribe.ts +8 -2
- 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-saas/package.json +11 -11
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +8 -8
- package/templates/apps/changelog/package.json +6 -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 +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- 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 +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- 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 +6 -6
- package/dist/apiBuild-CmtI6Ygh.js +0 -2
- package/dist/dev-Ca1jdwnt.js +0 -2
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { N as e } from "./inspectMetrics-BeMjgt8x.js";
|
|
2
|
+
import { t } from "./start-C_M4fGS2.js";
|
|
3
3
|
export { e as loadDotEnv, t as runStartCommand };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -57,24 +57,25 @@
|
|
|
57
57
|
"@effect/platform-node": "^0.107.0",
|
|
58
58
|
"@effect/sql": "^0.51.1",
|
|
59
59
|
"@effect/workflow": "^0.18.2",
|
|
60
|
-
"@voltro/ai": "0.
|
|
61
|
-
"@voltro/cache": "0.
|
|
62
|
-
"@voltro/data-transfer": "0.
|
|
63
|
-
"@voltro/database": "0.
|
|
64
|
-
"@voltro/env": "0.
|
|
65
|
-
"@voltro/kv": "0.
|
|
66
|
-
"@voltro/logger": "0.
|
|
67
|
-
"@voltro/plugin-auth": "0.
|
|
68
|
-
"@voltro/plugin-broadcast": "0.
|
|
69
|
-
"@voltro/plugin-mail": "0.
|
|
70
|
-
"@voltro/plugin-storage": "0.
|
|
71
|
-
"@voltro/plugin-webhooks": "0.
|
|
72
|
-
"@voltro/protocol": "0.
|
|
73
|
-
"@voltro/runtime": "0.
|
|
74
|
-
"@voltro/serverless": "0.
|
|
75
|
-
"@voltro/workflow": "0.
|
|
60
|
+
"@voltro/ai": "0.10.0",
|
|
61
|
+
"@voltro/cache": "0.10.0",
|
|
62
|
+
"@voltro/data-transfer": "0.10.0",
|
|
63
|
+
"@voltro/database": "0.10.0",
|
|
64
|
+
"@voltro/env": "0.10.0",
|
|
65
|
+
"@voltro/kv": "0.10.0",
|
|
66
|
+
"@voltro/logger": "0.10.0",
|
|
67
|
+
"@voltro/plugin-auth": "0.10.0",
|
|
68
|
+
"@voltro/plugin-broadcast": "0.10.0",
|
|
69
|
+
"@voltro/plugin-mail": "0.10.0",
|
|
70
|
+
"@voltro/plugin-storage": "0.10.0",
|
|
71
|
+
"@voltro/plugin-webhooks": "0.10.0",
|
|
72
|
+
"@voltro/protocol": "0.10.0",
|
|
73
|
+
"@voltro/runtime": "0.10.0",
|
|
74
|
+
"@voltro/serverless": "0.10.0",
|
|
75
|
+
"@voltro/workflow": "0.10.0",
|
|
76
76
|
"chokidar": "^5.0.0",
|
|
77
77
|
"ioredis": "^5.11.1",
|
|
78
|
+
"tinyglobby": "^0.2.17",
|
|
78
79
|
"ts-morph": "^28.0.0"
|
|
79
80
|
},
|
|
80
81
|
"optionalDependencies": {
|
|
@@ -427,37 +427,63 @@ voltro dev apps/acme/api # explicit path
|
|
|
427
427
|
6. Watches `src/`. On save, Vite HMR fires:
|
|
428
428
|
- A page / layout **component** change → React Fast Refresh patches the live
|
|
429
429
|
component in place; client state survives (see below).
|
|
430
|
-
- A page
|
|
430
|
+
- A page-local **value export** (`const COLUMNS = [...]`) change → also a hot
|
|
431
|
+
update, even in the same save as the JSX (see below).
|
|
432
|
+
- A **server-read export** — `loader`, `renderMode`, `meta`, … — change → a
|
|
431
433
|
full page reload, on purpose (see below).
|
|
432
434
|
- CSS changes → swap stylesheets in place.
|
|
433
435
|
- New page file → regen the entry files → reload the route tree.
|
|
434
436
|
|
|
437
|
+
`renderMode: 'ssr'` pages compile on demand the first time each route is hit. To
|
|
438
|
+
keep a burst of cold pages — several browser tabs, or a health-check sweep across
|
|
439
|
+
many routes — from spiking memory, `voltro dev` compiles at most **4** of them at
|
|
440
|
+
once and collapses duplicate concurrent requests for the same route into a single
|
|
441
|
+
compile. Already-compiled (warm) pages are never throttled, so a hot app stays
|
|
442
|
+
fully concurrent. Tune the cap with `VOLTRO_DEV_SSR_COMPILE_CONCURRENCY` (below) —
|
|
443
|
+
drop it on a low-memory box, raise it on a big machine.
|
|
444
|
+
|
|
435
445
|
|
|
436
446
|
### Fast Refresh: what hot-updates and what reloads
|
|
437
447
|
|
|
438
448
|
Editing a **page or layout component** applies as a hot update — the React tree
|
|
439
449
|
stays mounted, so form input, scroll position, open dialogs and every `useState`
|
|
440
|
-
survive.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
450
|
+
survive. So does editing a **page-local value** the page happens to export — a
|
|
451
|
+
`const COLUMNS = [...]` you change together with the table that renders it. The
|
|
452
|
+
module re-evaluates, the component renders the new value, and your client state
|
|
453
|
+
is untouched.
|
|
454
|
+
|
|
455
|
+
A full page reload happens for exactly one class of edit: an export the
|
|
456
|
+
**server** already read to produce the page in front of you.
|
|
457
|
+
|
|
458
|
+
| Export | What the server does with it |
|
|
459
|
+
| --- | --- |
|
|
460
|
+
| `loader` | runs it (SSR / prerender), and the router caches the result per route + params |
|
|
461
|
+
| `renderMode`, `dynamic` | picks the render strategy for the route |
|
|
462
|
+
| `meta` | renders it into `<head>` |
|
|
463
|
+
| `getStaticPaths` | enumerates which paths get prerendered |
|
|
464
|
+
| `revalidate`, `staleWhileRevalidate` | sets the ISR cache window |
|
|
465
|
+
| `cacheInvalidatesOn` | wires the page into the ISR cache invalidator |
|
|
466
|
+
| `interactive` | decides how much client JS is shipped |
|
|
467
|
+
| `tenantAware` | forms part of the server-side cache key |
|
|
468
|
+
|
|
469
|
+
That reload is deliberate, not a gap. The HTML you are looking at was produced
|
|
470
|
+
from the OLD value, so hot-swapping the export would leave stale output on
|
|
471
|
+
screen with nothing to signal it. A reload re-runs SSR with the new value, and
|
|
472
|
+
the console line names the export and the server step that consumed it.
|
|
447
473
|
|
|
448
474
|
The mechanism, in case you hit an edge: React Fast Refresh only accepts a module
|
|
449
475
|
whose exports are all components, and a page exporting `loader` beside its
|
|
450
476
|
component fails that test. `voltro dev` registers each route module's
|
|
451
|
-
non-component exports with the React plugin's ignore hook
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
477
|
+
non-component exports with the React plugin's ignore hook (so Fast Refresh
|
|
478
|
+
judges only the components), then makes the reload call itself by comparing the
|
|
479
|
+
server-read exports' VALUES across the update — a function by its source text,
|
|
480
|
+
anything else by its JSON form — so a JSX-only edit, which recreates the
|
|
481
|
+
`loader` function object, is correctly read as "unchanged".
|
|
455
482
|
|
|
456
|
-
One
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
again; the sibling's own edits still propagate through the page's boundary.
|
|
483
|
+
One residual caveat: **adding or removing** a non-component export still
|
|
484
|
+
reloads once, whatever it is. Fast Refresh sees an export that was not on the
|
|
485
|
+
ignore list yet and refuses the boundary; the next edit to that page hot-updates
|
|
486
|
+
normally.
|
|
461
487
|
|
|
462
488
|
### The in-page devtools overlay
|
|
463
489
|
|
|
@@ -515,6 +541,7 @@ A `<DevtoolsStringsProvider strings={…}>` mounted above the overlay works too;
|
|
|
515
541
|
| `PORT=4001` | Override the listen port (api or web). Both kinds read `PORT`, then `app.config.ts` `port:`, then the default. |
|
|
516
542
|
| `VOLTRO_DASHBOARD_PORT=5180` | Override the auto-launched dashboard port (default `5179`). |
|
|
517
543
|
| `VOLTRO_LOG_LEVEL=debug` | Verbose framework logs. |
|
|
544
|
+
| `VOLTRO_DEV_SSR_COMPILE_CONCURRENCY=4` | Max `ssr` pages compiled on demand at once (default `4`). Lower it (`1`/`2`) on a low-memory box if a burst of first-time `ssr` page loads spikes memory; raise it on a big machine. Warm (already-compiled) pages are never throttled. |
|
|
518
545
|
|
|
519
546
|
## Multi-app dev (turbo)
|
|
520
547
|
|
|
@@ -810,6 +837,16 @@ It covers both halves of the stack:
|
|
|
810
837
|
| client | `useMemo` fanning in several subscriptions | `useDerived` |
|
|
811
838
|
| client | a local Next.js compat shim | the native `@voltro/web` exports |
|
|
812
839
|
| client | a hand-rolled presence heartbeat | `@voltro/plugin-presence` |
|
|
840
|
+
| client | `data === undefined` / `!data` on a subscription result | branch on `loading` (and `idle`, if you pass `skip`) |
|
|
841
|
+
|
|
842
|
+
The subscription rule **resolves the binding** rather than matching text, and
|
|
843
|
+
that distinction is the reason this scanner parses at all. A consumer migrating
|
|
844
|
+
these call sites wrote a regex codemod for the same job, and it rewrote a
|
|
845
|
+
`summary === undefined` check inside a child component where `summary` was a
|
|
846
|
+
PROP. Their compiler happened to catch it, because that name was out of scope
|
|
847
|
+
there; had the names matched, a silent behaviour change would have shipped. Text
|
|
848
|
+
cannot tell you which declaration an identifier refers to — so a rule about
|
|
849
|
+
identifiers has no business being written in text.
|
|
813
850
|
|
|
814
851
|
The rules are deliberately conservative — a detector that cries wolf trains you
|
|
815
852
|
to ignore it. A column that already carries `.encrypted()`, or a handler that
|
|
@@ -873,13 +910,52 @@ voltro doctor . --json # the complete scan: every file path, machine-re
|
|
|
873
910
|
"use": ".one() — fails with the typed NoRowFound on zero rows AND on more than one",
|
|
874
911
|
"files": ["queries/team.get.ts", "queries/user.get.ts", "…"]
|
|
875
912
|
}
|
|
876
|
-
]
|
|
913
|
+
],
|
|
914
|
+
"spaCandidates": []
|
|
877
915
|
}
|
|
878
916
|
```
|
|
879
917
|
|
|
880
918
|
That is the form to hand an agent, or to pipe into a script that works the list
|
|
881
919
|
file by file.
|
|
882
920
|
|
|
921
|
+
### `renderMode:'spa'` candidates
|
|
922
|
+
|
|
923
|
+
`voltro doctor` also flags web pages that could adopt `renderMode: 'spa'` without
|
|
924
|
+
losing their server-rendered shell. A page under a layout renders that LAYOUT
|
|
925
|
+
chain on the server — nav, sidebar, auth gate, via the layout's own loader — even
|
|
926
|
+
when the page itself is `'spa'`. So a page whose BODY needs no SSR can skip its
|
|
927
|
+
per-page SSR compile while the shell still server-renders. Like the hand-roll
|
|
928
|
+
detector, this is **advisory and never blocking**.
|
|
929
|
+
|
|
930
|
+
A page is listed when ALL of these hold:
|
|
931
|
+
|
|
932
|
+
- it is a **page file** — not `layout.tsx` / `loading.tsx` / `error.tsx` /
|
|
933
|
+
`not-found.tsx`;
|
|
934
|
+
- it exports **no `loader`** (so `'spa'` loses nothing the page contributed
|
|
935
|
+
server-side);
|
|
936
|
+
- its `renderMode` is **`'ssr'` or unset/default** — not a page that already
|
|
937
|
+
opted into a non-SSR render (`'spa'` / `'static'` / `'isr'`, or any other
|
|
938
|
+
explicit mode);
|
|
939
|
+
- a **`layout.tsx` sits somewhere in its directory chain** — root, an ancestor, or
|
|
940
|
+
the page's own dir. This is the load-bearing condition: only then does a layout
|
|
941
|
+
still SSR the shell. A page with no layout would, as `'spa'`, ship no server
|
|
942
|
+
HTML at all — so it is never flagged.
|
|
943
|
+
|
|
944
|
+
```text
|
|
945
|
+
• renderMode:'spa' candidates (2 pages — loader-free, under a layout, currently ssr/default):
|
|
946
|
+
src/pages/dashboard/index.tsx (/dashboard) — default renderMode
|
|
947
|
+
src/pages/admin/settings.tsx (/admin/settings) — renderMode:'ssr'
|
|
948
|
+
→ renderMode:'spa' skips this page's SSR compile while its layout shell still renders server-side — adopt it if the page BODY does not need SSR (internal/authenticated pages); keep 'ssr' if the page content needs SEO or server first-paint.
|
|
949
|
+
```
|
|
950
|
+
|
|
951
|
+
Adopt `'spa'` for internal or authenticated pages whose content needs no SEO or
|
|
952
|
+
server first-paint; keep `'ssr'` (or the `'static'` default) when it does. There
|
|
953
|
+
is deliberately **no codemod** to flip pages automatically — dropping a page
|
|
954
|
+
body's server render is a per-page product decision, not a mechanically-safe
|
|
955
|
+
transform. Every candidate (with its `file`, `pattern`, and `currentMode`) is
|
|
956
|
+
also in `voltro doctor --json` under a `spaCandidates` array; the human view
|
|
957
|
+
above caps at ten pages and points to `--json` for the rest.
|
|
958
|
+
|
|
883
959
|
## `voltro capabilities` — what the framework actually exports
|
|
884
960
|
|
|
885
961
|
Asked "what does this framework export", a language model will produce a
|
|
@@ -994,6 +994,51 @@ try {
|
|
|
994
994
|
}
|
|
995
995
|
```
|
|
996
996
|
|
|
997
|
+
### Multi-step sequences — `useSequence`
|
|
998
|
+
|
|
999
|
+
The rule above says a loop keeps its `try/catch`, and that left multi-step
|
|
1000
|
+
writes as the only verbose thing on the write path — while being the hardest
|
|
1001
|
+
case, not the easiest. `useSequence` runs the steps in order, gives the whole
|
|
1002
|
+
sequence **one** `onError`, and lets each step say how to undo itself:
|
|
1003
|
+
|
|
1004
|
+
```tsx
|
|
1005
|
+
const seq = useSequence({ onError: (e) => toast.error(readError(e)) })
|
|
1006
|
+
|
|
1007
|
+
const result = await seq.run(
|
|
1008
|
+
sequence()
|
|
1009
|
+
.step('upload', () => upload.run({ file }), {
|
|
1010
|
+
undo: (created) => removeObject.run({ id: created.id }),
|
|
1011
|
+
})
|
|
1012
|
+
.step('attach', (c) => createAttachment.run({ refId: c.upload.id })),
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
if (result.ok) setAttachmentId(result.data.attach.id)
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
Each step's context is typed and accumulates, so a fourth step can read the
|
|
1019
|
+
first step's result by name. `run` resolves with a discriminated result rather
|
|
1020
|
+
than rejecting — same "handled" semantics as `useMutation`/`useAction`.
|
|
1021
|
+
|
|
1022
|
+
**`undo` receives its own step's result**, which is the point: the id you just
|
|
1023
|
+
created is what you need to delete it again. Undos run in reverse for the steps
|
|
1024
|
+
that already succeeded.
|
|
1025
|
+
|
|
1026
|
+
**This is not a transaction, and the difference matters.** After `createTicket`
|
|
1027
|
+
returns, the ticket exists in Jira; nothing the browser does un-creates it, it
|
|
1028
|
+
can only issue a delete and hope. And the compensation runs *in the tab* — close
|
|
1029
|
+
it, lose the network, or navigate away mid-rollback and the remaining undos
|
|
1030
|
+
never happen. Two rules follow, and the primitive enforces both:
|
|
1031
|
+
|
|
1032
|
+
- the step that **failed** is never compensated (it may or may not have had an
|
|
1033
|
+
effect — undoing it is a guess, and a wrong guess deletes something else);
|
|
1034
|
+
- a failing `undo` never replaces the original error, and never stops the
|
|
1035
|
+
remaining undos. Cleanup failures come back in `compensationFailures`, so an
|
|
1036
|
+
orphan is something you can see rather than something you find later.
|
|
1037
|
+
|
|
1038
|
+
**When the rollback has to survive a closed tab, this is the wrong tool.** Put
|
|
1039
|
+
the sequence in a [workflow](/docs/workflows/overview): the engine owns retries
|
|
1040
|
+
and compensation there, and a crash resumes instead of leaking.
|
|
1041
|
+
|
|
997
1042
|
## Action vs Mutation vs Stream
|
|
998
1043
|
|
|
999
1044
|
| Need | Use |
|
|
@@ -1118,6 +1163,40 @@ const { data } = useSubscription(
|
|
|
1118
1163
|
|
|
1119
1164
|
While skipped, no WebSocket subscription opens and `data` stays `undefined`.
|
|
1120
1165
|
|
|
1166
|
+
### Skipped is `idle`, not `loading`
|
|
1167
|
+
|
|
1168
|
+
A skipped subscription reports **`idle: true`, `loading: false`**. The two are
|
|
1169
|
+
different questions — "waiting for the first snapshot" and "not asking at all" —
|
|
1170
|
+
and conflating them breaks the pattern this hook otherwise blesses:
|
|
1171
|
+
|
|
1172
|
+
```tsx
|
|
1173
|
+
// WRONG on a skipping call site — renders a skeleton for a query you switched off
|
|
1174
|
+
if (loading) return <Skeleton/>
|
|
1175
|
+
```
|
|
1176
|
+
|
|
1177
|
+
Passing a dynamic `skip` therefore changes the return type: you get a third
|
|
1178
|
+
state, and TypeScript will not let you ignore it.
|
|
1179
|
+
|
|
1180
|
+
```tsx
|
|
1181
|
+
const s = useSubscription<Team[]>('app', 'teams.list', {}, { skip: !open })
|
|
1182
|
+
|
|
1183
|
+
if (s.idle) return null // deliberately not asking
|
|
1184
|
+
if (s.loading) return <Skeleton/> // asking, no answer yet
|
|
1185
|
+
return <TeamsTable teams={s.data}/> // `data` is Team[] — narrowed
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1188
|
+
Call sites that never skip are untouched — `if (loading)` still proves `data` is
|
|
1189
|
+
present there, and a literal `{ skip: false }` counts as never skipping. The
|
|
1190
|
+
third state exists only where "not asking" is a real outcome.
|
|
1191
|
+
|
|
1192
|
+
With a `fallback` there is nothing to narrow either way: `data` is always
|
|
1193
|
+
present, and `idle` tells you whether what is on screen is the fallback because
|
|
1194
|
+
you chose not to ask.
|
|
1195
|
+
|
|
1196
|
+
A subscription that was live and is then skipped goes idle — it does **not**
|
|
1197
|
+
keep serving the snapshot it still holds. Otherwise `skip: !open` would show
|
|
1198
|
+
last time's data the moment a dialog reopens.
|
|
1199
|
+
|
|
1121
1200
|
## Streams Are Different
|
|
1122
1201
|
|
|
1123
1202
|
For non-database or transient element feeds, use [streams](/docs/data/streams), not subscriptions:
|
|
@@ -1794,6 +1873,35 @@ export default defineSubscriber({
|
|
|
1794
1873
|
|
|
1795
1874
|
The default export must be a `defineSubscriber({...})` result. The file is identified by suffix (`*.subscribe.ts` / `*.subscribe.tsx`).
|
|
1796
1875
|
|
|
1876
|
+
## `ctx.store` — reading and writing back
|
|
1877
|
+
|
|
1878
|
+
`ctx` carries `log`, `id`, and **`store`** — the same store surface a handler
|
|
1879
|
+
uses, so a subscriber can act on the change it just saw:
|
|
1880
|
+
|
|
1881
|
+
```ts
|
|
1882
|
+
export default defineSubscriber({
|
|
1883
|
+
table: 'orders',
|
|
1884
|
+
on: 'insert',
|
|
1885
|
+
handler: async (event, ctx) => {
|
|
1886
|
+
await ctx.store.insert('order_audit', {
|
|
1887
|
+
orderId: String(event.new?.['id']),
|
|
1888
|
+
tenantId: String(event.new?.['tenantId']), // explicit — see below
|
|
1889
|
+
})
|
|
1890
|
+
},
|
|
1891
|
+
})
|
|
1892
|
+
```
|
|
1893
|
+
|
|
1894
|
+
**`ctx.store` is NOT tenant-scoped.** A subscriber fires from the change stream,
|
|
1895
|
+
not from a request, so there is no subject to scope to and no tenant to infer.
|
|
1896
|
+
Reads see every tenant's rows; a write to a `tenant()` table without an explicit
|
|
1897
|
+
`tenantId` fails with `TenantScopeViolation` rather than landing in an arbitrary
|
|
1898
|
+
tenant. When your reaction is per-tenant, take the tenant from the row that
|
|
1899
|
+
changed, as above. This is the same posture a schedule's `ctx.app.store` has —
|
|
1900
|
+
both are post-request system work.
|
|
1901
|
+
|
|
1902
|
+
Writes are mixin-stamped exactly as a request-path write is: id scheme,
|
|
1903
|
+
timestamps, audit columns. Only the subject differs.
|
|
1904
|
+
|
|
1797
1905
|
## What fires when
|
|
1798
1906
|
|
|
1799
1907
|
The framework binds to the store's `onChange` channel. Subscribers fire **after the transaction commits** — the row IS persisted when your handler runs. This means:
|
|
@@ -1818,7 +1926,7 @@ Subscribers are **non-durable** by design:
|
|
|
1818
1926
|
- **Async handlers are NOT awaited by the dispatcher.** Fire-and-forget — a slow handler can't back-pressure the change stream. Errors propagate to the structured log via `.catch()`, but the change-emission path returns immediately.
|
|
1819
1927
|
- **No retry, no resume.** If the process crashes mid-handler, the work is gone. Same if the network call inside the handler fails — there's no built-in retry policy.
|
|
1820
1928
|
|
|
1821
|
-
If you need any of those properties (transactional, durable, retried), don't hand-roll the kickoff in the handler — a subscriber's `ctx` carries
|
|
1929
|
+
If you need any of those properties (transactional, durable, retried), don't hand-roll the kickoff in the handler — a subscriber's `ctx` carries no workflow handle, deliberately. "A row changed → start a workflow" has its own primitive: a [reaction](/docs/data/reactions). A `*.reaction.tsx` watches the same post-commit change stream and its `act` starts the workflow for you, behind mandatory guards:
|
|
1822
1930
|
|
|
1823
1931
|
```tsx
|
|
1824
1932
|
// reactions/fulfillOrder.reaction.tsx
|
|
@@ -38,7 +38,11 @@ export default defineSeed({
|
|
|
38
38
|
|
|
39
39
|
Seeds are meant to be safe to re-run. The `upsertByUnique(table, matchFields, fullRow)` helper does "row matching `matchFields` exists? update it : insert it", returning `{ row, created }`. Use stable string ids (not random tokens) so subsequent runs match the same rows instead of duplicating.
|
|
40
40
|
|
|
41
|
-
The runner also fingerprints each seed by hashing its source. On a `boot`-lifecycle seed it only re-runs when the fingerprint changes — so an unchanged seed doesn't re-execute on every `voltro dev` restart.
|
|
41
|
+
The runner also fingerprints each seed by hashing its source. On a `boot`-lifecycle seed it only re-runs when the fingerprint changes — so an unchanged seed doesn't re-execute on every `voltro dev` restart. The record lives in `_voltro_seeds`, one row per seed.
|
|
42
|
+
|
|
43
|
+
Two things the skip deliberately does **not** do. A **failed** run is recorded as failed and never satisfies the skip, so a broken seed retries on the next boot instead of disabling itself permanently. And if the ledger cannot be read at all — unmigrated database, memory store, missing table — every boot seed **runs**: re-doing idempotent work costs time, whereas skipping data restoration on a database we could not inspect costs data.
|
|
44
|
+
|
|
45
|
+
Override the fingerprint when the seed depends on external state (env vars) that should force a re-run:
|
|
42
46
|
|
|
43
47
|
```ts
|
|
44
48
|
fingerprint: ({ src }) => `${src}:${process.env.SEED_VERSION ?? ''}`,
|
|
@@ -50,9 +54,21 @@ fingerprint: ({ src }) => `${src}:${process.env.SEED_VERSION ?? ''}`,
|
|
|
50
54
|
|---|---|---|
|
|
51
55
|
| `boot` | On every `voltro dev` boot, **only if the fingerprint changed** | — |
|
|
52
56
|
| `manual` | Only via `voltro db seed --id <name>` or the dashboard | — |
|
|
53
|
-
| `onTenantCreate` |
|
|
54
|
-
| `onSchemaChange` |
|
|
55
|
-
| `cron` |
|
|
57
|
+
| `onTenantCreate` | **Not wired yet** — declared and validated, never triggered | — |
|
|
58
|
+
| `onSchemaChange` | **Not wired yet** — declared and validated, never triggered | `watchedTables` |
|
|
59
|
+
| `cron` | **Not wired yet** — declared and validated, never triggered | `cron` |
|
|
60
|
+
|
|
61
|
+
Only `boot` and `manual` actually execute today. The other three lifecycles
|
|
62
|
+
validate at definition time (a `cron` seed without a `cron` field throws, an
|
|
63
|
+
`onSchemaChange` seed without `watchedTables` throws) and are registered so the
|
|
64
|
+
dashboard can list them — but nothing fires them. A seed declared with one of
|
|
65
|
+
them will never run, silently.
|
|
66
|
+
|
|
67
|
+
Until they are wired, express the same intent with a primitive that does run: a
|
|
68
|
+
`*.cron.tsx` schedule whose handler does the seeding, or an explicit `voltro db
|
|
69
|
+
seed --id <name>` from your migration/provisioning step. This table said
|
|
70
|
+
otherwise until 0.10.0, which is exactly the kind of promise that costs someone
|
|
71
|
+
a debugging afternoon — it is corrected here rather than quietly dropped.
|
|
56
72
|
|
|
57
73
|
```ts
|
|
58
74
|
defineSeed({
|
|
@@ -469,22 +469,25 @@ The generated `.framework/app.tsx` records this chain explicitly per route — y
|
|
|
469
469
|
<!-- source: en/routing/render-modes.md -->
|
|
470
470
|
## Render modes
|
|
471
471
|
|
|
472
|
-
_static (SSG) vs ssr vs isr — when each runs, what it caches, and how to pick._
|
|
472
|
+
_static (SSG) vs spa vs ssr vs isr — when each runs, what it caches, and how to pick._
|
|
473
473
|
|
|
474
|
-
Every page declares a `renderMode`. The mode controls **when** the HTML is produced — at build time, on every request,
|
|
474
|
+
Every page declares a `renderMode`. The mode controls **when** the HTML is produced — at build time, on every request, build-once-revalidate-occasionally, or not on the server at all.
|
|
475
475
|
|
|
476
476
|
```tsx
|
|
477
|
-
export const renderMode = 'static' as const // 'static' | 'ssr' | 'isr'
|
|
477
|
+
export const renderMode = 'static' as const // 'static' | 'spa' | 'ssr' | 'isr'
|
|
478
478
|
```
|
|
479
479
|
|
|
480
|
-
## The
|
|
480
|
+
## The four modes
|
|
481
481
|
|
|
482
482
|
| Mode | When HTML is produced | Cached? | Best for |
|
|
483
483
|
|---|---|---|---|
|
|
484
484
|
| `static` *(default)* | `voltro build` time | Forever | Marketing pages, docs, anything that doesn't change per-request |
|
|
485
|
+
| `spa` | Never for the page itself (its layout chain may still be server-rendered) | — | Reactive dashboards whose state lives in the browser |
|
|
485
486
|
| `ssr` | Every request | Never | Authenticated dashboards, search results, anything cookie-driven |
|
|
486
487
|
| `isr` | First request after build, then on revalidate | Per-key in-memory or Postgres | News feeds, listings, dashboards that change but not per-user |
|
|
487
488
|
|
|
489
|
+
Those four are the **complete** set. An unrecognised value is a hard error naming the page — see [What doesn't work](#what-doesnt-work).
|
|
490
|
+
|
|
488
491
|
## static (SSG)
|
|
489
492
|
|
|
490
493
|
```tsx
|
|
@@ -625,6 +628,35 @@ The framework reads Postgres logical replication; writes to `posts` or `comments
|
|
|
625
628
|
|
|
626
629
|
Requires `SSR_CACHE=postgres` and a `wal_level=logical` Postgres.
|
|
627
630
|
|
|
631
|
+
## spa (client-only, with an optional SSR layout shell)
|
|
632
|
+
|
|
633
|
+
```tsx
|
|
634
|
+
export const renderMode = 'spa' as const
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
A `spa` page renders entirely in the browser — the page itself is never server-rendered. Reach for it when a page genuinely needs a fresh client render every load (most reactive dashboards) and doesn't need its own first-paint HTML or SEO.
|
|
638
|
+
|
|
639
|
+
**If the page's route has a layout, that layout is still server-rendered.** The server renders the layout chain — running its layout loaders — around an empty page slot (`<div data-voltro-page-slot>`), inlines the layout data, and marks the page client-only. The browser hydrates that shell and mounts the page into the slot after hydration. So the shell (nav, sidebar, auth gate) gets an instant first paint while the page stays client-only. The page's own `loader` still runs in the browser.
|
|
640
|
+
|
|
641
|
+
Because a layout now runs on the server for spa routes too, a layout used **only** by spa pages must be SSR-safe — no unguarded `window` / `document` in its render or its `loader`. Layouts shared with any `static` / `ssr` / `isr` page already render server-side (and `static` is the default), so they are unaffected. A spa page with **no** layout is a pure client mount, unchanged.
|
|
642
|
+
|
|
643
|
+
**`voltro build` prerenders that shell to a file — but only when no layout in the page's chain exports a `loader`.** A layout loader may resolve per-visitor data (the signed-in user, a tenant), and freezing one render of it into a static file would serve the first visitor's data to everyone. So a chain with any layout loader is left to `voltro start`, which runs the loader per request; the build logs which route it skipped and why. A loader-less chain is request-independent by construction and is written to `dist/<route>/index.html`, so a static host paints the layout immediately instead of an empty `#root`.
|
|
644
|
+
|
|
645
|
+
**On a static host, that file is also the SPA fallback.** A static host answers every URL it has no file for with `index.html` — so once your ROOT route is prerendered, a deep link to `/reports` is served the root's document. The framework handles this: the inlined hydration payload records the pathname it was rendered for, and the client refuses to adopt markup that belongs to another route, falling back to a normal client render instead. Without that check React would hydrate the root's layout while rendering `/reports`, report a hydration mismatch, and silently re-render the whole tree.
|
|
646
|
+
|
|
647
|
+
Nothing to configure. Two things follow from it, though:
|
|
648
|
+
|
|
649
|
+
- Deep links into a static deployment are **client-rendered**, not hydrated. The visitor sees the app; they don't get the prerendered paint. If that matters for a route, give it `static` (or `isr`/`ssr` behind `voltro start`) so it has a file of its own.
|
|
650
|
+
- The pathname is compared *after* normalising a trailing slash, a trailing `/index.html`, and percent-encoding — the shapes a static host varies on. Your own routes are unaffected.
|
|
651
|
+
|
|
652
|
+
**A layout in that chain may use `defer()`.** The shell then STREAMS: the layout chain and the empty page slot flush immediately, and the deferred layout value arrives afterwards behind its `<Await>` boundary — the same mechanism an `ssr` page gets, applied to the shell. So a sidebar whose nav counts take 300ms no longer holds back the first paint of the rest of the shell. Nothing about the hydration seam changes: the first flush still carries the empty page slot, and the page still mounts into it after hydration.
|
|
653
|
+
|
|
654
|
+
What still cannot defer on this path:
|
|
655
|
+
|
|
656
|
+
- **A prerendered shell.** `voltro build` only prerenders a shell whose chain has no layout loader (see above), and `defer()` can only come from a loader — so the two never meet. If they did, the build would refuse by name rather than freeze the `<Await>` fallback into the file.
|
|
657
|
+
- **The page's own loader.** A `spa` page's loader runs in the *browser*, so there is no server render to stream into. Use `<Await>` on a client promise instead, or move the data into a layout loader.
|
|
658
|
+
- **`interactive: 'none'` / `'islands'`**, for the same reason as on an `ssr` page: revealing a streamed boundary needs React's inline reveal scripts, and neither mode ever hydrates the root.
|
|
659
|
+
|
|
628
660
|
## Picking between them
|
|
629
661
|
|
|
630
662
|
| You have… | Use |
|
|
@@ -663,12 +695,15 @@ A request to `/foo`:
|
|
|
663
695
|
1. **Pre-rendered HTML exists at `dist/foo/index.html`?** Serve it. (static + isr-already-cached.)
|
|
664
696
|
2. **No pre-render, page is `ssr`?** Render fresh, serve.
|
|
665
697
|
3. **No pre-render, page is `isr`?** Cache lookup → MISS → render → store → serve.
|
|
666
|
-
4. **No pre-render, page is `
|
|
698
|
+
4. **No pre-render, page is `spa` with a layout?** Render the SSR layout shell (layouts + an empty page slot) on demand; the client mounts the page into the slot.
|
|
699
|
+
5. **No pre-render, page is `spa` with no layout, or `static`?** Serve the SPA shell — the client router takes over.
|
|
667
700
|
|
|
668
701
|
That last case is how dynamic `static` routes work in dev / when `getStaticPaths` didn't include the URL.
|
|
669
702
|
|
|
670
703
|
## What doesn't work
|
|
671
704
|
|
|
705
|
+
- **Any value outside the four modes.** `renderMode` is a closed set — `'static' | 'spa' | 'ssr' | 'isr'`. Anything else (`'client'`, `'csr'`, a typo) fails the build and `voltro dev` at codegen, naming the page, the value and the valid set. There are no aliases: a page that renders only in the browser is `'spa'`.
|
|
706
|
+
- **Declaring `renderMode` on a `layout.tsx` / `error.tsx` / `loading.tsx`.** The mode is a property of the PAGE; the framework never reads one off a special file. Whether a layout renders on the server follows from the page's mode.
|
|
672
707
|
- **Switching `renderMode` per request.** It's a static module export — one value per build.
|
|
673
708
|
- **Assuming `ssr` is client-only in dev.** It is not: `voltro dev` runs the same SSR path `voltro start` does, streaming included, so cookie-driven gates and `useServerRequest()` behave the same in both. What dev does NOT do is pre-render `static` pages — those fall through to the SPA shell.
|
|
674
709
|
- **`isr` with `cacheInvalidatesOn` against memory cache.** Memory cache is per-process; CDC events fire across processes. Use `SSR_CACHE=postgres`.
|
|
@@ -763,12 +798,19 @@ loaders in the browser as usual. A `spa` page has no server render, so its
|
|
|
763
798
|
loader runs on the client on first mount.
|
|
764
799
|
|
|
765
800
|
Layout loaders are inlined the same way, keyed per layout, so each layout reads
|
|
766
|
-
its OWN data on the first render.
|
|
767
|
-
prerender
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
801
|
+
its OWN data on the first render. This includes `voltro build`'s static
|
|
802
|
+
prerender: it runs the page loader **and** every layout loader in the chain at
|
|
803
|
+
build time, so a CMS-backed nav or footer is baked into the prerendered file and
|
|
804
|
+
the layout loader does not re-run after hydration. Layout loaders see the same
|
|
805
|
+
build-time context the page loader does — `params`, `pathname`, `signal` — and
|
|
806
|
+
nothing request-shaped: there is no `headers` and no `query` at build time. A
|
|
807
|
+
layout loader that needs either belongs on an `ssr` page.
|
|
808
|
+
|
|
809
|
+
If a layout loader throws during the build, the page is still prerendered — with
|
|
810
|
+
no layout data, and a warning in the build log. The layout then resolves its
|
|
811
|
+
data on the client after mount, and the PAGE keeps its own inlined data for the
|
|
812
|
+
whole of that window, so the no-guard promise above still holds: only the layout
|
|
813
|
+
shows its no-data fallback until its loader settles.
|
|
772
814
|
|
|
773
815
|
|
|
774
816
|
## Deferring slow data: `defer()` + `<Await>`
|
|
@@ -789,7 +831,7 @@ export const loader = async ({ query }: { query?: <T>(tag: string, input?: Recor
|
|
|
789
831
|
// EAGER — awaited before the shell renders. Keep this fast.
|
|
790
832
|
{ user: await query?.<User>('users.me') },
|
|
791
833
|
// DEFERRED — NOT awaited. Each becomes a promise on useLoaderData().
|
|
792
|
-
{ report: query!<
|
|
834
|
+
{ report: query!<QuarterlyReport>('reports.quarterly') },
|
|
793
835
|
)
|
|
794
836
|
|
|
795
837
|
export default function Dashboard() {
|
|
@@ -835,8 +877,12 @@ otherwise guarantees by construction.
|
|
|
835
877
|
A rejected deferred value never takes the page down: it renders
|
|
836
878
|
`errorFallback` in place, on the server and on the client alike.
|
|
837
879
|
|
|
838
|
-
### `defer()` requires
|
|
880
|
+
### `defer()` requires a streamed response and full interactivity
|
|
839
881
|
|
|
882
|
+
That means `renderMode: 'ssr'`, or — for a **layout** loader — the SSR layout
|
|
883
|
+
shell of a `renderMode: 'spa'` page under it, which `voltro dev` and
|
|
884
|
+
`voltro serve` also stream (see
|
|
885
|
+
[render modes](/docs/routing/render-modes#spa-client-only-with-an-optional-ssr-layout-shell)).
|
|
840
886
|
Every other combination is a **hard error at boot or build**, naming the page —
|
|
841
887
|
because each one fails silently otherwise:
|
|
842
888
|
|
|
@@ -846,8 +892,9 @@ because each one fails silently otherwise:
|
|
|
846
892
|
| `renderMode: 'isr'` | ISR caches a completed HTML string. Filling it in would make `defer()` a silent no-op that still reads like it streams. |
|
|
847
893
|
| `interactive: 'none'` | Revealing a streamed boundary needs React's inline reveal scripts, and this mode ships no JS. The fallback would be permanent. |
|
|
848
894
|
| `interactive: 'islands'` | The page's React root never hydrates, so nothing consumes the streamed value. |
|
|
895
|
+
| A **prerendered** spa layout shell | `voltro build` writes it to a file, which has no "after". Unreachable in practice — the build only prerenders a shell whose chain has no layout loader — but refused by name if it ever is reached. |
|
|
849
896
|
|
|
850
|
-
In all
|
|
897
|
+
In all of these the fix is the same: put the value in the eager bucket (or
|
|
851
898
|
return it directly) and let the page render as it did before.
|
|
852
899
|
|
|
853
900
|
### Layout loaders can defer too
|
|
@@ -856,6 +903,13 @@ A `layout.tsx` loader may return `defer()` under the same rules. Its deferred
|
|
|
856
903
|
fields are keyed per layout, so a layout reads its own promises via
|
|
857
904
|
`useLoaderData()` exactly as a page does.
|
|
858
905
|
|
|
906
|
+
This includes a layout that wraps a **client-only (`renderMode: 'spa'`) page**.
|
|
907
|
+
The server renders that route as a layout shell — the layout chain around an
|
|
908
|
+
empty page slot — and a deferring layout makes that shell stream: chain and slot
|
|
909
|
+
first, the deferred layout value afterwards. The page still mounts on the client
|
|
910
|
+
after hydration, unchanged. What a spa page's **own** loader cannot do is defer:
|
|
911
|
+
it runs in the browser, so there is no server render to stream into.
|
|
912
|
+
|
|
859
913
|
### Client-side navigation
|
|
860
914
|
|
|
861
915
|
On a client-side navigation there is no server render, so the loader runs in the
|
|
@@ -931,6 +985,15 @@ Two rules that fall out of this:
|
|
|
931
985
|
|
|
932
986
|
Under the hood, `ctx.query` is a one-shot `POST /rpc` call (see [Wire protocol](/docs/data/wire-protocol#http-one-shot-rpc-post-rpc)).
|
|
933
987
|
|
|
988
|
+
### Authentication in loaders
|
|
989
|
+
|
|
990
|
+
Auth is resolved by the **api**, never the web app. Two rules follow:
|
|
991
|
+
|
|
992
|
+
- **Strategies live on the `type:'api'` app.** A `type:'web'` app has no auth middleware, so `auth.strategies` in a web `app.config.ts` does nothing. Configure your IdP (`supabaseStrategy`, `workosStrategy`, the built-in password strategy, …) on the api's `app.config.ts`.
|
|
993
|
+
- **`ctx.query` forwards the request's cookie automatically.** A server-side loader's `ctx.query` sends the browser's `Cookie` header on the one-shot `POST /rpc`, so the api resolves the SAME Subject + tenant it would over the WebSocket. You never thread a token through by hand — a logged-in user's cookie-mode session (e.g. `@supabase/ssr`'s `sb-<ref>-auth-token`) is verified by the api's strategies, and tenant-scoped queries return that user's rows during SSR.
|
|
994
|
+
|
|
995
|
+
So a cookie-mode Supabase app configures `supabaseStrategy({ cookieName: 'sb-<ref>-auth-token' })` on the **api**; the web loader's `ctx.query` then authenticates for free. See [Supabase Auth](/docs/plugins/auth-supabase).
|
|
996
|
+
|
|
934
997
|
## Errors from loaders
|
|
935
998
|
|
|
936
999
|
If the loader throws, the framework:
|
|
@@ -224,6 +224,38 @@ Use `scheduledAt`, not `Date.now()`, for any time-bucketed query (the "prune ses
|
|
|
224
224
|
|
|
225
225
|
`ctx.trigger === 'manual'` distinguishes a dashboard **Run now** click from a clock firing — handy when a manual run should skip a guard (e.g. "only on weekdays") that the scheduled path enforces.
|
|
226
226
|
|
|
227
|
+
## A schedule runs as the SYSTEM subject — no tenant
|
|
228
|
+
|
|
229
|
+
`ctx.app.store` is **not tenant-scoped**. A schedule has no request, so it has
|
|
230
|
+
no signed-in user and no tenant to infer, and the framework refuses to pick one
|
|
231
|
+
for you. Reads see every tenant's rows.
|
|
232
|
+
|
|
233
|
+
That is the right default for what crons usually are — a backfill, a reconcile,
|
|
234
|
+
a GC sweep — but it means a **per-tenant** cron has to say which tenant it means:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
handler: async (ctx) => {
|
|
238
|
+
const tenants = await ctx.app.store.select('tenants').all()
|
|
239
|
+
for (const t of tenants) {
|
|
240
|
+
const stale = await ctx.app.store.select('sessions')
|
|
241
|
+
.where('tenantId', t.id) // explicit, not inferred
|
|
242
|
+
.where('expiresAt', '<', ctx.scheduledAt)
|
|
243
|
+
.all()
|
|
244
|
+
// …
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Writes to a `tenant()` table need the same treatment: pass `tenantId`
|
|
250
|
+
explicitly, or the write fails with `TenantScopeViolation` rather than landing
|
|
251
|
+
somewhere arbitrary.
|
|
252
|
+
|
|
253
|
+
This behaves identically under `voltro dev` and `voltro serve`. It did not
|
|
254
|
+
always — before 0.10.0, dev scoped schedules to `$TENANT` (default `acme`)
|
|
255
|
+
while production ran them unscoped, so the same cron read one tenant in
|
|
256
|
+
development and all of them in production. If you added `.unscoped()` to a cron
|
|
257
|
+
to work around that, it is now a no-op and can go.
|
|
258
|
+
|
|
227
259
|
## Discovery
|
|
228
260
|
|
|
229
261
|
`voltro dev` and `voltro build` glob `**/*.cron.{ts,tsx}` under your api app. Each discovered schedule is logged at boot:
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@effect/platform": "^0.96.1",
|
|
13
13
|
"@effect/rpc": "^0.75.1",
|
|
14
|
-
"@voltro/ai": "0.
|
|
15
|
-
"@voltro/cli": "0.
|
|
16
|
-
"@voltro/database": "0.
|
|
17
|
-
"@voltro/env": "0.
|
|
18
|
-
"@voltro/protocol": "0.
|
|
19
|
-
"@voltro/runtime": "0.
|
|
14
|
+
"@voltro/ai": "0.10.0",
|
|
15
|
+
"@voltro/cli": "0.10.0",
|
|
16
|
+
"@voltro/database": "0.10.0",
|
|
17
|
+
"@voltro/env": "0.10.0",
|
|
18
|
+
"@voltro/protocol": "0.10.0",
|
|
19
|
+
"@voltro/runtime": "0.10.0",
|
|
20
20
|
"effect": "^3.21.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@voltro/testing": "0.
|
|
23
|
+
"@voltro/testing": "0.10.0",
|
|
24
24
|
"typescript": "^5.7.0",
|
|
25
25
|
"vitest": "^3.0.0"
|
|
26
26
|
}
|