@voltro/cli 0.26.0 → 0.27.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 +297 -0
- package/dist/apiBuild-BrjrVJJh.js +2 -0
- package/dist/{apiBuild-BtrTyKnF.js → apiBuild-D22_EpoR.js} +1 -1
- package/dist/bin.js +1 -1
- package/dist/{commands-D-9iYF2D.js → commands-jBX8no1I.js} +23 -23
- package/dist/{dev-BnWq4jeA.js → dev-DNkso403.js} +1 -1
- package/dist/{dev-Dbm6SWtn.js → dev-DcbIJrWg.js} +1777 -1718
- package/dist/index.js +1 -1
- package/dist/serveCommand-DfkisVWP.js +1310 -0
- package/dist/serveEntry.js +1 -1
- package/package.json +17 -17
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/data.md +219 -0
- package/templates/agent-docs/plugins.md +28 -0
- package/templates/agent-docs/whats-new.md +128 -310
- 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-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 +9 -9
- 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-DDJ0It4j.js +0 -2
- package/dist/serveCommand-XBXuwJty.js +0 -1294
package/dist/serveEntry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Z as e } from "./inspectMetrics-BU90mvJN.js";
|
|
2
2
|
import { c as t } from "./seedRunner-D6eu-u5U.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-DfkisVWP.js";
|
|
5
5
|
export { e as loadDotEnv, n as registerAppModules, t as registerDriver, r as runServe };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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.27.0",
|
|
66
|
+
"@voltro/cache": "0.27.0",
|
|
67
|
+
"@voltro/data-transfer": "0.27.0",
|
|
68
|
+
"@voltro/database": "0.27.0",
|
|
69
|
+
"@voltro/env": "0.27.0",
|
|
70
|
+
"@voltro/kv": "0.27.0",
|
|
71
|
+
"@voltro/logger": "0.27.0",
|
|
72
|
+
"@voltro/plugin-auth": "0.27.0",
|
|
73
|
+
"@voltro/plugin-broadcast": "0.27.0",
|
|
74
|
+
"@voltro/plugin-mail": "0.27.0",
|
|
75
|
+
"@voltro/plugin-storage": "0.27.0",
|
|
76
|
+
"@voltro/plugin-webhooks": "0.27.0",
|
|
77
|
+
"@voltro/protocol": "0.27.0",
|
|
78
|
+
"@voltro/runtime": "0.27.0",
|
|
79
|
+
"@voltro/serverless": "0.27.0",
|
|
80
|
+
"@voltro/workflow": "0.27.0",
|
|
81
81
|
"chokidar": "^5.0.0",
|
|
82
82
|
"ioredis": "^5.11.1",
|
|
83
83
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.md
CHANGED
|
@@ -602,7 +602,7 @@ each plugin's own README.
|
|
|
602
602
|
|
|
603
603
|
| Topic | Open | Summary |
|
|
604
604
|
|---|---|---|
|
|
605
|
-
| **What's new in 0.
|
|
605
|
+
| **What's new in 0.26.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
606
|
| 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
607
|
| 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
608
|
| 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. |
|
|
@@ -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.26.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. |
|
|
@@ -536,6 +536,32 @@ Queries are streaming RPCs whose elements are **subscription events**: an initia
|
|
|
536
536
|
- **Using a stream for durable data.** Streams are transient. Persist rows and expose them through a query when the UI should survive reloads or sync across tabs.
|
|
537
537
|
|
|
538
538
|
|
|
539
|
+
## Contradictions refused at declaration
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
defineQuery({ name: 'q', guards: [], … }) // ✗ enforces nothing
|
|
543
|
+
defineQuery({ name: 'q', source: '', … }) // ✗ reactive, subscribed to nothing
|
|
544
|
+
defineQuery({ name: 'q', internal: true, overridesPlugin: true, … }) // ✗ removes, replaces nothing
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
The same three shapes `defineEvent` refuses, for the same reasons — a rule that
|
|
548
|
+
holds for one primitive and not another is worse than no rule, because the
|
|
549
|
+
answer then depends on which file you happened to open.
|
|
550
|
+
|
|
551
|
+
**`guards: []`** reads at the call site as if the procedure were protected and
|
|
552
|
+
enforces nothing; the check runs only for a non-empty list. Omit the field.
|
|
553
|
+
|
|
554
|
+
**An empty `source`** declares reactivity and subscribes to nothing: the query
|
|
555
|
+
serves one snapshot and never updates, which is indistinguishable from "nothing
|
|
556
|
+
changed". Worse than a *stale* source, which the boot warning can at least name
|
|
557
|
+
— this one names no table at all, so nothing can report it.
|
|
558
|
+
|
|
559
|
+
**`internal: true` + `overridesPlugin`** removes the plugin's route and puts
|
|
560
|
+
something unreachable in its place: callers get a 404 for something that used to
|
|
561
|
+
work, with no diff that says so. Joins the existing refusals of `internal` with
|
|
562
|
+
`publicApi` or `exposeAsTool`.
|
|
563
|
+
|
|
564
|
+
|
|
539
565
|
## Loading vs empty — don't conflate them
|
|
540
566
|
|
|
541
567
|
`useSubscription` returns `loading` and `isEmpty` alongside `data`. They are
|
|
@@ -1695,6 +1721,13 @@ export default (input, ctx) => Effect.gen(function* () {
|
|
|
1695
1721
|
|
|
1696
1722
|
Three typed errors reach the **producer**, so a mismatch is one failing call rather than every consumer's handler breaking on a field that is not there: `EventPayloadInvalid`, `EventKeyInvalid`, `EventPayloadTooLarge`.
|
|
1697
1723
|
|
|
1724
|
+
`EventPayloadTooLarge` fires at **7,500 bytes** for the encoded envelope (event
|
|
1725
|
+
name + key + payload, as JSON). The ceiling is not arbitrary and it is not a
|
|
1726
|
+
transport limit to tune around: an event says that something *happened*, so
|
|
1727
|
+
`photo.added` carries a photo REFERENCE and the consumer fetches the photo
|
|
1728
|
+
through a route that can stream, cache and authorize it. A payload approaching
|
|
1729
|
+
this size is usually a read that has been pushed into a notification.
|
|
1730
|
+
|
|
1698
1731
|
<Callout>
|
|
1699
1732
|
**Both handler styles publish.** `ctx.events.publish` returns an Effect, so the
|
|
1700
1733
|
`Effect.gen` form above is the idiomatic one — but `await ctx.events.publish(…)`
|
|
@@ -1721,6 +1754,30 @@ Everything you would otherwise hand-roll is gone, and each of these was a real b
|
|
|
1721
1754
|
- **A key change is a clean switch** — the old subscription ends before the new one starts.
|
|
1722
1755
|
- **`key: null` means "not yet"**: no subscription, `status: 'idle'`. You never need a placeholder key.
|
|
1723
1756
|
|
|
1757
|
+
## Showing that it is showing stale
|
|
1758
|
+
|
|
1759
|
+
`useEvent` returns `{ status, missed, lastMiss }` — `status` is
|
|
1760
|
+
`'idle' | 'connecting' | 'live'`, so `status === 'live'` is your connected flag
|
|
1761
|
+
and needs no extra plumbing.
|
|
1762
|
+
|
|
1763
|
+
```tsx
|
|
1764
|
+
const { status, missed } = useEvent(gameStarted, { arenaId }, onStart)
|
|
1765
|
+
|
|
1766
|
+
// A screen nobody is standing at should say when it stopped being current.
|
|
1767
|
+
{status !== 'live' && <Badge>reconnecting…</Badge>}
|
|
1768
|
+
{missed > 0 && <Badge>{missed} missed — refreshing</Badge>}
|
|
1769
|
+
```
|
|
1770
|
+
|
|
1771
|
+
This matters most where nobody is watching the tab. A wall display that loses
|
|
1772
|
+
its connection keeps rendering the last thing it received, and from across the
|
|
1773
|
+
room "stale" and "current" look identical. The difference between *showing old
|
|
1774
|
+
data* and *showing that it is showing old data* is one badge.
|
|
1775
|
+
|
|
1776
|
+
`missed` is the other half: it is COMPUTED, never estimated, so a non-zero value
|
|
1777
|
+
means deliveries provably did not arrive — worth surfacing rather than hiding,
|
|
1778
|
+
because the refresh that follows is visible anyway.
|
|
1779
|
+
|
|
1780
|
+
|
|
1724
1781
|
## What it guarantees — read this before you build on it
|
|
1725
1782
|
|
|
1726
1783
|
- **At-most-once, best-effort, live.** No persistence, no retry, no redelivery. For guarantees use the [outbox](/docs/data/outbox); this is the other axis.
|
|
@@ -1796,6 +1853,144 @@ consequence: during a **rolling deploy** replicas on different framework version
|
|
|
1796
1853
|
use different channel names, so cross-replica delivery is degraded for the length
|
|
1797
1854
|
of the rollout. Local delivery on each replica is unaffected throughout.
|
|
1798
1855
|
|
|
1856
|
+
## Measured against socket.io
|
|
1857
|
+
|
|
1858
|
+
Same machine, same Redis, same topology, back to back:
|
|
1859
|
+
|
|
1860
|
+
| | p50 (median of 5) | p50 range | p99 (median of 5) | p99 range |
|
|
1861
|
+
| --- | --- | --- | --- | --- |
|
|
1862
|
+
| **Voltro**, publisher → Redis → subscriber | **0.68 ms** | 0.58–0.86 | 7.01 ms | 4.50–13.36 |
|
|
1863
|
+
| **socket.io + @socket.io/redis-adapter** | 1.31 ms | 1.16–1.73 | **4.52 ms** | 4.33–7.83 |
|
|
1864
|
+
|
|
1865
|
+
Five runs each, alternating, on one machine. **The p50 ranges do not overlap —
|
|
1866
|
+
that is a real ~2x advantage.** The p99 ranges DO overlap, so the tail difference
|
|
1867
|
+
is weaker evidence than the medians.
|
|
1868
|
+
|
|
1869
|
+
**An earlier version of this table reported one run each** and claimed "32%
|
|
1870
|
+
faster at the median, 44% worse at the tail". Both numbers were noise: the
|
|
1871
|
+
median advantage is nearer 2x and the tail gap is inside the overlap. A single
|
|
1872
|
+
measurement presented as a fact is the defect this framework spends its time
|
|
1873
|
+
removing, and it was committed here.
|
|
1874
|
+
|
|
1875
|
+
**Where our tail comes from, measured rather than guessed.** Splitting the
|
|
1876
|
+
publish path: our own code — building the envelope, the Effect fiber, handing
|
|
1877
|
+
off — costs **p50 0.056 ms / p99 0.444 ms**. Waiting for Redis to acknowledge
|
|
1878
|
+
costs **p50 1.17 ms / p99 6.43 ms**. So roughly 0.4 ms of a 7 ms tail is ours;
|
|
1879
|
+
the rest is the broker round-trip, which socket.io pays too. There is no
|
|
1880
|
+
code-level tail defect to fix here — on this machine the number is dominated by
|
|
1881
|
+
Docker's network stack.
|
|
1882
|
+
|
|
1883
|
+
The script is in the repo (`scripts/bench/socketio-cross-replica.mjs`) so the
|
|
1884
|
+
number can be re-taken rather than believed. It is not a test: keeping a
|
|
1885
|
+
competitor in the dependency tree to hold a number green is the wrong trade.
|
|
1886
|
+
|
|
1887
|
+
**Topology is what makes this a comparison at all.** Two server instances share
|
|
1888
|
+
one Redis; the client hangs off instance B and every emit is issued on instance
|
|
1889
|
+
A. The first version measured socket.io on a plain localhost websocket and came
|
|
1890
|
+
out 3x faster — which proved nothing, because that is one hop and this is two
|
|
1891
|
+
through a broker.
|
|
1892
|
+
|
|
1893
|
+
Hosted products (Firebase, Pusher, Ably) are deliberately absent. Measuring them
|
|
1894
|
+
honestly needs their accounts, regions and tiers, and a wrong number about
|
|
1895
|
+
someone else's product is worse than no number.
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
## The hard questions, and our answers
|
|
1899
|
+
|
|
1900
|
+
| the question | the answer here |
|
|
1901
|
+
| --- | --- |
|
|
1902
|
+
| Does a client learn that deliveries were missed? | Yes — `missed` is COMPUTED from per-origin serials against a watermark, never estimated |
|
|
1903
|
+
| Can a late arrival tell "nothing happened" from "I was not listening"? | Yes — every delivery carries `prior`, the watermark before it was accepted |
|
|
1904
|
+
| Is a subscription authorized, or only the connection? | Per subscription, on the routing key, re-checked per delivery |
|
|
1905
|
+
| Does a subscription outlive the credential that authorized it? | No — bounded by the credential's verified expiry, cookie AND bearer |
|
|
1906
|
+
| Does the link heal itself after a broker outage? | Yes — no restart, no app-side retry, no resubscribe |
|
|
1907
|
+
| Does a degraded network lose messages or only slow them? | Only slows them — 7x the median latency, zero loss |
|
|
1908
|
+
| Does fan-out cost grow with subscriber count? | No — 0.027 µs per delivery, flat from 1 to 100 |
|
|
1909
|
+
| Are channels typed, or strings? | Typed — a rename is a compile error |
|
|
1910
|
+
| Is a declared event nothing publishes reported? | Yes, at boot, reading sibling apps in the workspace |
|
|
1911
|
+
| Is cross-replica fan-out separated per app by default? | Yes — the namespace derives from the app name |
|
|
1912
|
+
|
|
1913
|
+
**Every row is enforced by a test** (`realtimeProperties.test.ts`) that fails if
|
|
1914
|
+
the proof behind it disappears. A property may not be claimed here without
|
|
1915
|
+
something in the repository that demonstrates it.
|
|
1916
|
+
|
|
1917
|
+
**Why this is not a benchmark against other products.** A table of our measured
|
|
1918
|
+
numbers beside someone else's published ones is not a comparison — it is two
|
|
1919
|
+
things in a row. Benchmarking a hosted competitor honestly needs their accounts,
|
|
1920
|
+
regions, tiers and retry policies, and a wrong number about someone else's
|
|
1921
|
+
product is worse than no number. What decides a choice anyway is not the
|
|
1922
|
+
microseconds; it is whether the system answers these questions at all. Each
|
|
1923
|
+
answer above is checkable against this repository by anyone, which is the
|
|
1924
|
+
opposite of a claim.
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
## What you can build — and what to reach for
|
|
1928
|
+
|
|
1929
|
+
| what you want to build | reach for |
|
|
1930
|
+
| --- | --- |
|
|
1931
|
+
| a list that updates as rows change | `useSubscription` |
|
|
1932
|
+
| a huge list without loading all of it | `useWindowedSubscription` |
|
|
1933
|
+
| an edit that appears before the server confirms | `useMutation` (optimistic is derived) |
|
|
1934
|
+
| a signal with no row behind it — a game start, a trigger | `defineEvent` |
|
|
1935
|
+
| a 60 Hz value where only the newest matters | `defineEvent` + `delivery: 'latest'` |
|
|
1936
|
+
| knowing a delivery was provably missed | `useEvent` → `missed` |
|
|
1937
|
+
| who is online in a room | `usePresence` |
|
|
1938
|
+
| who is typing right now | `useTyping` |
|
|
1939
|
+
| showing that the screen went stale | `useEvent` → `status`, or `useConnectionStatus` |
|
|
1940
|
+
| fan-out across replicas | `broadcastPlugin()` |
|
|
1941
|
+
| durable work with progress a client can watch | `useWorkflow` |
|
|
1942
|
+
| an in-app inbox | `useInbox` |
|
|
1943
|
+
| delivering an event to a third party | `@voltro/plugin-webhooks` |
|
|
1944
|
+
| gating who may subscribe | `guards:` on the event |
|
|
1945
|
+
| an upload whose progress the UI follows | `useUpload` |
|
|
1946
|
+
|
|
1947
|
+
This table is a TEST (`realtimeCapabilities.test.ts`), not a claim: each row
|
|
1948
|
+
asserts its primitive is still exported, so a capability that loses its
|
|
1949
|
+
primitive to a rename goes red in CI rather than being discovered by whoever
|
|
1950
|
+
tries to build it.
|
|
1951
|
+
|
|
1952
|
+
**Three of these are the ones people usually reach for wrongly.** A value that
|
|
1953
|
+
changes many times a second is an EVENT, not a row — writing it to a table wakes
|
|
1954
|
+
every subscriber of every query reading that table, and each pays a full re-diff.
|
|
1955
|
+
"Who is online" is presence rather than a table, because the answer is ephemeral
|
|
1956
|
+
and per-connection. And "did anything get lost" has a real answer (`missed`),
|
|
1957
|
+
computed rather than estimated, so you do not have to build a heartbeat of your
|
|
1958
|
+
own to find out.
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
## The four costs side by side
|
|
1962
|
+
|
|
1963
|
+
Every number below is asserted by a test in the repo, not quoted from a report —
|
|
1964
|
+
each surface has a `*.perf.test.ts` that prints what it measured.
|
|
1965
|
+
|
|
1966
|
+
| primitive | operation | cost | scales with |
|
|
1967
|
+
| --- | --- | --- | --- |
|
|
1968
|
+
| **Events** | `ctx.events.publish` | **4.3 µs** (~232 k/s) | nothing — flat |
|
|
1969
|
+
| **Events** | delivery to a subscriber | **0.027 µs** | subscribers, linearly and cheaply |
|
|
1970
|
+
| **Presence** | a heartbeat | **0.16 µs** | nothing — flat |
|
|
1971
|
+
| **Presence** | a roster read, 10 k members | **547 µs** | the ROOM |
|
|
1972
|
+
| **Records** | a live query re-diff, 5 000 rows | **3 062 µs** | the RESULT SET |
|
|
1973
|
+
| **Broadcast** | cross-replica, real Redis | **p50 1.1 ms · p99 11.2 ms** | the network |
|
|
1974
|
+
|
|
1975
|
+
**The comparison is the useful part.** Publishing an event costs about a
|
|
1976
|
+
thousandth of what re-diffing a large live query does, and that ratio — not
|
|
1977
|
+
either number — is what should decide between them. A value that changes at 60 Hz
|
|
1978
|
+
belongs in an event; the same value written to a table wakes every subscriber of
|
|
1979
|
+
every query reading it, and each pays the full walk.
|
|
1980
|
+
|
|
1981
|
+
**Two of the four are flat and two are not.** A publish and a heartbeat cost the
|
|
1982
|
+
same whatever the load, so they scale by adding replicas. A roster read is
|
|
1983
|
+
linear in the room and a live-query diff is linear in the RESULT SET — including
|
|
1984
|
+
when one column of one row changed, because the cost is the walk rather than the
|
|
1985
|
+
patch. Those are the two numbers to keep an eye on as an app grows.
|
|
1986
|
+
|
|
1987
|
+
**Cross-replica adds milliseconds, not microseconds**, and that is a network
|
|
1988
|
+
crossing rather than framework overhead. Under an injected 20 ms ± 10 jitter it
|
|
1989
|
+
becomes p50 26 ms / p99 89 ms; adding a 50 KB/s ceiling makes it p50 188 ms — and
|
|
1990
|
+
in every one of those runs, all 200 messages arrive. Degradation costs latency,
|
|
1991
|
+
never messages.
|
|
1992
|
+
|
|
1993
|
+
|
|
1799
1994
|
## Throughput — the numbers, and where this is the wrong primitive
|
|
1800
1995
|
|
|
1801
1996
|
Measured on one core, publish path only:
|
|
@@ -1924,6 +2119,30 @@ Three bugs in nine lines, and every consumer has to get all three right: the `se
|
|
|
1924
2119
|
|
|
1925
2120
|
Migrating is mechanical: declare the event, replace the insert with `ctx.events.publish`, replace the hook with `useEvent`, and drop the table.
|
|
1926
2121
|
|
|
2122
|
+
## What `defineEvent` refuses, and why
|
|
2123
|
+
|
|
2124
|
+
```ts
|
|
2125
|
+
defineEvent({ name: 'orders paid', … }) // ✗ whitespace — see below
|
|
2126
|
+
defineEvent({ name: 'orders.paid', guards: [], … }) // ✗ enforces nothing
|
|
2127
|
+
defineEvent({ name: 'x', webhook: { rateLimit: { perMinute: 0 } } }) // ✗ never delivers
|
|
2128
|
+
```
|
|
2129
|
+
|
|
2130
|
+
**Whitespace in a name is a broker-level failure, not a style rule.** The name
|
|
2131
|
+
becomes a broker SUBJECT segment. NATS refuses a subject containing whitespace
|
|
2132
|
+
and delivers nothing — with no error on the publishing side. An app that works
|
|
2133
|
+
on Redis therefore stops working when the transport changes, silently and in
|
|
2134
|
+
production only. Use a dot to namespace: `orders.paid`.
|
|
2135
|
+
|
|
2136
|
+
**`guards: []` is refused** because it reads at the call site as if the event
|
|
2137
|
+
were protected and enforces nothing — the empty list never reaches the check.
|
|
2138
|
+
Omit the field for an unguarded event.
|
|
2139
|
+
|
|
2140
|
+
**`rateLimit: { perMinute: 0 }`** defers every delivery forever. There is no
|
|
2141
|
+
"unlimited" spelling — omit `rateLimit` for no ceiling. **`version: 0`** would
|
|
2142
|
+
make a subscriber pinned to 1 read the event as *behind*, the opposite of what a
|
|
2143
|
+
bump is for.
|
|
2144
|
+
|
|
2145
|
+
|
|
1927
2146
|
## See also
|
|
1928
2147
|
|
|
1929
2148
|
[Subscriptions](/docs/data/subscriptions) · [Outbox](/docs/data/outbox) · [Subscribers](/docs/data/subscribers) · [Streams](/docs/data/streams)
|
|
@@ -196,6 +196,34 @@ registers refuses at boot rather than sitting there looking enforced.
|
|
|
196
196
|
orphan rows are cleaned up *before* the FK constraint is added — and has no
|
|
197
197
|
runtime semantics. Runtime behaviour comes from `onDelete`.
|
|
198
198
|
|
|
199
|
+
## Composing with a plugin's namespace
|
|
200
|
+
|
|
201
|
+
Sharing a namespace with a plugin already works: the collision check compares
|
|
202
|
+
FULL tags, so `notifications.list` of yours beside the plugin's
|
|
203
|
+
`notifications.inbox` is not a clash. Only an identical name is — two handlers
|
|
204
|
+
behind one tag is not something a caller can reason about.
|
|
205
|
+
|
|
206
|
+
To REPLACE one deliberately, declare it:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
// Adopt the plugin's namespace, add your own leaves beside it…
|
|
210
|
+
defineQuery({ name: 'notifications.archive', … })
|
|
211
|
+
|
|
212
|
+
// …and REPLACE just the one you need to behave differently.
|
|
213
|
+
defineMutation({ name: 'notifications.markRead', overridesPlugin: true, … })
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The plugin's route is dropped, not merely permitted alongside yours — permitting
|
|
217
|
+
both would leave two handlers bound, which is the state the check exists to
|
|
218
|
+
prevent. The boot logs which routes were replaced.
|
|
219
|
+
|
|
220
|
+
**Explicit, never inferred.** Letting your route win silently would mean a
|
|
221
|
+
plugin upgrade that adds a route could shadow one of yours with no diff to read.
|
|
222
|
+
It is also why the two obvious alternatives are worse: renaming your procedure,
|
|
223
|
+
or `alias`ing the whole plugin away, both move the split from a domain boundary
|
|
224
|
+
to "who built it" — for whoever calls the api, the worst possible partition.
|
|
225
|
+
|
|
226
|
+
|
|
199
227
|
## When NOT to write a plugin
|
|
200
228
|
|
|
201
229
|
- **One-off side effect** — just call it from the mutation directly.
|