@voltro/cli 0.29.0 → 0.30.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 +377 -0
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/dist/{apiBuild-BESZbTjA.js → apiBuild-BceikpQf.js} +2 -2
- package/dist/apiBuild-CxvR1Dwt.js +2 -0
- package/dist/bin.js +3 -3
- package/dist/{checkCommand-BPAtyWs8.js → checkCommand-BMr-X48w.js} +23 -16
- package/dist/checkCommand-BOk9y264.js +2 -0
- package/dist/{commands-82BDEktg.js → commands-D4Y1-Nlu.js} +1936 -1339
- package/dist/{dbCommand-bCBbk6tz.js → dbCommand-C7hbl5pk.js} +344 -265
- package/dist/dbCommand-D8nhyppR.js +2 -0
- package/dist/{dev-DeEJj5T4.js → dev-Cr-teYsz.js} +1 -1
- package/dist/{dev-Cg4BKLmi.js → dev-DmnkH-wr.js} +2660 -1742
- package/dist/{frameworkTableAssembly-4Db40V23.js → frameworkTableAssembly-BzAajuUv.js} +114 -98
- package/dist/frameworkTableAssembly-DU0aOxjL.js +2 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +1 -1
- package/dist/{inspect-BMvQpwqy.js → inspect-BUUjt773.js} +150 -14
- package/dist/inspect-mmBuRXmy.js +2 -0
- package/dist/{inspectMetrics-DHYygE4s.js → inspectMetrics-BeJdP_3b.js} +564 -512
- package/dist/{manifestBuild-C4R1EdHn.js → manifestBuild-BLrVuSlM.js} +1 -1
- package/dist/manifestBuild-Dj8Jjoto.js +2 -0
- package/dist/{sdkgen-CW5NbYF0.js → sdkgen-CqpOq_fy.js} +1 -1
- package/dist/serveCommand-Ciq_wIY8.js +1577 -0
- package/dist/serveEntry.js +2 -2
- package/dist/{start-CsCIaW4-.js → start-ocqB1seX.js} +19 -19
- package/dist/startEntry.js +2 -2
- package/package.json +17 -17
- package/templates/AGENTS.core.md +50 -0
- package/templates/AGENTS.md +53 -2
- package/templates/agent-docs/_index.md +3 -2
- package/templates/agent-docs/_manifest.json +16 -4
- package/templates/agent-docs/ai.md +56 -0
- package/templates/agent-docs/cli.md +19 -0
- package/templates/agent-docs/internationalization.md +8 -7
- package/templates/agent-docs/local-first-mobile.md +18 -6
- package/templates/agent-docs/routing.md +1 -1
- package/templates/agent-docs/security.md +17 -0
- package/templates/agent-docs/templates/apibackends.md +209 -0
- package/templates/agent-docs/templates/appshells.md +144 -1
- package/templates/agent-docs/templates/mobile.md +60 -0
- package/templates/agent-docs/templates/overview.md +17 -7
- package/templates/agent-docs/whats-new.md +256 -157
- package/templates/agent-docs/workflows.md +365 -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-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +10 -10
- package/templates/apps/api-collab/README.md +70 -0
- package/templates/apps/api-collab/app.config.ts +28 -0
- package/templates/apps/api-collab/database/schema.ts +63 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.server.ts +19 -0
- package/templates/apps/api-collab/mutations/documents.create.mutation.ts +36 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.server.ts +20 -0
- package/templates/apps/api-collab/mutations/documents.setBody.mutation.ts +30 -0
- package/templates/apps/api-collab/package.json +30 -0
- package/templates/apps/api-collab/queries/documents.query.server.ts +16 -0
- package/templates/apps/api-collab/queries/documents.query.ts +23 -0
- package/templates/apps/api-collab/template.json +6 -0
- package/templates/apps/api-collab/tests/documents.setBody.test.ts +89 -0
- package/templates/apps/api-collab/tsconfig.json +5 -0
- 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-saas/package.json +11 -11
- 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-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/app.config.ts +1 -1
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-admin/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/app.config.ts +1 -1
- package/templates/apps/frontend-app/package.json +8 -8
- package/templates/apps/frontend-app/src/locales/index.ts +1 -1
- package/templates/apps/frontend-app/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-app/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-auth/src/locales/index.ts +1 -1
- package/templates/apps/frontend-blank/app.config.ts +1 -1
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-blank/src/locales/index.ts +1 -1
- package/templates/apps/frontend-blank/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-blank/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-cms/src/locales/index.ts +1 -1
- package/templates/apps/frontend-collab/README.md +80 -0
- package/templates/apps/frontend-collab/app.config.ts +38 -0
- package/templates/apps/frontend-collab/package.json +33 -0
- package/templates/apps/frontend-collab/src/locales/de.ts +29 -0
- package/templates/apps/frontend-collab/src/locales/en.ts +31 -0
- package/templates/apps/frontend-collab/src/locales/index.ts +15 -0
- package/templates/apps/frontend-collab/src/pages/layout.tsx +39 -0
- package/templates/apps/frontend-collab/src/pages/page.test.tsx +150 -0
- package/templates/apps/frontend-collab/src/pages/page.tsx +186 -0
- package/templates/apps/frontend-collab/template.json +6 -0
- package/templates/apps/frontend-collab/tsconfig.json +11 -0
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/app.config.ts +1 -1
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-dashboard/src/locales/index.ts +1 -1
- 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-portal/package.json +8 -8
- package/templates/apps/frontend-portal/src/locales/index.ts +1 -1
- package/templates/apps/frontend-saas/README.md +1 -1
- package/templates/apps/frontend-saas/app.config.ts +1 -1
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-saas/src/locales/index.ts +1 -1
- package/templates/apps/frontend-spa/app.config.ts +1 -1
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-spa/src/locales/index.ts +1 -1
- package/templates/apps/frontend-spa/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-spa/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr/app.config.ts +1 -1
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-ssr/src/pages/page.tsx +1 -1
- package/templates/apps/frontend-ssr-api/app.config.ts +1 -1
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-ssr-api/src/locales/index.ts +1 -1
- package/templates/apps/frontend-ssr-api/src/pages/layout.tsx +1 -1
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/frontend-status/src/locales/index.ts +1 -1
- package/templates/apps/mobile-app/README.md +58 -0
- package/templates/apps/mobile-app/app.config.ts +33 -0
- package/templates/apps/mobile-app/babel.config.js +5 -0
- package/templates/apps/mobile-app/metro.config.js +20 -0
- package/templates/apps/mobile-app/package.json +33 -0
- package/templates/apps/mobile-app/src/app/_layout.tsx +61 -0
- package/templates/apps/mobile-app/src/app/index.tsx +62 -0
- package/templates/apps/mobile-app/src/app/orders/[id].tsx +19 -0
- package/templates/apps/mobile-app/src/app/settings.tsx +51 -0
- package/templates/apps/mobile-app/src/client.ts +74 -0
- package/templates/apps/mobile-app/src/lib/deeplinks.ts +39 -0
- package/templates/apps/mobile-app/src/lib/notifications.ts +31 -0
- package/templates/apps/mobile-app/src/lib/sync.ts +29 -0
- package/templates/apps/mobile-app/src/persistence.ts +38 -0
- package/templates/apps/mobile-app/template.json +6 -0
- package/templates/apps/mobile-app/tests/deeplinks.test.ts +32 -0
- package/templates/apps/mobile-app/tests/notifications.test.ts +26 -0
- package/templates/apps/mobile-app/tests/sync.test.ts +34 -0
- package/templates/apps/mobile-app/tsconfig.json +10 -0
- package/templates/apps/mobile-app/voltro.mobile.ts +11 -0
- package/templates/patches/@effect__cluster@0.60.0.patch +6 -6
- package/dist/apiBuild-C8d74T6g.js +0 -2
- package/dist/checkCommand-DBvZvCh1.js +0 -2
- package/dist/dbCommand-CaIVTp-a.js +0 -2
- package/dist/frameworkTableAssembly-BTNirAAk.js +0 -2
- package/dist/inspect-DHeaMZak.js +0 -2
- package/dist/manifestBuild-Ck_90gZy.js +0 -2
- package/dist/serveCommand-BXl8mhZD.js +0 -1489
package/dist/serveEntry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { et as e } from "./inspectMetrics-
|
|
1
|
+
import { et as e } from "./inspectMetrics-BeJdP_3b.js";
|
|
2
2
|
import { c as t } from "./seedRunner-Bqxgp7HZ.js";
|
|
3
3
|
import { r as n } from "./appModuleLoader-C9r9mxZt.js";
|
|
4
|
-
import { t as r } from "./serveCommand-
|
|
4
|
+
import { t as r } from "./serveCommand-Ciq_wIY8.js";
|
|
5
5
|
export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { B as e, E as t, G as n, I as r, K as i, N as a, O as o, Q as s, S as c, T as l, U as u, W as ee, Z as d, _ as te, _t as f, a as p, at as m, b as h, c as g, f as _, ft as v, g as y, gt as b, h as x, ht as ne, i as re, it as ie, lt as ae, m as S, mt as C, nt as oe, o as se, ot as w, p as T, q as E, r as D, rt as O, s as k, t as A, v as j, x as ce, y as M } from "./inspectMetrics-
|
|
2
|
-
import {
|
|
1
|
+
import { B as e, E as t, G as n, I as r, K as i, N as a, O as o, Q as s, S as c, T as l, U as u, W as ee, Z as d, _ as te, _t as f, a as p, at as m, b as h, c as g, f as _, ft as v, g as y, gt as b, h as x, ht as ne, i as re, it as ie, lt as ae, m as S, mt as C, nt as oe, o as se, ot as w, p as T, q as E, r as D, rt as O, s as k, t as A, v as j, x as ce, y as M } from "./inspectMetrics-BeJdP_3b.js";
|
|
2
|
+
import { A as le, E as N, O as P, a as ue, k as F, m as de } from "./inspect-BUUjt773.js";
|
|
3
3
|
import { t as fe } from "./bootTiming-BdyP9nYw.js";
|
|
4
4
|
import { dirname as I, extname as pe, join as L, resolve as R } from "node:path";
|
|
5
5
|
import { fileURLToPath as z, pathToFileURL as B } from "node:url";
|
|
@@ -283,7 +283,7 @@ CREATE INDEX IF NOT EXISTS ${e}_servable_until_idx ON ${e} (servable_until);
|
|
|
283
283
|
locale: E.locale
|
|
284
284
|
} : {}
|
|
285
285
|
}
|
|
286
|
-
}), t = E?.locale ?? T.cookies["voltro:
|
|
286
|
+
}), t = E?.locale ?? T.cookies["voltro:locale"] ?? "en", r = (t) => ce({
|
|
287
287
|
helpers: C,
|
|
288
288
|
prepared: e,
|
|
289
289
|
meta: t,
|
|
@@ -349,7 +349,7 @@ CREATE INDEX IF NOT EXISTS ${e}_servable_until_idx ON ${e} (servable_until);
|
|
|
349
349
|
C.renderRouterStateScript(O.stateInput)
|
|
350
350
|
].filter((e) => e.length > 0).join("\n ");
|
|
351
351
|
if (g === "ssr" && D === "full" && e.buffered !== !0) {
|
|
352
|
-
let e = E?.locale ?? T.cookies["voltro:
|
|
352
|
+
let e = E?.locale ?? T.cookies["voltro:locale"] ?? "en", t = x({ shell: s });
|
|
353
353
|
return {
|
|
354
354
|
renderMode: "ssr",
|
|
355
355
|
stream: (n) => y({
|
|
@@ -371,13 +371,13 @@ CREATE INDEX IF NOT EXISTS ${e}_servable_until_idx ON ${e} (servable_until);
|
|
|
371
371
|
})
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
|
-
let { html: le, meta:
|
|
375
|
-
D === "none" && (
|
|
376
|
-
let
|
|
374
|
+
let { html: le, meta: N } = C.renderPageToHtml(k), P = A(N), ue = E?.locale ?? T.cookies["voltro:locale"] ?? "en", F = s.replace("<div id=\"root\"></div>", `<div id="root">${le}</div>`).replace("</head>", ` ${P}\n </head>`).replace(/<html(\s+lang="[^"]*")?/i, `<html lang="${ue}"`);
|
|
375
|
+
D === "none" && (F = Se(F));
|
|
376
|
+
let de = g === "isr" ? w(m.revalidate) : null;
|
|
377
377
|
return {
|
|
378
|
-
html:
|
|
378
|
+
html: F,
|
|
379
379
|
renderMode: g,
|
|
380
|
-
revalidateMs:
|
|
380
|
+
revalidateMs: de
|
|
381
381
|
};
|
|
382
382
|
}, $ = (e) => e === "" ? "/" : e, De = async (e) => {
|
|
383
383
|
let t = await import(B(e).href);
|
|
@@ -651,13 +651,13 @@ CREATE INDEX IF NOT EXISTS ${e}_servable_until_idx ON ${e} (servable_until);
|
|
|
651
651
|
status: r.status,
|
|
652
652
|
headers: {
|
|
653
653
|
"content-type": r.contentType,
|
|
654
|
-
...
|
|
654
|
+
...N(n.origin)
|
|
655
655
|
},
|
|
656
656
|
body: r.body
|
|
657
657
|
};
|
|
658
658
|
}
|
|
659
659
|
if (i !== "/_voltro/inspect/stream") {
|
|
660
|
-
let e =
|
|
660
|
+
let e = de(t, Ne, n);
|
|
661
661
|
if (e !== null) return {
|
|
662
662
|
status: e.status,
|
|
663
663
|
headers: {
|
|
@@ -859,7 +859,7 @@ CREATE INDEX IF NOT EXISTS ${e}_servable_until_idx ON ${e} (servable_until);
|
|
|
859
859
|
return;
|
|
860
860
|
}
|
|
861
861
|
if (s === "OPTIONS" && o.startsWith("/_voltro/inspect")) {
|
|
862
|
-
let e =
|
|
862
|
+
let e = N(a.origin);
|
|
863
863
|
t.writeHead(204, e), t.end();
|
|
864
864
|
return;
|
|
865
865
|
}
|
|
@@ -869,13 +869,13 @@ CREATE INDEX IF NOT EXISTS ${e}_servable_until_idx ON ${e} (servable_until);
|
|
|
869
869
|
t.writeHead(400, { "content-type": "text/plain" }), t.end("missing ?target=");
|
|
870
870
|
return;
|
|
871
871
|
}
|
|
872
|
-
if (!
|
|
872
|
+
if (!P(n)) {
|
|
873
873
|
t.writeHead(400, { "content-type": "text/plain" }), t.end("target not allowed: must be loopback or an origin configured in VOLTRO_DASHBOARD_APPS");
|
|
874
874
|
return;
|
|
875
875
|
}
|
|
876
876
|
try {
|
|
877
877
|
let r = { accept: a.accept ?? "application/json" }, i = a.authorization;
|
|
878
|
-
i ? r.authorization = Array.isArray(i) ? i[0] : i :
|
|
878
|
+
i ? r.authorization = Array.isArray(i) ? i[0] : i : le(n) && process.env.VOLTRO_INSPECT_TOKEN && (r.authorization = `Bearer ${process.env.VOLTRO_INSPECT_TOKEN}`);
|
|
879
879
|
let o;
|
|
880
880
|
if (s && s !== "GET" && s !== "HEAD") {
|
|
881
881
|
let t = [];
|
|
@@ -905,14 +905,14 @@ CREATE INDEX IF NOT EXISTS ${e}_servable_until_idx ON ${e} (servable_until);
|
|
|
905
905
|
t.writeHead(400, { "content-type": "text/plain" }), t.end("missing ?target=");
|
|
906
906
|
return;
|
|
907
907
|
}
|
|
908
|
-
if (!
|
|
908
|
+
if (!P(n)) {
|
|
909
909
|
t.writeHead(400, { "content-type": "text/plain" }), t.end("target not allowed: must be loopback or an origin configured in VOLTRO_DASHBOARD_APPS");
|
|
910
910
|
return;
|
|
911
911
|
}
|
|
912
912
|
let i = new AbortController();
|
|
913
913
|
e.on("close", () => i.abort()), t.on("close", () => i.abort());
|
|
914
914
|
try {
|
|
915
|
-
let e = a.authorization ?? (
|
|
915
|
+
let e = a.authorization ?? (le(n) && process.env.VOLTRO_INSPECT_TOKEN ? `Bearer ${process.env.VOLTRO_INSPECT_TOKEN}` : void 0), r = await fetch(n, {
|
|
916
916
|
redirect: "manual",
|
|
917
917
|
method: "GET",
|
|
918
918
|
headers: {
|
|
@@ -947,15 +947,15 @@ CREATE INDEX IF NOT EXISTS ${e}_servable_until_idx ON ${e} (servable_until);
|
|
|
947
947
|
return;
|
|
948
948
|
}
|
|
949
949
|
if (o === "/_voltro/inspect/stream") {
|
|
950
|
-
let r =
|
|
951
|
-
if (
|
|
950
|
+
let r = N(a.origin);
|
|
951
|
+
if (F()) {
|
|
952
952
|
t.writeHead(503, {
|
|
953
953
|
"content-type": "application/json",
|
|
954
954
|
...r
|
|
955
955
|
}), t.end(JSON.stringify({ error: "inspect disabled (VOLTRO_INSPECT=off)" }));
|
|
956
956
|
return;
|
|
957
957
|
}
|
|
958
|
-
let i =
|
|
958
|
+
let i = ue(a, "GET");
|
|
959
959
|
if (!i.ok) {
|
|
960
960
|
t.writeHead(401, {
|
|
961
961
|
"content-type": "application/json",
|
package/dist/startEntry.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { et as e } from "./inspectMetrics-
|
|
2
|
-
import { t } from "./start-
|
|
1
|
+
import { et as e } from "./inspectMetrics-BeJdP_3b.js";
|
|
2
|
+
import { t } from "./start-ocqB1seX.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.30.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",
|
|
@@ -62,22 +62,22 @@
|
|
|
62
62
|
"@effect/platform-node": "^0.108.0",
|
|
63
63
|
"@effect/sql": "^0.52.0",
|
|
64
64
|
"@effect/workflow": "^0.19.0",
|
|
65
|
-
"@voltro/ai": "0.
|
|
66
|
-
"@voltro/cache": "0.
|
|
67
|
-
"@voltro/data-transfer": "0.
|
|
68
|
-
"@voltro/database": "0.
|
|
69
|
-
"@voltro/env": "0.
|
|
70
|
-
"@voltro/kv": "0.
|
|
71
|
-
"@voltro/logger": "0.
|
|
72
|
-
"@voltro/plugin-auth": "0.
|
|
73
|
-
"@voltro/plugin-broadcast": "0.
|
|
74
|
-
"@voltro/plugin-mail": "0.
|
|
75
|
-
"@voltro/plugin-storage": "0.
|
|
76
|
-
"@voltro/plugin-webhooks": "0.
|
|
77
|
-
"@voltro/protocol": "0.
|
|
78
|
-
"@voltro/runtime": "0.
|
|
79
|
-
"@voltro/serverless": "0.
|
|
80
|
-
"@voltro/workflow": "0.
|
|
65
|
+
"@voltro/ai": "0.30.0",
|
|
66
|
+
"@voltro/cache": "0.30.0",
|
|
67
|
+
"@voltro/data-transfer": "0.30.0",
|
|
68
|
+
"@voltro/database": "0.30.0",
|
|
69
|
+
"@voltro/env": "0.30.0",
|
|
70
|
+
"@voltro/kv": "0.30.0",
|
|
71
|
+
"@voltro/logger": "0.30.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.30.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.30.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.30.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.30.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.30.0",
|
|
77
|
+
"@voltro/protocol": "0.30.0",
|
|
78
|
+
"@voltro/runtime": "0.30.0",
|
|
79
|
+
"@voltro/serverless": "0.30.0",
|
|
80
|
+
"@voltro/workflow": "0.30.0",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.core.md
CHANGED
|
@@ -67,6 +67,56 @@ Ask in order; stop at the first "yes":
|
|
|
67
67
|
Deep dive: **`data.md`** (queries/mutations/actions/streams/errors),
|
|
68
68
|
**`workflows.md`**, **`scheduling.md`**.
|
|
69
69
|
|
|
70
|
+
**Do NOT hand-roll debounce / one-at-a-time / rate limits around a workflow.**
|
|
71
|
+
They are fields on `workflow({...})`, enforced BEFORE a run exists:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
debounce: { key: (p) => `tour:${p.rowId}`, period: '15 minutes' }
|
|
75
|
+
singleton: { key: (p) => p.tenantId, mode: 'skip' | 'cancel' }
|
|
76
|
+
concurrency: { limit: 5, key: (p) => p.tenantId }
|
|
77
|
+
throttle: { limit: 100, period: '1 minute' } // queues the excess
|
|
78
|
+
rateLimit: { limit: 100, period: '1 minute' } // DROPS the excess
|
|
79
|
+
batch: { item: Item, key: (i) => i.tenantId, maxSize: 100, timeout: '30 seconds' }
|
|
80
|
+
timeouts: { start: '1 hour', finish: '10 minutes' }
|
|
81
|
+
onFailure: 'notifierWorkflowName'
|
|
82
|
+
cancelOn: [{ event: 'jira.issue.deleted', schema: Deleted,
|
|
83
|
+
match: (e, p) => e.issueKey === p.issueKey }]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The hand-rolled version (an idempotency key carrying a timestamp + a re-check
|
|
87
|
+
loop + a round cap) costs one durable cluster entity PER START to express "one
|
|
88
|
+
job, latest deadline". `voltro workflows flow` shows what is queued and why.
|
|
89
|
+
|
|
90
|
+
`cancelOn` reaches a run whose fiber is not executing anything — sleeping,
|
|
91
|
+
suspended, or still queued — which a race inside the body cannot. `match` is
|
|
92
|
+
REQUIRED (the omitted case would cancel every live run); a run that started AFTER
|
|
93
|
+
the event is never cancelled; queued starts of the same workflow are discarded
|
|
94
|
+
too, or the debounced duplicate fires seconds later against the deleted row.
|
|
95
|
+
|
|
96
|
+
Stopping or re-driving MANY runs is `voltro workflows cancel-many` /
|
|
97
|
+
`replay-many`. Both are a DRY RUN until `--commit`, `--limit` is required (the cap
|
|
98
|
+
IS the blast radius; `truncated` says whether more matched), and a cancel needs a
|
|
99
|
+
`--reason` that lands on every affected run.
|
|
100
|
+
|
|
101
|
+
A model call inside a workflow: `aiStep` / `aiObjectStep` from
|
|
102
|
+
`@voltro/ai/workflow`. Pass `store: ctx.store` and the spend is recorded per step;
|
|
103
|
+
the prompt is journaled as a DIGEST unless you write `recordPrompt: 'full'`. Add
|
|
104
|
+
`offload: true` and the run SUSPENDS instead of holding a worker while the model
|
|
105
|
+
thinks — a dispatcher owns the call and resumes the run. Costs one suspend/resume
|
|
106
|
+
round trip (~250ms), so offload the slow calls and leave a fast classification
|
|
107
|
+
inline.
|
|
108
|
+
|
|
109
|
+
**`idempotencyKey` is the execution's IDENTITY, permanently — not a dedupe
|
|
110
|
+
window.** A second start with the same key replays the first run's result
|
|
111
|
+
forever; after it completes the key is SPENT and a genuinely new invocation is a
|
|
112
|
+
silent no-op. So it must vary per unit of work (`` `tour:${rowId}:${editedAt}` ``,
|
|
113
|
+
not `` `tour:${rowId}` ``), while a flow-control `key` — the RESOURCE runs
|
|
114
|
+
compete for — stays stable. Those are two different fields; conflating them is
|
|
115
|
+
what makes "I need to re-arm a key" feel like a missing feature.
|
|
116
|
+
|
|
117
|
+
A start can now come back `queued` / `dropped` / `skipped` with
|
|
118
|
+
`executionId: null`, so narrow on `handle.status` before using the id.
|
|
119
|
+
|
|
70
120
|
### Pick the SERVER primitive (inside a handler)
|
|
71
121
|
|
|
72
122
|
The four above answer "which FILE do I write". This answers "what do I write
|
package/templates/AGENTS.md
CHANGED
|
@@ -67,6 +67,56 @@ Ask in order; stop at the first "yes":
|
|
|
67
67
|
Deep dive: **`data.md`** (queries/mutations/actions/streams/errors),
|
|
68
68
|
**`workflows.md`**, **`scheduling.md`**.
|
|
69
69
|
|
|
70
|
+
**Do NOT hand-roll debounce / one-at-a-time / rate limits around a workflow.**
|
|
71
|
+
They are fields on `workflow({...})`, enforced BEFORE a run exists:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
debounce: { key: (p) => `tour:${p.rowId}`, period: '15 minutes' }
|
|
75
|
+
singleton: { key: (p) => p.tenantId, mode: 'skip' | 'cancel' }
|
|
76
|
+
concurrency: { limit: 5, key: (p) => p.tenantId }
|
|
77
|
+
throttle: { limit: 100, period: '1 minute' } // queues the excess
|
|
78
|
+
rateLimit: { limit: 100, period: '1 minute' } // DROPS the excess
|
|
79
|
+
batch: { item: Item, key: (i) => i.tenantId, maxSize: 100, timeout: '30 seconds' }
|
|
80
|
+
timeouts: { start: '1 hour', finish: '10 minutes' }
|
|
81
|
+
onFailure: 'notifierWorkflowName'
|
|
82
|
+
cancelOn: [{ event: 'jira.issue.deleted', schema: Deleted,
|
|
83
|
+
match: (e, p) => e.issueKey === p.issueKey }]
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The hand-rolled version (an idempotency key carrying a timestamp + a re-check
|
|
87
|
+
loop + a round cap) costs one durable cluster entity PER START to express "one
|
|
88
|
+
job, latest deadline". `voltro workflows flow` shows what is queued and why.
|
|
89
|
+
|
|
90
|
+
`cancelOn` reaches a run whose fiber is not executing anything — sleeping,
|
|
91
|
+
suspended, or still queued — which a race inside the body cannot. `match` is
|
|
92
|
+
REQUIRED (the omitted case would cancel every live run); a run that started AFTER
|
|
93
|
+
the event is never cancelled; queued starts of the same workflow are discarded
|
|
94
|
+
too, or the debounced duplicate fires seconds later against the deleted row.
|
|
95
|
+
|
|
96
|
+
Stopping or re-driving MANY runs is `voltro workflows cancel-many` /
|
|
97
|
+
`replay-many`. Both are a DRY RUN until `--commit`, `--limit` is required (the cap
|
|
98
|
+
IS the blast radius; `truncated` says whether more matched), and a cancel needs a
|
|
99
|
+
`--reason` that lands on every affected run.
|
|
100
|
+
|
|
101
|
+
A model call inside a workflow: `aiStep` / `aiObjectStep` from
|
|
102
|
+
`@voltro/ai/workflow`. Pass `store: ctx.store` and the spend is recorded per step;
|
|
103
|
+
the prompt is journaled as a DIGEST unless you write `recordPrompt: 'full'`. Add
|
|
104
|
+
`offload: true` and the run SUSPENDS instead of holding a worker while the model
|
|
105
|
+
thinks — a dispatcher owns the call and resumes the run. Costs one suspend/resume
|
|
106
|
+
round trip (~250ms), so offload the slow calls and leave a fast classification
|
|
107
|
+
inline.
|
|
108
|
+
|
|
109
|
+
**`idempotencyKey` is the execution's IDENTITY, permanently — not a dedupe
|
|
110
|
+
window.** A second start with the same key replays the first run's result
|
|
111
|
+
forever; after it completes the key is SPENT and a genuinely new invocation is a
|
|
112
|
+
silent no-op. So it must vary per unit of work (`` `tour:${rowId}:${editedAt}` ``,
|
|
113
|
+
not `` `tour:${rowId}` ``), while a flow-control `key` — the RESOURCE runs
|
|
114
|
+
compete for — stays stable. Those are two different fields; conflating them is
|
|
115
|
+
what makes "I need to re-arm a key" feel like a missing feature.
|
|
116
|
+
|
|
117
|
+
A start can now come back `queued` / `dropped` / `skipped` with
|
|
118
|
+
`executionId: null`, so narrow on `handle.status` before using the id.
|
|
119
|
+
|
|
70
120
|
### Pick the SERVER primitive (inside a handler)
|
|
71
121
|
|
|
72
122
|
The four above answer "which FILE do I write". This answers "what do I write
|
|
@@ -602,7 +652,7 @@ each plugin's own README.
|
|
|
602
652
|
|
|
603
653
|
| Topic | Open | Summary |
|
|
604
654
|
|---|---|---|
|
|
605
|
-
| **What's new in 0.
|
|
655
|
+
| **What's new in 0.30.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
606
656
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
607
657
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
608
658
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -635,7 +685,8 @@ each plugin's own README.
|
|
|
635
685
|
| templates.apiBackends | `node_modules/@voltro/cli/templates/agent-docs/templates/apibackends.md` | The minimal Voltro backend — app.config + schema + one streaming query + one tenant-guarded mutation. Tenant-aware out of the box. |
|
|
636
686
|
| templates.appShells | `node_modules/@voltro/cli/templates/agent-docs/templates/appshells.md` | A marketing landing page — hero, features, CTA. Static-rendered with zero JS on the wire by default. |
|
|
637
687
|
| templates.custom | `node_modules/@voltro/cli/templates/agent-docs/templates/custom.md` | Add a template the CLI can scaffold from — a directory under voltro-templates/apps/ with a manifest and the files to copy. |
|
|
638
|
-
|
|
|
688
|
+
| templates.mobile | `node_modules/@voltro/cli/templates/agent-docs/templates/mobile.md` | An Expo (React Native) app that is the third consumer of your api — the same typed hooks (useSubscription / useMutation), offline-first by default, typed deep links, and device registration for push. Expo owns Metro; voltro dev runs the sibling api. |
|
|
689
|
+
| Templates | `node_modules/@voltro/cli/templates/agent-docs/templates/overview.md` | Thirty-eight dogfooded starter templates ship with the framework — twenty-two backend shapes, fourteen frontend shapes, a serverless function library, and an Expo mobile app. Scaffold any of them with one CLI call. |
|
|
639
690
|
| templates.serverless | `node_modules/@voltro/cli/templates/agent-docs/templates/serverless.md` | A library of standalone *.serverless.ts functions — pure compute, request-header/geo, outbound HTTP, Web Crypto HMAC, an LLM call, status-controlled errors. Run with voltro serverless; ship to node / Cloudflare / Scaleway. No server, no port. |
|
|
640
691
|
| Testing | `node_modules/@voltro/cli/templates/agent-docs/testing.md` | Voltro's test story — the @voltro/testing package, the test pyramid (unit handlers/tools → workflow runner → e2e), and the voltro test / voltro e2e CLI commands. |
|
|
641
692
|
| Workflows | `node_modules/@voltro/cli/templates/agent-docs/workflows.md` | Durable Effect workflows in Voltro — what they are, when to use them, and the current runtime boundaries. |
|
|
@@ -9,7 +9,7 @@ each plugin's own README.
|
|
|
9
9
|
|
|
10
10
|
| Topic | Open | Summary |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| **What's new in 0.
|
|
12
|
+
| **What's new in 0.30.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
13
13
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
14
14
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
15
15
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -42,7 +42,8 @@ each plugin's own README.
|
|
|
42
42
|
| templates.apiBackends | `node_modules/@voltro/cli/templates/agent-docs/templates/apibackends.md` | The minimal Voltro backend — app.config + schema + one streaming query + one tenant-guarded mutation. Tenant-aware out of the box. |
|
|
43
43
|
| templates.appShells | `node_modules/@voltro/cli/templates/agent-docs/templates/appshells.md` | A marketing landing page — hero, features, CTA. Static-rendered with zero JS on the wire by default. |
|
|
44
44
|
| templates.custom | `node_modules/@voltro/cli/templates/agent-docs/templates/custom.md` | Add a template the CLI can scaffold from — a directory under voltro-templates/apps/ with a manifest and the files to copy. |
|
|
45
|
-
|
|
|
45
|
+
| templates.mobile | `node_modules/@voltro/cli/templates/agent-docs/templates/mobile.md` | An Expo (React Native) app that is the third consumer of your api — the same typed hooks (useSubscription / useMutation), offline-first by default, typed deep links, and device registration for push. Expo owns Metro; voltro dev runs the sibling api. |
|
|
46
|
+
| Templates | `node_modules/@voltro/cli/templates/agent-docs/templates/overview.md` | Thirty-eight dogfooded starter templates ship with the framework — twenty-two backend shapes, fourteen frontend shapes, a serverless function library, and an Expo mobile app. Scaffold any of them with one CLI call. |
|
|
46
47
|
| templates.serverless | `node_modules/@voltro/cli/templates/agent-docs/templates/serverless.md` | A library of standalone *.serverless.ts functions — pure compute, request-header/geo, outbound HTTP, Web Crypto HMAC, an LLM call, status-controlled errors. Run with voltro serverless; ship to node / Cloudflare / Scaleway. No server, no port. |
|
|
47
48
|
| Testing | `node_modules/@voltro/cli/templates/agent-docs/testing.md` | Voltro's test story — the @voltro/testing package, the test pyramid (unit handlers/tools → workflow runner → e2e), and the voltro test / voltro e2e CLI commands. |
|
|
48
49
|
| Workflows | `node_modules/@voltro/cli/templates/agent-docs/workflows.md` | Durable Effect workflows in Voltro — what they are, when to use them, and the current runtime boundaries. |
|
|
@@ -269,7 +269,7 @@
|
|
|
269
269
|
"group": "templates.apiBackends",
|
|
270
270
|
"description": "The minimal Voltro backend — app.config + schema + one streaming query + one tenant-guarded mutation. Tenant-aware out of the box.",
|
|
271
271
|
"path": "agent-docs/templates/apibackends.md",
|
|
272
|
-
"files":
|
|
272
|
+
"files": 22
|
|
273
273
|
},
|
|
274
274
|
{
|
|
275
275
|
"id": "templates/appshells",
|
|
@@ -278,7 +278,7 @@
|
|
|
278
278
|
"group": "templates.appShells",
|
|
279
279
|
"description": "A marketing landing page — hero, features, CTA. Static-rendered with zero JS on the wire by default.",
|
|
280
280
|
"path": "agent-docs/templates/appshells.md",
|
|
281
|
-
"files":
|
|
281
|
+
"files": 14
|
|
282
282
|
},
|
|
283
283
|
{
|
|
284
284
|
"id": "templates/custom",
|
|
@@ -289,12 +289,21 @@
|
|
|
289
289
|
"path": "agent-docs/templates/custom.md",
|
|
290
290
|
"files": 1
|
|
291
291
|
},
|
|
292
|
+
{
|
|
293
|
+
"id": "templates/mobile",
|
|
294
|
+
"title": "templates.mobile",
|
|
295
|
+
"section": "Templates",
|
|
296
|
+
"group": "templates.mobile",
|
|
297
|
+
"description": "An Expo (React Native) app that is the third consumer of your api — the same typed hooks (useSubscription / useMutation), offline-first by default, typed deep links, and device registration for push. Expo owns Metro; voltro dev runs the sibling api.",
|
|
298
|
+
"path": "agent-docs/templates/mobile.md",
|
|
299
|
+
"files": 1
|
|
300
|
+
},
|
|
292
301
|
{
|
|
293
302
|
"id": "templates/overview",
|
|
294
303
|
"title": "Templates",
|
|
295
304
|
"section": "Templates",
|
|
296
305
|
"group": null,
|
|
297
|
-
"description": "Thirty-
|
|
306
|
+
"description": "Thirty-eight dogfooded starter templates ship with the framework — twenty-two backend shapes, fourteen frontend shapes, a serverless function library, and an Expo mobile app. Scaffold any of them with one CLI call.",
|
|
298
307
|
"path": "agent-docs/templates/overview.md",
|
|
299
308
|
"files": 1
|
|
300
309
|
},
|
|
@@ -323,7 +332,7 @@
|
|
|
323
332
|
"group": null,
|
|
324
333
|
"description": "Durable Effect workflows in Voltro — what they are, when to use them, and the current runtime boundaries.",
|
|
325
334
|
"path": "agent-docs/workflows.md",
|
|
326
|
-
"files":
|
|
335
|
+
"files": 10
|
|
327
336
|
}
|
|
328
337
|
],
|
|
329
338
|
"plugins": [
|
|
@@ -631,6 +640,7 @@
|
|
|
631
640
|
"api-backend-mariadb": "agent-docs/templates/apibackends.md",
|
|
632
641
|
"api-backend-storage": "agent-docs/templates/apibackends.md",
|
|
633
642
|
"api-backend": "agent-docs/templates/apibackends.md",
|
|
643
|
+
"api-collab": "agent-docs/templates/apibackends.md",
|
|
634
644
|
"api-data-advanced": "agent-docs/templates/apibackends.md",
|
|
635
645
|
"api-durable": "agent-docs/templates/apibackends.md",
|
|
636
646
|
"api-feature-flags": "agent-docs/templates/apibackends.md",
|
|
@@ -648,6 +658,7 @@
|
|
|
648
658
|
"app": "agent-docs/templates/appshells.md",
|
|
649
659
|
"blank": "agent-docs/templates/appshells.md",
|
|
650
660
|
"changelog": "agent-docs/templates/appshells.md",
|
|
661
|
+
"collab": "agent-docs/templates/appshells.md",
|
|
651
662
|
"contact": "agent-docs/templates/appshells.md",
|
|
652
663
|
"custom": "agent-docs/templates/custom.md",
|
|
653
664
|
"dashboard": "agent-docs/templates/appshells.md",
|
|
@@ -655,6 +666,7 @@
|
|
|
655
666
|
"edge-functions": "agent-docs/templates/serverless.md",
|
|
656
667
|
"i18n": "agent-docs/templates/appshells.md",
|
|
657
668
|
"landing": "agent-docs/templates/appshells.md",
|
|
669
|
+
"mobile-app": "agent-docs/templates/mobile.md",
|
|
658
670
|
"overview": "agent-docs/templates/overview.md",
|
|
659
671
|
"spa": "agent-docs/templates/appshells.md",
|
|
660
672
|
"ssr-api": "agent-docs/templates/appshells.md",
|
|
@@ -1821,6 +1821,62 @@ const groundedAnswer = (prompt: string) =>
|
|
|
1821
1821
|
- **A cache outage degrades to always-generate.** Both the lookup and the store are best-effort — a `CacheError` reads as a miss (or a swallowed put), never a failed call. The cache is an optimisation, not a dependency.
|
|
1822
1822
|
- **The object variant stores the DECODED object.** With the memory backend it round-trips by reference; with a RESP backend it is JSON, so a schema whose decoded form is not JSON-safe (class instances, non-plain branded carriers) will not survive a cross-process hit — cache the text form or a JSON-safe projection for those.
|
|
1823
1823
|
|
|
1824
|
+
## A model call inside a workflow — `aiStep`
|
|
1825
|
+
|
|
1826
|
+
`@voltro/ai/workflow` wraps a call as a durable step:
|
|
1827
|
+
|
|
1828
|
+
```ts
|
|
1829
|
+
import { aiStep, aiObjectStep } from '@voltro/ai/workflow'
|
|
1830
|
+
|
|
1831
|
+
const summary = yield* aiStep({
|
|
1832
|
+
name: 'summarise-thread',
|
|
1833
|
+
prompt: `Summarise:\n${thread}`,
|
|
1834
|
+
store: ctx.store,
|
|
1835
|
+
tenantId: payload.tenantId,
|
|
1836
|
+
})
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
Journaling is **not** what this adds — every `step()` is already journaled, so a replay of a plain wrapped `generateText` returns the recorded completion rather than re-calling the model. Three things are different:
|
|
1840
|
+
|
|
1841
|
+
1. **It records what the run cost.** A model call inside a workflow was invisible to `_voltro_ai_usage` unless the app remembered to call `recordAiUsage` by hand — so the spend ledger was systematically missing exactly the calls that run unattended. Pass `store` and every call is recorded, attributed to the workflow and the step.
|
|
1842
|
+
2. **It does not copy the prompt into a second table.** `step({ input })` is written to `_voltro_workflow_run_steps` and rendered in the dashboard; for a prompt built from customer data that is a plaintext copy outside whatever boundary you established for the source. The default records a **digest** plus the length. `recordPrompt: 'full'` exists and has to be typed out.
|
|
1843
|
+
3. **Provider failures retry like provider failures.** The default policy handles a 429 with a `Retry-After` and a 5xx, rather than every app rediscovering that a bare call fails the whole durable run on a rate limit.
|
|
1844
|
+
|
|
1845
|
+
`aiObjectStep` is the schema-constrained form; the schema is the step's success schema too, so the journaled value decodes on replay exactly as it did on the first run.
|
|
1846
|
+
|
|
1847
|
+
Pass `offload: true` and the run stops occupying a worker while the model thinks — see the next section.
|
|
1848
|
+
|
|
1849
|
+
## Offloading the call — `offload: true`
|
|
1850
|
+
|
|
1851
|
+
An inline `aiStep` holds a runner fiber for the length of the model call. At six seconds a call and two hundred concurrent runs, that is two hundred parked workers waiting on a socket, and the cluster's concurrency is spent on latency rather than on work.
|
|
1852
|
+
|
|
1853
|
+
```ts
|
|
1854
|
+
const summary = yield* aiStep({
|
|
1855
|
+
name: 'summarise-thread',
|
|
1856
|
+
prompt: `Summarise:\n${thread}`,
|
|
1857
|
+
store: ctx.store,
|
|
1858
|
+
offload: true,
|
|
1859
|
+
})
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
The run **suspends**: the worker is released, the wait lives as a row in `_voltro_ai_inferences`, and a dispatcher owns the socket. Two hundred waiting runs become two hundred rows and (by default) four in-flight requests.
|
|
1863
|
+
|
|
1864
|
+
Nothing about this needs a third party to operate an inference tier. It needs something to own the socket while the run sleeps — and a server process is something. The two pieces it is built from already existed: durable suspend/resume (`awaitSignalSuspending`, built for human-in-the-loop waits) and a leased work queue with a coordinated drainer (the same shape the admission queue has).
|
|
1865
|
+
|
|
1866
|
+
**The cost, so you can decide per call.** A suspend/resume round trip adds the dispatcher's poll interval (250 ms) plus one engine wake. On a six-second call that is under 5%; on a 200 ms classification call it doubles the latency. So it is a mode, not a default: offload the calls that are slow enough for a worker to be worth freeing — which is most of them — and leave the fast ones inline.
|
|
1867
|
+
|
|
1868
|
+
**What the queue guarantees.**
|
|
1869
|
+
|
|
1870
|
+
- The enqueue is idempotent. The row id is derived from the execution and the step name, so a replay cannot queue — and pay for — the same call twice.
|
|
1871
|
+
- The claim is a conditional update, not a read-then-write. Two dispatchers cannot both perform (and both bill) one call.
|
|
1872
|
+
- The order is **perform → resume the run → mark the row**. A crash between the resume and the mark leaves a row whose lease expires and is reclaimed, and the second resume of a resolved deferred is a no-op. The other order would leave a run waiting for a signal nobody will send again.
|
|
1873
|
+
- A give-up **resumes the run with the failure**. A queued call that was abandoned without telling its run is the one unrecoverable outcome here, and the ordering exists to rule it out.
|
|
1874
|
+
- Retries follow the same rules as the inline policy, `Retry-After` included, so the two modes do not back off differently.
|
|
1875
|
+
|
|
1876
|
+
`aiObjectStep({ offload: true })` renders your schema to JSON Schema for the dispatcher — a JavaScript Schema cannot be journaled — and still **decodes on the awaiting side**, where the real schema exists.
|
|
1877
|
+
|
|
1878
|
+
The Flow tab shows the queue: what is waiting and for how long, which calls have been waiting more than two minutes, which claims have a lease their dispatcher will never release, and the dispatcher's own last tick. A run parked on an offloaded call reads `suspended` in the run list with no step row yet, so this is the only view of the wait while it is happening.
|
|
1879
|
+
|
|
1824
1880
|
## Deliberately your call
|
|
1825
1881
|
|
|
1826
1882
|
The toolkit prices + records + gates; a few things stay explicit by design:
|
|
@@ -1853,16 +1853,35 @@ voltro workflows resume <runId>
|
|
|
1853
1853
|
voltro workflows signal <runId> --name approval # inject a named signal
|
|
1854
1854
|
voltro workflows update <runId> --name …
|
|
1855
1855
|
voltro workflows children <parentExecutionId>
|
|
1856
|
+
voltro workflows flow # the admission queue + ledger
|
|
1857
|
+
voltro workflows pause|unpause <workflowName> # stop/restart admission fleet-wide
|
|
1858
|
+
voltro workflows cancel-many --reason "…" # DRY RUN until --commit
|
|
1859
|
+
voltro workflows replay-many --mode redrive # DRY RUN until --commit
|
|
1860
|
+
voltro workflows inferences # offloaded model calls in flight
|
|
1856
1861
|
```
|
|
1857
1862
|
|
|
1863
|
+
`inferences` shows what nothing else can: a run parked on an offloaded model call reads `suspended` in the run list with no step row yet, so during a slow provider — the moment you would look — the run list has nothing to say.
|
|
1864
|
+
|
|
1858
1865
|
Underneath, workflow state lives in the `_voltro_workflow_runs` + `_voltro_workflow_run_steps` tables and is surfaced live by the dashboard's Workflows panel. The same data is reachable over HTTP:
|
|
1859
1866
|
|
|
1860
1867
|
```bash
|
|
1861
1868
|
curl -s localhost:$PORT/_voltro/inspect/workflows/runs | jq # recent runs
|
|
1862
1869
|
curl -s "localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/steps" | jq # step-by-step
|
|
1863
1870
|
curl -s "localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/events" | jq # the run's event log
|
|
1871
|
+
curl -s "localhost:$PORT/_voltro/inspect/workflows/stats?hours=24" | jq # bucketed run activity (the dashboard chart)
|
|
1864
1872
|
```
|
|
1865
1873
|
|
|
1874
|
+
The runs endpoint filters **server-side**, so a triage query over a large run history costs one narrow page instead of the whole table:
|
|
1875
|
+
|
|
1876
|
+
```bash
|
|
1877
|
+
# multi-status + tag search + source + id-prefix + time range — all composable
|
|
1878
|
+
curl -s "localhost:$PORT/_voltro/inspect/workflows/runs?statuses=failed,cancelled&q=orders&source=workflow-rpc&idPrefix=wfrun_&from=2026-08-01T00:00:00Z&to=2026-08-08T00:00:00Z" | jq
|
|
1879
|
+
```
|
|
1880
|
+
|
|
1881
|
+
`statuses` is a comma list; `q` is a case-insensitive tag substring; `idPrefix` matches the run id **or** the execution id (you never have to know which kind your log line carried); `from`/`to` bound `startedAt`. The dashboard's filter bar sends exactly these params.
|
|
1882
|
+
|
|
1883
|
+
`/workflows/stats` returns ~48 buckets over a trailing window (`hours`, default 24, max 168; optional `tag`), each with `started` / `succeeded` / `failed` / `cancelled` counts, plus per-workflow totals. When the window held more runs than the scan cap, the response says `truncated: true` — the chart renders that as a warning, because a silently-truncated chart shows throughput dropping at exactly the moment it spiked.
|
|
1884
|
+
|
|
1866
1885
|
Each run row carries ID, name, status (running / succeeded / failed / dead), step count, last completed step, and duration. The per-run action endpoint matches `…/workflows/runs/<runId>/<action>` for `cancel` / `retry` / `suspend` / `resume` / `signal` — the `voltro workflows` subcommands and the dashboard's run-detail buttons both POST to these:
|
|
1867
1886
|
|
|
1868
1887
|
```bash
|
|
@@ -45,7 +45,7 @@ The framework's generated `.framework/app.tsx` wraps the Router in `<I18nProvide
|
|
|
45
45
|
|
|
46
46
|
Server-side, the active locale is determined by, in priority order:
|
|
47
47
|
|
|
48
|
-
1. **`voltro:
|
|
48
|
+
1. **`voltro:locale` cookie** — the user's explicit choice (written by `@voltro/ui-shadcn`'s ProfileMenu).
|
|
49
49
|
2. **`Accept-Language` header** — the browser/OS preference, q-weighted and sorted per RFC 4647.
|
|
50
50
|
3. **`defaultLocale`** — last-resort fallback.
|
|
51
51
|
|
|
@@ -240,7 +240,7 @@ export const getCatalog = (locale?: string) => pickCatalog({ en, de }, locale, '
|
|
|
240
240
|
```
|
|
241
241
|
|
|
242
242
|
```tsx
|
|
243
|
-
// any page — meta gets the active locale (from the URL prefix, or the voltro:
|
|
243
|
+
// any page — meta gets the active locale (from the URL prefix, or the voltro:locale cookie)
|
|
244
244
|
import { getCatalog } from '../locales'
|
|
245
245
|
|
|
246
246
|
export const meta = ({ locale }: { readonly locale: string }): PageMeta => ({
|
|
@@ -330,14 +330,14 @@ The wrap deliberately doesn't re-export everything, to keep the framework's bles
|
|
|
330
330
|
<!-- source: en/i18n/url-strategies.md -->
|
|
331
331
|
## URL strategies
|
|
332
332
|
|
|
333
|
-
_Cookie-only vs URL-prefix routing (Strategy A / B), the URL-prefix integration sketch, resolveLocale on the /server subpath, the voltro:
|
|
333
|
+
_Cookie-only vs URL-prefix routing (Strategy A / B), the URL-prefix integration sketch, resolveLocale on the /server subpath, the voltro:locale / voltro:theme cookie convention._
|
|
334
334
|
|
|
335
335
|
There are two ways an i18n app can encode the active locale. Pick **one**, based on what the app's URLs are *for*.
|
|
336
336
|
|
|
337
337
|
## Strategy A — cookie-only (default for product apps)
|
|
338
338
|
|
|
339
339
|
- All locales are served from the **same** URL (`/dashboard`, `/settings`).
|
|
340
|
-
- The `voltro:
|
|
340
|
+
- The `voltro:locale` cookie + `Accept-Language` determine which catalog renders.
|
|
341
341
|
- The ProfileMenu's language switch — or the standalone `<LocaleSwitcher>` from `@voltro/ui-shadcn` — writes the cookie and reloads.
|
|
342
342
|
- Use when URLs are **functional** (`/dashboard/projects/42/deployments`) and don't need to encode the language. This is most apps.
|
|
343
343
|
|
|
@@ -511,15 +511,16 @@ The `/server` subpath is convention. Both that path and the main entry are pure
|
|
|
511
511
|
|
|
512
512
|
## Cookie convention
|
|
513
513
|
|
|
514
|
-
The framework reads + writes **`voltro:
|
|
514
|
+
The framework reads + writes **`voltro:locale`** for the active language choice (lowercase IETF tag: `en`, `de`, `fr-CA`). `@voltro/ui-shadcn`'s ProfileMenu and the framework's auto-wired resolver agree on this name — **don't pick a different one in app code.**
|
|
515
515
|
|
|
516
|
-
For theme the parallel cookie is **`voltro:theme`** (values `'system' | 'light' | 'dark'`). Both are managed by the kit's ProfileMenu out of the box; the helpers live in `@voltro/ui-shadcn` (`THEME_COOKIE`, `
|
|
516
|
+
For theme the parallel cookie is **`voltro:theme`** (values `'system' | 'light' | 'dark'`). Both are managed by the kit's ProfileMenu out of the box; the helpers live in `@voltro/ui-shadcn` (`THEME_COOKIE`, `LOCALE_COOKIE`, `getCookie`, `setCookie`, `deleteCookie`, `parsePreferenceCookies`, `applyTheme`).
|
|
517
517
|
|
|
518
518
|
## Anti-patterns
|
|
519
519
|
|
|
520
520
|
- **Don't mix strategies within one app.** Pick cookie-only *or* URL-prefix per app; mixing them produces ambiguous canonical URLs and broken language switching.
|
|
521
521
|
- **Don't read `Accept-Language` on the client.** It's server-only — `navigator.languages` can diverge from what the server saw and cause a hydration mismatch.
|
|
522
|
-
- **Don't invent your own cookie name.** The kit and the resolver only agree on `voltro:
|
|
522
|
+
- **Don't invent your own cookie name.** The kit and the resolver only agree on `voltro:locale` / `voltro:theme`.
|
|
523
|
+
- **Don't hardcode the name either — import the constant.** `LOCALE_COOKIE` / `THEME_COOKIE` from `@voltro/ui-shadcn`. A cookie name the framework READS and your app WRITES is a public API, and it is the only kind where both sides can disagree with nothing failing: the resolver finds nothing, falls back to `Accept-Language`, and a user's language choice quietly stops working — but only for the users whose browser language differs from their choice, which is the least likely case anyone tests. With the constant, a rename in the framework is a compile error in your app. `voltro doctor` flags a written literal for exactly this reason.
|
|
523
524
|
|
|
524
525
|
|
|
525
526
|
|
|
@@ -265,11 +265,23 @@ code already speaks:
|
|
|
265
265
|
|
|
266
266
|
_"@voltro/react-native — the credential-free mobile plumbing: registerDevice + the _voltro_devices table, defineDeepLink + its matcher, useBackgroundSync, and offline-first client defaults + connection status."_
|
|
267
267
|
|
|
268
|
-
The
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
The React-client bindings are **import-safe** in React Native — every DOM touch
|
|
269
|
+
in `@voltro/client` is `typeof window`-guarded, so nothing crashes at import.
|
|
270
|
+
What is **not done yet** is wiring the client's runtime for RN: building the
|
|
271
|
+
`ApiHandle`s (runtime + subscription cache + rpc client) over a native
|
|
272
|
+
WebSocket, plus an RN persistence adapter and a NetInfo connection signal. That
|
|
273
|
+
is Phase **M0** of the mobile plan — until it lands, the reactive loop is
|
|
274
|
+
unproven on a device. `@voltro/react-native` ships the mobile-specific plumbing
|
|
275
|
+
that works **today**, limited to the parts that need **no per-tenant credentials
|
|
276
|
+
and no native runtime**: device registration, background-sync scheduling,
|
|
277
|
+
offline-first defaults, a connection-status surface, and the deep-link
|
|
278
|
+
declaration shape.
|
|
279
|
+
|
|
280
|
+
> **Scaffold a mobile app.** `voltro create-project acme --api=api-backend
|
|
281
|
+
> --mobile` (or `voltro add-app mobile --template mobile-app`) scaffolds an Expo
|
|
282
|
+
> app that consumes your api with the same typed hooks. Expo owns Metro
|
|
283
|
+
> (`expo start` / `expo run:ios`), not `voltro dev`. See
|
|
284
|
+
> [the `mobile-app` template](/docs/templates/mobile-app).
|
|
273
285
|
|
|
274
286
|
The package **root is RN-safe** — no `node:*`, no `@voltro/database`, and React
|
|
275
287
|
is reached only through the hooks (an optional peer). The `_voltro_devices` table
|
|
@@ -409,6 +421,6 @@ not built here:
|
|
|
409
421
|
|---|---|
|
|
410
422
|
| **APNs / FCM sender adapters** | Need per-tenant Apple Developer / Firebase credentials — genuinely external, managed via provider provisioning. Registration stores the token; sending to it is the seam. |
|
|
411
423
|
| **Native module bindings** (camera, biometrics, secure token storage) | Need a native runtime this TS package cannot provide. |
|
|
412
|
-
| **Swift / Kotlin SDK generators** |
|
|
424
|
+
| **Swift / Kotlin SDK generators** | **Built + golden-tested** — `voltro build api --target swift\|kotlin` emits a native SDK package. What is deferred is *compiling* the emitted package (`swiftc` / Gradle): that is a mobile-CI step, no cross-language toolchain lives in the framework repo. |
|
|
413
425
|
| **Universal-links / App-Links file automation** (`apple-app-site-association`, `assetlinks.json`) | A deployment-layer concern, not a client primitive. |
|
|
414
426
|
| **`*.deepLink.ts` codegen discovery** | One additive file after the release settles; the descriptor shape is final, so register links via `matchFirstDeepLink()` today. |
|