toiljs 0.0.85 → 0.0.87

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.
Files changed (132) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +303 -293
  5. package/build/compiler/.tsbuildinfo +1 -1
  6. package/build/compiler/config.d.ts +2 -0
  7. package/build/compiler/config.js +1 -0
  8. package/build/compiler/docs.js +8 -24
  9. package/build/compiler/generate.js +4 -2
  10. package/build/compiler/index.d.ts +1 -1
  11. package/build/compiler/index.js +69 -6
  12. package/build/compiler/toil-docs.generated.js +64 -22
  13. package/build/devserver/.tsbuildinfo +1 -1
  14. package/build/devserver/analytics/index.js +7 -3
  15. package/build/devserver/db/database.d.ts +4 -0
  16. package/build/devserver/db/database.js +43 -1
  17. package/build/devserver/db/index.d.ts +1 -1
  18. package/build/devserver/db/index.js +1 -1
  19. package/build/devserver/db/types.d.ts +3 -0
  20. package/build/devserver/db/types.js +2 -0
  21. package/build/devserver/runtime/module.js +4 -1
  22. package/docs/README.md +104 -65
  23. package/docs/auth/README.md +102 -0
  24. package/docs/auth/configuration.md +94 -0
  25. package/docs/auth/extending.md +202 -0
  26. package/docs/auth/how-it-works.md +138 -0
  27. package/docs/auth/usage.md +188 -0
  28. package/docs/backend/README.md +143 -0
  29. package/docs/backend/data.md +351 -0
  30. package/docs/backend/rest.md +402 -0
  31. package/docs/backend/rpc.md +226 -0
  32. package/docs/background/README.md +114 -0
  33. package/docs/background/daemons.md +230 -0
  34. package/docs/background/derive.md +179 -0
  35. package/docs/cli/README.md +377 -0
  36. package/docs/concepts/config.md +416 -0
  37. package/docs/concepts/decorators.md +127 -0
  38. package/docs/concepts/security.md +108 -0
  39. package/docs/concepts/tiers.md +166 -0
  40. package/docs/concepts/types.md +216 -0
  41. package/docs/database/README.md +143 -0
  42. package/docs/database/capacity.md +350 -0
  43. package/docs/database/counters.md +174 -0
  44. package/docs/database/documents.md +255 -0
  45. package/docs/database/events.md +307 -0
  46. package/docs/database/membership.md +246 -0
  47. package/docs/database/setup.md +155 -0
  48. package/docs/database/unique.md +216 -0
  49. package/docs/database/views.md +246 -0
  50. package/docs/frontend/README.md +101 -0
  51. package/docs/frontend/data-fetching.md +243 -0
  52. package/docs/frontend/images.md +148 -0
  53. package/docs/frontend/metadata.md +344 -0
  54. package/docs/frontend/rendering.md +236 -0
  55. package/docs/frontend/routing.md +344 -0
  56. package/docs/frontend/scripts.md +118 -0
  57. package/docs/frontend/search.md +191 -0
  58. package/docs/frontend/styling.md +147 -0
  59. package/docs/getting-started/README.md +81 -0
  60. package/docs/getting-started/create-project.md +131 -0
  61. package/docs/getting-started/deploy.md +101 -0
  62. package/docs/getting-started/first-app.md +215 -0
  63. package/docs/getting-started/installation.md +106 -0
  64. package/docs/getting-started/migrating.md +125 -0
  65. package/docs/getting-started/project-structure.md +163 -0
  66. package/docs/introduction/README.md +41 -0
  67. package/docs/introduction/design-principles.md +55 -0
  68. package/docs/introduction/distributed.md +74 -0
  69. package/docs/introduction/how-it-works.md +74 -0
  70. package/docs/introduction/hyperscale.md +51 -0
  71. package/docs/introduction/modern-stack.md +36 -0
  72. package/docs/introduction/vs-other-frameworks.md +48 -0
  73. package/docs/introduction/why-toil.md +93 -0
  74. package/docs/llms.txt +90 -0
  75. package/docs/realtime/README.md +102 -0
  76. package/docs/realtime/channels.md +211 -0
  77. package/docs/realtime/streams.md +369 -0
  78. package/docs/services/README.md +60 -0
  79. package/docs/services/analytics.md +268 -0
  80. package/docs/services/caching.md +175 -0
  81. package/docs/services/cookies.md +235 -0
  82. package/docs/services/crypto.md +209 -0
  83. package/docs/services/email.md +289 -0
  84. package/docs/services/environment.md +141 -0
  85. package/docs/services/ratelimit.md +174 -0
  86. package/docs/services/time.md +85 -0
  87. package/examples/basic/client/routes/analytics.tsx +13 -12
  88. package/examples/basic/server/models/SiteAnalytics.ts +29 -17
  89. package/examples/basic/server/routes/Analytics.ts +15 -17
  90. package/examples/basic/server/routes/UserId.ts +22 -0
  91. package/package.json +15 -11
  92. package/scripts/gen-toil-docs.mjs +24 -35
  93. package/server/auth/AuthController.ts +336 -0
  94. package/server/auth/AuthUser.ts +23 -0
  95. package/server/auth/index.ts +16 -0
  96. package/server/globals/auth.ts +31 -0
  97. package/server/globals/userid.ts +107 -0
  98. package/src/compiler/config.ts +13 -0
  99. package/src/compiler/docs.ts +16 -33
  100. package/src/compiler/generate.ts +6 -2
  101. package/src/compiler/index.ts +114 -6
  102. package/src/compiler/toil-docs.generated.ts +64 -22
  103. package/src/devserver/analytics/index.ts +10 -4
  104. package/src/devserver/db/database.ts +67 -1
  105. package/src/devserver/db/index.ts +1 -0
  106. package/src/devserver/db/types.ts +13 -0
  107. package/src/devserver/runtime/module.ts +7 -0
  108. package/test/analytics-dev.test.ts +2 -1
  109. package/test/devserver-database.test.ts +113 -0
  110. package/docs/auth-todo.md +0 -149
  111. package/docs/auth.md +0 -322
  112. package/docs/caching.md +0 -115
  113. package/docs/cli.md +0 -17
  114. package/docs/client.md +0 -39
  115. package/docs/cookies.md +0 -457
  116. package/docs/crypto.md +0 -130
  117. package/docs/daemon.md +0 -123
  118. package/docs/data.md +0 -131
  119. package/docs/derive.md +0 -159
  120. package/docs/email.md +0 -326
  121. package/docs/environment.md +0 -97
  122. package/docs/getting-started.md +0 -128
  123. package/docs/index.md +0 -30
  124. package/docs/ratelimit.md +0 -95
  125. package/docs/routing.md +0 -259
  126. package/docs/rpc.md +0 -149
  127. package/docs/server.md +0 -61
  128. package/docs/ssr.md +0 -632
  129. package/docs/streams.md +0 -178
  130. package/docs/styling.md +0 -22
  131. package/docs/tiers.md +0 -133
  132. package/docs/time.md +0 -43
@@ -0,0 +1,268 @@
1
+ # Analytics
2
+
3
+ Read your site's own usage numbers (requests, bytes, cache hits, database ops, errors, and more) straight from a route, with no extra code and no third-party service.
4
+
5
+ Reach for analytics to build a status or usage dashboard for a site: how much traffic it serves, how effective its cache is, how many database operations it runs. It is **infrastructure metering per domain**, not per-user product analytics. There are no custom events; you read a fixed catalog of counters the edge already keeps for you.
6
+
7
+ `Analytics` is an **ambient global**: use it with no import, like [`crypto`](./crypto.md) or `Time`.
8
+
9
+ ## What "metering" means
10
+
11
+ Every time the edge (the Dacely server running your code) serves a request, runs a database operation, opens a stream, or sends an email, it **counts** that into a set of per-domain counters. Those counters live in the same worldwide, distributed database that backs [ToilDB](../database/README.md), so the total for your site is correct across every edge location, not just the one machine that happened to serve a request.
12
+
13
+ `Analytics.self()` reads a **snapshot** of your site's current counter values. You do not record anything yourself; the numbers are already there.
14
+
15
+ ```mermaid
16
+ flowchart LR
17
+ R["Every request, DB op,<br/>stream, email"] --> C["Edge counts it into<br/>per-domain counters"]
18
+ C --> D[("Distributed counters,<br/>worldwide")]
19
+ D --> S["Analytics.self()<br/>reads a snapshot"]
20
+ S --> Y["Your route returns<br/>the numbers"]
21
+ ```
22
+
23
+ ## Read your site's stats
24
+
25
+ `Analytics.self()` returns a `TenantStats` object. Every counter is a **named getter** that returns a `u64` (an unsigned 64-bit integer, always zero or positive), so mapping the numbers into a response needs no casts at all.
26
+
27
+ ```ts
28
+ import { RouteContext } from 'toiljs/server/runtime';
29
+
30
+ @rest('metrics')
31
+ class Metrics {
32
+ @get('/')
33
+ public overview(ctx: RouteContext): string {
34
+ const s = Analytics.self();
35
+ // Every getter is a u64. cacheRatio is the one f64 (a fraction 0..1).
36
+ return `requests=${s.requests} cacheHits=${s.cacheHits} ratio=${s.cacheRatio}`;
37
+ }
38
+ }
39
+ ```
40
+
41
+ You can also read any counter by its numeric id with `s.metric(MetricId.Requests)`, which returns `0` for an unknown id. The named getters are preferred: they are self-documenting and there are no magic strings to mistype.
42
+
43
+ ### The counter catalog
44
+
45
+ Every getter below is a lifetime running total: it only ever goes up, and reading it never resets it. They are grouped by area. (`MetricId` is the matching numeric id, an ambient enum you can use with no import.)
46
+
47
+ **Requests and the L1 edge**
48
+
49
+ | Getter | What it counts |
50
+ | --- | --- |
51
+ | `requests` | HTTP requests served |
52
+ | `bytesOutL1` | Response bytes sent |
53
+ | `bytesInL1` | Request bytes received |
54
+ | `status2xx` | 2xx responses (success) |
55
+ | `status3xx` | 3xx responses (redirects) |
56
+ | `status4xx` | 4xx responses (client errors) |
57
+ | `status5xx` | 5xx responses (server errors) |
58
+ | `staticHits` | Static assets served without running your code |
59
+ | `wasmDispatches` | Times your compiled handler ran |
60
+ | `executorFullRejects` | Requests rejected because the worker pool was full |
61
+ | `unknownHostRejects` | Requests rejected for an unknown host |
62
+ | `rateLimitedRejects` | Requests rejected by rate limiting |
63
+ | `gasUsed` | Total compute "gas" your handlers consumed |
64
+
65
+ **Database (ToilDB)**
66
+
67
+ | Getter | What it counts |
68
+ | --- | --- |
69
+ | `dbOps` | Database operations issued |
70
+ | `dbReads` | Read operations |
71
+ | `dbWrites` | Write operations |
72
+ | `dbErrors` | Operations that errored |
73
+ | `dbLatencyNsSum` | Summed operation latency, in nanoseconds |
74
+ | `meanDbLatencyNs` | Derived: `dbLatencyNsSum / dbOps` (0 with no ops) |
75
+
76
+ **Streams (WebTransport realtime)**
77
+
78
+ | Getter | What it counts |
79
+ | --- | --- |
80
+ | `streamAccepts` | Stream connections accepted |
81
+ | `streamRejectWrongNode` | Streams that reached the wrong node |
82
+ | `streamRejectCapacity` | Streams rejected for capacity |
83
+ | `streamRejectArtifact` | Streams rejected for a missing or invalid build |
84
+ | `streamRejectGuest` | Streams your code rejected |
85
+ | `streamTraps` | Stream handler crashes |
86
+ | `streamIdleTimeouts` | Streams closed for being idle |
87
+ | `streamBytesIn` / `streamBytesOut` | Bytes received / sent on streams |
88
+ | `streamBackpressureEvents` | Times a stream had to slow down |
89
+ | `streamCloses` | Clean closes |
90
+ | `streamDisconnects` | Abrupt disconnects |
91
+
92
+ **Daemons (L4 background jobs)**
93
+
94
+ | Getter | What it counts |
95
+ | --- | --- |
96
+ | `daemonStarts` / `daemonStartFailures` | Daemon starts / failed starts |
97
+ | `daemonTicksFired` | Scheduled ticks that ran |
98
+ | `daemonTicksSkippedNotLeader` | Ticks skipped because this node was not the leader |
99
+ | `daemonTicksFailed` | Ticks that failed |
100
+ | `daemonLeaderAcquires` / `daemonLeaderFenced` | Leadership gained / lost |
101
+ | `daemonHttpCallAttempts` / `daemonHttpCallFailures` | Outbound HTTP calls attempted / failed |
102
+
103
+ **Memory, email, and cache**
104
+
105
+ | Getter | What it counts |
106
+ | --- | --- |
107
+ | `memGrownBytes` | Wasm memory grown, in bytes |
108
+ | `emails` | Emails sent |
109
+ | `cacheHits` | Responses served from the edge cache |
110
+ | `cacheMisses` | Cacheable responses that missed the cache |
111
+ | `cacheRatio` | Derived **`f64`**: `cacheHits / (cacheHits + cacheMisses)`, a fraction from 0 to 1 (0 when there were no cacheable responses) |
112
+
113
+ ### Live gauges and request windows
114
+
115
+ A few fields are not lifetime totals. They still read as `u64`.
116
+
117
+ **Live gauges** are the current level right now, not a running total:
118
+
119
+ - `connectedStreams`: streams connected at this moment.
120
+ - `committedMemory`: wasm memory committed right now, in bytes.
121
+
122
+ **Request windows** show your current rate-limit usage against your plan cap (a cap of `0` means unlimited):
123
+
124
+ - `reqMinuteUsed` / `reqMinuteCap`: requests used and the cap for the current 1-minute window.
125
+ - `reqDayUsed` / `reqDayCap`: requests used and the cap for the current 24-hour window.
126
+
127
+ There is also `nowMs`, the edge wall-clock time (in milliseconds) when the snapshot was taken, so you can show "as of" and compute how fresh the numbers are.
128
+
129
+ ## Time series (for graphs)
130
+
131
+ The totals above are single numbers. To draw a graph over time, use `Analytics.series(metric, range)`, which returns a `Series`: a list of per-bucket totals from oldest to newest.
132
+
133
+ ```ts
134
+ @get('/requests-24h')
135
+ public requests24h(ctx: RouteContext): string {
136
+ const s = Analytics.series(MetricId.Requests, AnalyticsRange.H24);
137
+ // s.points -> per-bucket totals, oldest to newest
138
+ // s.bucketSecs -> seconds each bucket covers
139
+ // s.headMs -> end time (ms) of the newest bucket
140
+ // s.ratePerSec(i) -> requests/second for bucket i (points[i] / bucketSecs)
141
+ return `buckets=${s.points.length} bucketSecs=${s.bucketSecs}`;
142
+ }
143
+ ```
144
+
145
+ `AnalyticsRange` picks the window and the resolution. Short ranges use per-minute buckets for a smooth line; the rest use per-hour buckets (kept for 30 days):
146
+
147
+ | Range | Span | Bucket width |
148
+ | --- | --- | --- |
149
+ | `H1` | 1 hour | 1 minute |
150
+ | `H6` | 6 hours | 1 minute |
151
+ | `H12` | 12 hours | 1 hour |
152
+ | `H24` | 24 hours | 1 hour |
153
+ | `D3` | 3 days | 1 hour |
154
+ | `D7` | 7 days | 1 hour |
155
+ | `D14` | 14 days | 1 hour |
156
+ | `D30` | 30 days | 1 hour |
157
+
158
+ The series stores **totals per bucket**, never rates. A rate is derived: `ratePerSec(i)` divides bucket `i` by its width for you, or you can compute `points[i] / bucketSecs` yourself. Most metrics you will chart are counters (`MetricId.Requests`, `MetricId.CacheHits`, and so on).
159
+
160
+ ## Permissions: who can read what
161
+
162
+ - **Your own site.** `Analytics.self()` and `Analytics.series(...)` always read the site the request landed on. The calling domain is decided by the edge; your code cannot forge it.
163
+ - **Cross-site reads are for `dacely.com` only.** The privileged `dacely.com` domain (the platform dashboard) may read any site with `Analytics.site(domain)`, `Analytics.siteSeries(domain, ...)`, and `Analytics.listSites(...)`. For any other caller, or an unknown domain, `site` and `siteSeries` return `null` and `listSites` returns an empty page. There is no way for a normal site to read another site's numbers.
164
+
165
+ `Analytics.listSites(cursor, limit)` enumerates site names for the dashboard, one page at a time. It returns a `SiteList` with `sites: string[]` and `hasMore: bool`. When `hasMore` is true, pass the last name back as the next `cursor` to get the following page.
166
+
167
+ ```ts
168
+ // Only meaningful when the calling domain is dacely.com.
169
+ const page = Analytics.listSites('', 100); // first 100 site names
170
+ for (let i = 0; i < page.sites.length; i++) {
171
+ const other = Analytics.site(page.sites[i]); // TenantStats | null
172
+ if (other != null) {
173
+ // read other.requests, other.cacheRatio, ...
174
+ }
175
+ }
176
+ ```
177
+
178
+ ## Worked example: return everything
179
+
180
+ This route reads the full snapshot and maps it into a typed response. Mapping is mechanical: every getter is a `u64` and every response field is a `u64`, so there are no casts. The example groups a representative field from each area; add the rest of the getters from the catalog above the same way.
181
+
182
+ ```ts
183
+ import { RouteContext } from 'toiljs/server/runtime';
184
+
185
+ /** The shape returned to the client. All fields are u64 except the one ratio. */
186
+ @data
187
+ export class UsageReport {
188
+ // requests / L1
189
+ requests: u64 = 0;
190
+ bytesOut: u64 = 0;
191
+ status2xx: u64 = 0;
192
+ status5xx: u64 = 0;
193
+ gasUsed: u64 = 0;
194
+ // database
195
+ dbOps: u64 = 0;
196
+ dbErrors: u64 = 0;
197
+ meanDbLatencyNs: u64 = 0;
198
+ // cache
199
+ cacheHits: u64 = 0;
200
+ cacheMisses: u64 = 0;
201
+ cacheRatio: f64 = 0;
202
+ // live gauges (current level, not a total)
203
+ connectedStreams: u64 = 0;
204
+ committedMemory: u64 = 0;
205
+ // request windows (cap 0 = unlimited)
206
+ requestsThisMinute: u64 = 0;
207
+ requestsThisMinuteCap: u64 = 0;
208
+ requestsToday: u64 = 0;
209
+ requestsTodayCap: u64 = 0;
210
+ // when the snapshot was read (edge ms)
211
+ nowMs: u64 = 0;
212
+ }
213
+
214
+ @rest('usage')
215
+ class Usage {
216
+ @get('/')
217
+ public report(ctx: RouteContext): UsageReport {
218
+ const s = Analytics.self();
219
+ const out = new UsageReport();
220
+
221
+ out.requests = s.requests;
222
+ out.bytesOut = s.bytesOutL1;
223
+ out.status2xx = s.status2xx;
224
+ out.status5xx = s.status5xx;
225
+ out.gasUsed = s.gasUsed;
226
+
227
+ out.dbOps = s.dbOps;
228
+ out.dbErrors = s.dbErrors;
229
+ out.meanDbLatencyNs = s.meanDbLatencyNs;
230
+
231
+ out.cacheHits = s.cacheHits;
232
+ out.cacheMisses = s.cacheMisses;
233
+ out.cacheRatio = s.cacheRatio;
234
+
235
+ out.connectedStreams = s.connectedStreams;
236
+ out.committedMemory = s.committedMemory;
237
+
238
+ out.requestsThisMinute = s.reqMinuteUsed;
239
+ out.requestsThisMinuteCap = s.reqMinuteCap;
240
+ out.requestsToday = s.reqDayUsed;
241
+ out.requestsTodayCap = s.reqDayCap;
242
+
243
+ out.nowMs = s.nowMs;
244
+ return out;
245
+ }
246
+ }
247
+ ```
248
+
249
+ Because this is a `@data` return type, the client gets a fully typed result from `Server.REST.usage.report()`. See [structured data types](../backend/data.md) for how `@data` works.
250
+
251
+ ## Local dev behavior
252
+
253
+ Under `toiljs dev`, the real per-domain metering does not exist (there is only one machine and no fleet), so the dev server returns a **fixed sample** `TenantStats` and a gentle synthetic ramp for `series(...)`. That lets you build and style a dashboard locally. Dev is also permissive: it returns sample data for any domain, because the real `dacely.com`-only check is enforced on the edge. Your code is unchanged; only the numbers differ.
254
+
255
+ ## Gotchas and limits
256
+
257
+ - **Totals never reset.** The counter getters are lifetime running totals. To see "requests in the last hour", use `series(...)` and sum the buckets, not the totals.
258
+ - **Values are `u64`.** Map them into `u64` fields with no casts. The only non-integer field is `cacheRatio` (and `Series.ratePerSec`), which are `f64`.
259
+ - **Not per-user analytics.** These are infrastructure counters per domain. There is no way to record a custom event or attribute a number to a specific user.
260
+ - **Cross-site reads need `dacely.com`.** `Analytics.site(...)` returns `null` for everyone else. Do not build a feature that reads another site's stats.
261
+ - **Snapshots, not live streams.** Each call reads the current values once. `nowMs` tells you when.
262
+
263
+ ## Related
264
+
265
+ - [Caching](./caching.md), the source of `cacheHits`, `cacheMisses`, and `cacheRatio`.
266
+ - [ToilDB database](../database/README.md), the source of the `db*` counters and where these counters are stored.
267
+ - [Structured data types](../backend/data.md), for the `@data` return type in the worked example.
268
+ - [Compute tiers](../concepts/tiers.md), for what "L1", "streams", and "daemons" mean in the catalog.
@@ -0,0 +1,175 @@
1
+ # Caching
2
+
3
+ **Caching lets the edge keep a copy of a response and hand it back instantly, without re-running your code.** You opt in per route, either with the `@cache` decorator or by calling `Response.cache(...)`.
4
+
5
+ ## What "cache" means here
6
+
7
+ A **cache** is a fast, temporary store of a previous answer. When a request comes in, the edge can check: "have I already computed this exact response recently?" If yes, that is a **cache hit**: it returns the saved copy in microseconds and never wakes up your WebAssembly program. If no, that is a **cache miss**: it runs your handler, sends the result, and (if you asked it to) saves a copy for next time.
8
+
9
+ There are two places a response can be cached:
10
+
11
+ - **The edge cache** is shared: one saved copy on an edge server answers many users. This is where the big speedups come from.
12
+ - **The browser cache** is private to one user's browser. You control it with a standard `Cache-Control` header.
13
+
14
+ You can use one or both.
15
+
16
+ ## When to cache (and when not)
17
+
18
+ Cache a response only when it is the **same for everyone** and safe to reuse for a little while:
19
+
20
+ - Good: a public leaderboard, a product catalog, a blog index, an exchange-rate snapshot, a "server status" endpoint.
21
+ - Bad: anything personalized (a user's dashboard, their cart, "hello, Alice"). If you cached that, one user could be served another user's data. The edge has strong safety rails against this (below), but the simplest rule is: **do not cache per-user data on the shared edge.**
22
+
23
+ Caching is **always opt-in**. A response you do not mark is never stored. There is no "cache every GET" mode, because the edge cannot tell a personalized response from a public one on its own.
24
+
25
+ ## How: the `@cache` decorator
26
+
27
+ A **decorator** is an annotation you put right above a route method. `@cache` tells the edge how long it may reuse the response. The numbers are positional:
28
+
29
+ ```ts
30
+ import { Response } from 'toiljs/server/runtime';
31
+
32
+ @rest('leaderboard')
33
+ class Leaderboard {
34
+ @cache(60) // 60 MINUTES at the edge
35
+ @get('/')
36
+ public top(): Response {
37
+ // ...expensive query...
38
+ return Response.json(standings);
39
+ }
40
+ }
41
+ ```
42
+
43
+ The full form is `@cache(edgeTtlMinutes, browserTtlSeconds?, privateScope?, allowAuth?)`:
44
+
45
+ ```ts
46
+ @cache(60) // 60 minutes at the edge, browser does not cache
47
+ @cache(60, 300) // + tell the browser to cache for 5 minutes (300s)
48
+ @cache(60, 300, true) // + mark it "private": browser only, never the shared edge
49
+ @cache(60, 300, true, true) // + allow caching even for logged-in requests (see rails)
50
+ ```
51
+
52
+ **TTL** means "time to live": how long a saved copy stays fresh before the edge throws it away and recomputes. Notice the units differ on purpose: the edge TTL is in **minutes**, the browser TTL is in **seconds**.
53
+
54
+ Every argument must be a plain number or `true`/`false` literal. If you pass something computed (a variable, a function call), the decorator safely degrades to "not cached" instead of misbehaving.
55
+
56
+ ## How: `Response.cache(...)`
57
+
58
+ `@cache` is just a shorthand. You can do the same thing by hand on any `Response`, which is handy when you decide at runtime:
59
+
60
+ ```ts
61
+ public cache(
62
+ edgeTtlMinutes: u16,
63
+ browserTtlSeconds: u32 = 0,
64
+ privateScope: bool = false,
65
+ allowAuth: bool = false,
66
+ ): Response
67
+ ```
68
+
69
+ ```ts
70
+ return Response.json(body).cache(60, 300);
71
+ ```
72
+
73
+ `cache(...)` returns the same `Response`, so it chains. There is also a shorthand for "edge only, no browser caching":
74
+
75
+ ```ts
76
+ return Response.bytes(blob).cacheFor(5); // edge-cache 5 minutes
77
+ ```
78
+
79
+ Under the hood, both set one header, `Dacely-Cache-Control`, that the edge reads and then strips (it never reaches the client).
80
+
81
+ ## The parameters, in detail
82
+
83
+ | Parameter | What it does |
84
+ | --- | --- |
85
+ | `edgeTtlMinutes` | How long the shared edge may serve the saved copy. Clamped to a 24-hour maximum, no matter what you pass. `0` means no edge caching. |
86
+ | `browserTtlSeconds` | The `max-age` sent to the browser. `0` (default) means the browser is told not to cache. |
87
+ | `privateScope` | Marks the response `private`: it may only live in the browser's own cache, never the shared edge or a CDN. |
88
+ | `allowAuth` | Permits caching a response to a logged-in request. Off by default (see safety rails). |
89
+
90
+ ## A hit vs a miss, visually
91
+
92
+ ```mermaid
93
+ flowchart TD
94
+ R["Request arrives<br/>at the edge"] --> Q{"Saved copy<br/>still fresh?"}
95
+ Q -->|Yes: cache HIT| H["Return the saved response<br/>(microseconds, no code runs)"]
96
+ Q -->|No: cache MISS| M["Run your WebAssembly handler"]
97
+ M --> D{"Marked cacheable<br/>and eligible?"}
98
+ D -->|Yes| S["Save a copy for the TTL,<br/>then return it"]
99
+ D -->|No| X["Return it, save nothing"]
100
+ ```
101
+
102
+ Every response the edge sends carries a `Dacely-Cache` header so you can see what happened while debugging:
103
+
104
+ | `Dacely-Cache` value | Meaning |
105
+ | --- | --- |
106
+ | `HIT` | Served from the cache; your code did not run. |
107
+ | `MISS` | Your code ran, and the response was saved (the next identical request should `HIT`). |
108
+ | `DYNAMIC` | Your code ran, and the response was not cached (no directive, or not eligible). |
109
+ | `BYPASS` | A fallback path (for example the compute layer was momentarily unavailable); not a cached answer. |
110
+
111
+ ## Safety rails (why cross-user leaks cannot happen)
112
+
113
+ The edge refuses to store certain responses even if you asked it to. These rules exist because a shared cache is exactly where one user's data could accidentally leak to another:
114
+
115
+ - **5xx responses are never cached.** A server error is temporary; caching a `500` would keep serving the failure for the whole TTL. (2xx, 3xx, and 4xx are cacheable: a redirect or a `404`/`410` is a predictable function of the request.)
116
+ - **A response with a `Set-Cookie` is never cached.** A cookie is per-user state.
117
+ - **A response to a logged-in request is not cached** unless you pass `allowAuth = true`. This stops one user's personalized response from being handed to someone else. Even with `allowAuth = true`, such an entry is only ever served back to other **authenticated** requests, never to an anonymous one (which would skip the auth check entirely).
118
+ - **The edge TTL is clamped to 24 hours.**
119
+
120
+ The cache is also keyed precisely: an entry is identified by the **host, method, path, and the exact request body**. So a `POST` with body `A` never returns the copy saved for body `B`, and a `GET` never returns a `POST`'s copy.
121
+
122
+ Because auth checks and body decoding run **before** the cache directive is applied, an unauthorized request is rejected with `401` before anything is stored, and a cached copy only ever comes from a handler that actually ran successfully.
123
+
124
+ ## ETag, Last-Modified, and 304 (static files)
125
+
126
+ The `@cache` decorator above is for **dynamic** responses (ones your code computes). Your project's **static files** (the built HTML, JS, CSS, and images) get a second, automatic layer of caching that you do not configure. It is worth understanding because it is what makes repeat page loads cheap.
127
+
128
+ Every static file the edge serves carries three headers:
129
+
130
+ - **`ETag`**: a short fingerprint of the file's contents (a "version tag"). toiljs uses a *weak* ETag (it starts with `W/`), which survives compression by a CDN in front of the edge.
131
+ - **`Last-Modified`**: when the file last changed.
132
+ - **`Cache-Control`**: content-hashed assets (files whose name contains their content hash, like `app-CfvHW67Y.js`) get `immutable` with a one-year max-age, because the URL itself changes whenever the content does. Everything else (HTML, non-hashed assets) gets `must-revalidate`, so the browser always checks freshness.
133
+
134
+ **Revalidation** is the trick. Instead of re-downloading a file it already has, a browser asks "has this changed?" by echoing the tag back in an `If-None-Match` header (or the date in `If-Modified-Since`). If the file is unchanged, the edge replies **`304 Not Modified`** with an empty body, and the browser reuses its copy. A `304` is tiny and fast.
135
+
136
+ ```mermaid
137
+ sequenceDiagram
138
+ participant B as Browser
139
+ participant E as Dacely edge
140
+ B->>E: GET /app.js<br/>If-None-Match: W/"1a2b-c3d4"
141
+ alt file unchanged
142
+ E-->>B: 304 Not Modified<br/>(no body)
143
+ else file changed
144
+ E-->>B: 200 OK<br/>(new bytes + new ETag)
145
+ end
146
+ ```
147
+
148
+ The precedence follows the HTTP spec (RFC 7232): if the request has `If-None-Match`, that decides it; only when it is absent does `If-Modified-Since` apply. You get all of this for free; there is nothing to turn on.
149
+
150
+ ## Where cached bodies live (memory bounds)
151
+
152
+ You do not need this to use caching, but it explains the limits you may hit.
153
+
154
+ The edge response cache is bounded and hard-capped so it can never exhaust a node's memory:
155
+
156
+ - **RAM tier**: small, short-lived responses live in memory, within a bounded budget (on the order of ~128 MB) plus a cap on the number of entries. A single response over ~256 KB does not go in RAM.
157
+ - **Disk tier (optional)**: when the operator enables an on-disk spill directory, a **big** response (over ~256 KB) or a **long-lived** one (TTL of 10 minutes or more) is written to disk and served back with near-zero RAM, the same way static files are. If disk spill is not enabled, a big response is simply not cached (you will see `Dacely-Cache: DYNAMIC`).
158
+
159
+ Expired entries are dropped on read (a stale entry is treated as a miss) and reclaimed when the cache needs room. Nothing survives a process restart.
160
+
161
+ ## Gotchas
162
+
163
+ - **Units differ.** Edge TTL is in minutes; browser TTL is in seconds. `@cache(60)` is one hour at the edge, not one minute.
164
+ - **Do not cache personalized data on the shared edge.** If a response depends on who is asking, either do not cache it, or use `privateScope` so it is browser-only.
165
+ - **A `Set-Cookie` disables edge caching for that response.** If you set a cookie, that response will not be edge-cached, by design.
166
+ - **Very large or slow-to-change bodies may not be cached** unless disk spill is enabled on the node. Check the `Dacely-Cache` header if a response you expected to cache shows `DYNAMIC`.
167
+ - **Nothing persists across restarts.** The cache is a speed optimization, never a source of truth.
168
+
169
+ ## Related
170
+
171
+ - [Rate limiting](./ratelimit.md): protect a route before it runs.
172
+ - [Analytics](./analytics.md): see your cache hit ratio per site.
173
+ - [Cookies](./cookies.md): why a `Set-Cookie` opts a response out of edge caching.
174
+ - [Auth, sessions, and `@user`](../auth/usage.md): why logged-in responses are not cached by default.
175
+ - [Every decorator](../concepts/decorators.md).