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,114 @@
1
+ # Background work
2
+
3
+ Background work is code that runs **without a user waiting for it**: on a timer, once globally, or automatically after your data changes. toiljs gives you two tools for it, `@daemon` and `@derive`, and this page helps you pick the right one.
4
+
5
+ ## Why background work
6
+
7
+ Most of your server code runs **because a user asked**: a browser hits a [route](../backend/rest.md) or calls an [RPC](../backend/rpc.md), your handler runs, and a response goes back. But some work does not belong on that path:
8
+
9
+ - It should happen **on a schedule** (every hour, every night at 2am), not when a request happens to arrive.
10
+ - It would be **too slow** to do inside a request, so you want it precomputed and ready.
11
+ - It must happen **exactly once across the whole world**, not once per user and not once per server.
12
+
13
+ That is background work. In toiljs there are two kinds, and they solve two different problems.
14
+
15
+ ## The two tools
16
+
17
+ ```mermaid
18
+ flowchart TD
19
+ Q{What do you need?}
20
+ Q -->|Run on a timer, or once globally| D["@daemon + @scheduled<br/>a single background worker"]
21
+ Q -->|Keep a fast read-view in sync with your data| V["@derive<br/>maintains a materialized View"]
22
+ D --> DD["e.g. nightly cleanup, hourly poll,<br/>periodic rollup"]
23
+ V --> VV["e.g. a leaderboard, a 'latest 10' feed,<br/>a home-page summary"]
24
+ ```
25
+
26
+ ### `@daemon`: a scheduled, global worker
27
+
28
+ A [daemon](./daemons.md) is one long-lived background worker for your whole app. It runs on a **schedule** you set (an interval like every 5 minutes, or a cron time like "9:15 on weekdays"), and there is exactly **one** of it worldwide at any moment (a "singleton"). Use it for work that is driven by **time** or that must run **once globally**:
29
+
30
+ - clean up stale rows every night;
31
+ - poll an external API every few minutes;
32
+ - send a daily digest email;
33
+ - roll up yesterday's numbers into a summary.
34
+
35
+ ### `@derive`: keep a read-view up to date
36
+
37
+ A [derive](./derive.md) is not on a timer. It runs **automatically whenever the data it depends on changes**, and its job is to keep a precomputed **View** (a read-optimized copy of your data) fresh. Use it when a page needs data that is **expensive to compute on every read**, like a leaderboard or a "latest 10 comments" list, so the read itself stays a single cheap lookup:
38
+
39
+ - fold an [events](../database/events.md) log into a "latest N" list;
40
+ - total up [counters](../database/counters.md) into a scoreboard;
41
+ - assemble a home-page summary from several sources.
42
+
43
+ ## Which one do I reach for?
44
+
45
+ | Question | Use |
46
+ | ----------------------------------------------------- | ------------ |
47
+ | "Run this every hour / at midnight." | `@daemon` |
48
+ | "Do this once for the whole app, not per server." | `@daemon` |
49
+ | "Poll or call an outside service on a schedule." | `@daemon` |
50
+ | "This page's data is too slow to compute per request."| `@derive` |
51
+ | "Keep a leaderboard / feed in sync as data changes." | `@derive` |
52
+
53
+ A simple rule of thumb: if the trigger is **the clock**, use a `@daemon`. If the trigger is **a change to your data** and the goal is a fast read, use a `@derive`.
54
+
55
+ They also combine well. A `@daemon` might do a heavy nightly aggregation and write a summary row, while a `@derive` keeps a small live view fresh on every write. They are different tiers of the edge and are covered on their own pages.
56
+
57
+ ## `@job`: the widest database surface for background work
58
+
59
+ `@daemon` and `@derive` decide **where and when** background code runs. `@job` answers a different question: **what a function is allowed to do to the database.** It is one of the ToilDB **function kinds** (the same family as `@query`, `@action`, and `@derive`), and it grants the **widest** data surface of them all.
60
+
61
+ A **function kind** is a label the compiler puts on a backend function to gate which database operations it may issue. This is a safety rail: a read-only endpoint physically cannot write, and an expensive scan cannot run on the hot request path. The kinds line up from narrowest to widest:
62
+
63
+ | Kind | Typical trigger | Point reads | **Scan** (`latest`, membership `list`) | Writes | `publish` a View |
64
+ | ---------- | ------------------------- | ----------- | -------------------------------------- | ----------------- | ---------------- |
65
+ | `@query` | a `@get` / plain `@remote`| yes | no | no | no |
66
+ | `@action` | a `@post` / `@action` | yes | no | yes (bounded) | no |
67
+ | `@derive` | your data changed | yes | yes | append / counter add only | yes |
68
+ | `@job` | you drive it (background) | yes | yes | yes (all) | yes |
69
+
70
+ A **scan** is a read that can fan out across many rows (like "the newest 50 events" via `events.latest`, or "every member of this set" via `membership.list`). Scans are **barred from request handlers** because a request must stay fast and bounded. `@derive` and `@job` run **off the request path**, so they are the only kinds allowed to scan.
71
+
72
+ ### When to reach for `@job`
73
+
74
+ Use `@job` when a piece of background work needs **more** database power than the default:
75
+
76
+ - it must **scan** (fold `events.latest`, walk `membership.list`), and/or
77
+ - it must **publish a View** *while also* doing arbitrary writes (`create`, `patch`, `delete`). A `@derive` can publish a View but cannot `patch` or `delete` a record; a `@job` can do everything.
78
+
79
+ The compiler accepts `@job` on a method, including a `@daemon`'s `@scheduled` method. Tag a scheduled task `@job` when it needs that full surface (say, a nightly repair that scans a log, fixes rows, and republishes a summary View). A plain, **untagged** `@scheduled` method runs with the **`@action`** surface instead: point reads plus bounded writes, which is all most rollups and cleanups need.
80
+
81
+ ```ts
82
+ @daemon
83
+ class Jobs {
84
+ // A plain scheduled method: point reads + bounded writes (the @action surface).
85
+ @scheduled('1h')
86
+ rollup(): void { /* get a counter, patch a summary row */ }
87
+
88
+ // A scheduled method that also needs SCANS and to PUBLISH a View: tag it @job.
89
+ @scheduled('1d')
90
+ @job
91
+ nightlyRepair(): void {
92
+ // @job unlocks scan-class reads (events.latest / membership.list)
93
+ // and publishing a View, on top of ordinary reads and writes.
94
+ }
95
+ }
96
+ ```
97
+
98
+ ### `@job` versus `@derive`
99
+
100
+ They overlap (both run off the request path, both may scan, both may publish a View), but they are triggered differently and sized differently:
101
+
102
+ - **`@derive`** is **change-triggered**: it re-runs automatically whenever its source data changes, and its narrow job is to keep one **View** in sync. It cannot `create` / `patch` / `delete` records (only append, counter-add, and publish). Reach for it when a read is too slow and you want it kept fresh on every write.
103
+ - **`@job`** is **you-drive-it** background work (typically a `@scheduled` daemon method) with the **full** write surface. Reach for it when the work is clock-driven or one-off and needs to both scan and mutate freely.
104
+
105
+ > **Rule of thumb.** Clock-driven and needs the full database surface: a `@job` (usually inside a `@daemon`). Change-driven and only maintains a View: a `@derive`. Clock-driven with modest reads and writes: a plain `@scheduled` method (no `@job` needed).
106
+
107
+ For the complete permission grid see the [function-kind matrix](../database/setup.md#how-access-is-gated-query-action-and-friends), and for the decorator itself see [every decorator](../concepts/decorators.md#database-function-kinds-data-access-policy).
108
+
109
+ ## Related
110
+
111
+ - [Daemons and scheduled jobs](./daemons.md): `@daemon`, `@scheduled`, interval vs cron, and how a single global worker fails over safely.
112
+ - [Derived views (`@derive`)](./derive.md): keeping a materialized View in sync with its source data.
113
+ - [Compute tiers (L1 to L4)](../concepts/tiers.md): where daemons and request handlers each run on the edge.
114
+ - [Views](../database/views.md) and [Events](../database/events.md): the database families a `@derive` reads from and writes to.
@@ -0,0 +1,230 @@
1
+ # Daemons (`@daemon` / `@scheduled`)
2
+
3
+ A `@daemon` is a single, long-lived background worker for your whole app. You mark a class `@daemon`, add `@scheduled` methods that fire on a timer, and the Dacely edge keeps exactly **one** copy of it running worldwide, restarting it elsewhere if the machine it is on fails.
4
+
5
+ ## What a daemon is
6
+
7
+ The word "daemon" (say "DEE-mon") is an old computing term for a program that runs quietly in the background, not tied to any single user. That is exactly what this is.
8
+
9
+ Compare the three ways your server code can run:
10
+
11
+ | Kind | How many run | Lives for |
12
+ | ------------------------------- | ------------------------------------ | ----------------------------- |
13
+ | [Request handler](../backend/rest.md) (`@rest`) | a fresh one per request | one request |
14
+ | [Stream box](../realtime/streams.md) (`@stream`) | one per open connection | one connection |
15
+ | **Daemon** (`@daemon`) | **exactly one for the whole app** | as long as it holds the lease |
16
+
17
+ Because it is a single, resident instance, its fields persist across scheduled runs (a request handler forgets everything after each request; a daemon does not). It is the right home for work that must happen **once globally on a cadence**, not once per user and not once per server.
18
+
19
+ ```ts
20
+ @daemon
21
+ class Jobs {
22
+ @scheduled('1h')
23
+ hourly(): void {
24
+ // Runs once an hour, on the one elected worker. Put recurring background
25
+ // work here: rollups, cleanup, polling an upstream, and so on.
26
+ }
27
+ }
28
+ ```
29
+
30
+ ## `@scheduled`: run on a cadence
31
+
32
+ A `@scheduled` method fires on a schedule. The single string argument is the cadence, and it comes in two flavours.
33
+
34
+ ### Interval schedules
35
+
36
+ An **interval** fires every fixed span of time. Write a number followed by a unit: `s` (seconds), `m` (minutes), `h` (hours), or `d` (days). The number must be at least 1, and the span may not exceed 7 days.
37
+
38
+ ```ts
39
+ @scheduled('30s') everyHalfMinute(): void { /* ... */ }
40
+ @scheduled('5m') everyFiveMinutes(): void { /* ... */ }
41
+ @scheduled('1h') hourly(): void { /* ... */ }
42
+ @scheduled('1d') daily(): void { /* ... */ }
43
+ ```
44
+
45
+ ### Cron schedules
46
+
47
+ A **cron** expression fires at wall-clock times ("every weekday at 9:15", "midnight on the first of the month"). Use it when you care about the actual time of day, not just a repeating gap. A cron spec is five fields separated by spaces, in this order:
48
+
49
+ ```
50
+ minute hour day-of-month month day-of-week
51
+ ```
52
+
53
+ toiljs recognises a cron spec by the spaces in it (an interval has none).
54
+
55
+ ```ts
56
+ @scheduled('15 9 * * 1-5') // 09:15, Monday to Friday
57
+ weekdayMorning(): void { /* ... */ }
58
+
59
+ @scheduled('0 0 1 * *') // 00:00 on the 1st of every month
60
+ monthlyReset(): void { /* ... */ }
61
+ ```
62
+
63
+ Cron times are evaluated in **UTC** and are **minute-granular** (the smallest cron step is one minute). A `*` means "every value" for that field.
64
+
65
+ ### Rules
66
+
67
+ - A `@scheduled` method takes **no arguments and returns `void`**.
68
+ - A daemon class may have **several** `@scheduled` methods, each on its own cadence.
69
+ - Because only the one elected worker fires them, a task runs **once per tick for the whole app**, never once per server.
70
+
71
+ ### `onStart`: run once at boot
72
+
73
+ A daemon may also declare a plain `onStart(): void` method (not decorated). It runs **once**, when the daemon box first starts on the elected worker. Use it to set up state or kick off a long-running loop.
74
+
75
+ ```ts
76
+ @daemon
77
+ class Jobs {
78
+ onStart(): void {
79
+ // one-time setup when this daemon becomes active
80
+ }
81
+
82
+ @scheduled('1h')
83
+ hourly(): void { /* ... */ }
84
+ }
85
+ ```
86
+
87
+ ## No backfill: missed runs are skipped, not replayed
88
+
89
+ This is the single most important thing to understand about scheduling.
90
+
91
+ If the daemon is down when a tick was due (say the leader failed and a standby is still taking over), or if the clock jumps forward, toiljs does **not** go back and run all the ticks you missed. It simply fires the **next** due run and moves on. This is called a **no-backfill** policy.
92
+
93
+ Two practical consequences:
94
+
95
+ 1. **Design tasks to be safe to skip.** "Recompute the summary" is fine to miss (the next run fixes it). "Charge every user once" is not, unless you make it idempotent.
96
+ 2. **Make tasks idempotent where a missed run matters.** Idempotent means running it twice (or catching up later) has the same effect as running it once. For example, "set yesterday's total to X" is idempotent; "add 1 to a counter" is not.
97
+
98
+ ## One global worker, with safe failover
99
+
100
+ There is exactly **one** daemon running for your app at any moment. A second machine keeps a **warm standby** ready but idle. If the active worker's hold on the job expires (it crashed, lost the network, or was shut down), the standby takes over and fires the following runs.
101
+
102
+ The mechanism is a **lease**. Think of the lease as a "who is in charge" token that only one worker can hold at a time, and that has to be renewed to keep. Only the worker holding the lease (the **leader**) runs the schedule.
103
+
104
+ ```mermaid
105
+ sequenceDiagram
106
+ participant A as Worker A (leader)
107
+ participant L as Lease
108
+ participant B as Worker B (standby)
109
+ A->>L: hold the lease, renew it
110
+ Note over A: A fires @scheduled ticks
111
+ Note over B: B stays idle, watching
112
+ A--xL: A crashes, stops renewing
113
+ Note over L: lease expires
114
+ B->>L: acquire the lease
115
+ Note over B: B is now leader, fires the next ticks
116
+ ```
117
+
118
+ The important guarantee: **two workers never run the same tick at the same time.** This is called **at-most-once** scheduling. The trade-off is the no-backfill behaviour above: to be sure a tick is never run twice, the edge would rather skip the in-flight tick when a leader is lost than risk running it on two machines. You never start, stop, or place the daemon yourself; the edge elects the leader and drives it.
119
+
120
+ ## Leadership fencing: side effects only run on the leader
121
+
122
+ A subtle risk with a warm standby is a "split brain": for a brief moment, two workers might both think they are the leader. To make that harmless, toiljs **fences** every side effect behind a leadership check. A **side effect** is anything that changes the outside world:
123
+
124
+ - **Database writes** (creating, patching, deleting rows, adding to counters, appending events, publishing views).
125
+ - **Outbound HTTP calls** (`http_call`, described below).
126
+
127
+ These run **only** on the confirmed leader. If code that is not the leader tries one, the edge refuses it (a "not leader" error) rather than let it happen twice. Plain **reads** and computation are not fenced (they are safe to do anywhere). So even if two workers briefly overlap, only one of them can actually write or call out. You do not write the fencing yourself; it is automatic. The upshot: put your writes and outbound calls in `@scheduled` methods freely, and trust that they happen once.
128
+
129
+ ## `daemon.*` host calls
130
+
131
+ A daemon has a small set of host abilities beyond ordinary computation:
132
+
133
+ - **Database access.** A daemon reads and writes [ToilDB](../database/README.md) with the same collection handles you use in a route or a derive (`.get`, `.add`, `.append`, `.publish`, and so on). Writes are leader-fenced as described above.
134
+ - **Outbound HTTP (`http_call`).** A daemon can call an external service (to poll an API, post to a webhook). This is the one place your server code reaches out to the internet, so it is deliberately restricted:
135
+ - It is **leader-only** (fenced, like any side effect).
136
+ - It is **SSRF-bounded**. SSRF (server-side request forgery) is an attack where code is tricked into calling internal addresses it should not reach. The edge resolves the target host and blocks private or internal addresses, so a daemon cannot use `http_call` to poke around inside the network.
137
+ - It is **metered**: making many calls or pulling huge responses costs budget, which caps abuse.
138
+ - **Leadership info** (`is_leader`, `current_epoch`). A daemon can check whether it is currently the leader, which is useful for guarding a long `onStart` loop.
139
+
140
+ > **Note:** In `toiljs dev` (the single-process local emulator), the daemon is always the leader (there is nothing to fail over to), and `http_call` is stubbed to return a "call failed" result rather than make real network requests. Everything else, including the schedule and your database writes, runs exactly as it does on the edge.
141
+
142
+ ### `http_call` is not callable from your code yet
143
+
144
+ An honest caveat, because it is easy to assume otherwise: **there is no guest-callable `http_call` API in toiljs today.** The capability itself is fully built on the *host* side (the edge implements the leader-only, SSRF-bounded, metered outbound call as a low-level host import, `daemon.http_call(reqPtr, reqLen, outPtr, outCap) -> i64`, and the dev emulator reserves the same name), but toiljs does **not** ship a friendly TypeScript wrapper you can call from a `@scheduled` method. There is no `daemon.httpCall(...)` (or similarly named) function in the standard library, so your daemon code cannot make an outbound HTTP request at the moment.
145
+
146
+ Treat outbound HTTP from a daemon as **planned, not yet available**. When the guest binding lands it will behave exactly as described above (leader-fenced, SSRF-bounded, and metered, and its usage will show up in the `Analytics` counters `daemonHttpCallAttempts` / `daemonHttpCallFailures`). Until then, if a daemon needs data from an outside service, have that service write into [ToilDB](../database/README.md) some other way, and let the daemon read it from there.
147
+
148
+ ## The `main.daemon.ts` file (a separate tier)
149
+
150
+ Like streams, daemons live in their **own entry file**, `server/main.daemon.ts`, and compile into their **own artifact**, `build/server/release-cold.wasm`. Importing your `@daemon` module there pulls it into that artifact.
151
+
152
+ ```ts
153
+ // server/main.daemon.ts
154
+ import { revertOnError } from 'toiljs/server/runtime/abort/abort';
155
+
156
+ import './daemon/Jobs'; // add each @daemon module here
157
+
158
+ // NOTE: unlike main.ts / main.stream.ts, the daemon entry does NOT re-export the
159
+ // request runtime. A daemon artifact exposes its schedule hooks, not the request handler.
160
+ export function abort(message: string, fileName: string, line: u32, column: u32): void {
161
+ revertOnError(message, fileName, line, column);
162
+ }
163
+ ```
164
+
165
+ `toiljs build` produces `release-cold.wasm` automatically when your project declares a `@daemon` surface:
166
+
167
+ ```sh
168
+ $ ls build/server/*.wasm
169
+ build/server/release.wasm # L1 request (@rest / @service)
170
+ build/server/release-stream.wasm # L2/L3 stream (@stream)
171
+ build/server/release-cold.wasm # L4 daemon (@daemon)
172
+ ```
173
+
174
+ There is **at most one** `@daemon` class per project, and it is a compile error to put a `@daemon` in the request build. See [Compute tiers](../concepts/tiers.md) for how one source tree becomes three artifacts.
175
+
176
+ ## Worked example: a periodic rollup
177
+
178
+ A common daemon job: every hour, read a running total and store a small summary so pages can show it with one cheap read. This reads a [counter](../database/counters.md) and writes a summary [document](../database/documents.md); the write only takes effect on the leader.
179
+
180
+ ```ts
181
+ // server/models/StatKey.ts and Summary.ts (@data types)
182
+ @data
183
+ class StatKey {
184
+ name: string = 'signups';
185
+ constructor(name: string = 'signups') { this.name = name; }
186
+ }
187
+
188
+ @data
189
+ class Summary {
190
+ total: u64 = 0;
191
+ updatedAt: u64 = 0;
192
+ }
193
+
194
+ // server/data/StatsDb.ts
195
+ @database
196
+ class StatsDb {
197
+ @collection static signups: Counter<StatKey>;
198
+ @collection static summary: Documents<StatKey, Summary>;
199
+ }
200
+
201
+ // server/daemon/Jobs.ts
202
+ @daemon
203
+ class Jobs {
204
+ @scheduled('1h')
205
+ rollup(): void {
206
+ const key = new StatKey('signups');
207
+ const total = StatsDb.signups.get(key); // a read (allowed anywhere)
208
+
209
+ const s = new Summary();
210
+ s.total = total;
211
+ s.updatedAt = <u64>(Date.now() / 1000);
212
+ StatsDb.summary.patch(key, s); // a write: runs only on the leader
213
+ }
214
+ }
215
+ ```
216
+
217
+ Now any request handler can serve the summary with a single keyed read, and it is never more than an hour stale. Because the write is fenced to the leader and the rollup is idempotent (it *sets* the total rather than adding to it), a missed or failed-over tick is harmless: the next hourly run simply refreshes it.
218
+
219
+ ## When not to use a daemon
220
+
221
+ - **When a user is waiting for the result.** Do that work in a [route](../backend/rest.md) or [RPC](../backend/rpc.md), on the request path.
222
+ - **When you need a fast read-view kept in sync with data changes.** That is a [`@derive`](./derive.md), which runs on every write rather than on a timer.
223
+ - **When you need exactly-once, never-skipped execution.** Scheduling is at-most-once with no backfill, so make tasks idempotent or safe to skip. A daemon is not a guaranteed job queue.
224
+
225
+ ## Related
226
+
227
+ - [Background overview](./README.md): daemons versus `@derive`, and which to reach for.
228
+ - [Derived views (`@derive`)](./derive.md): keep a read-view in sync on every write.
229
+ - [Compute tiers (L1 to L4)](../concepts/tiers.md): the daemon runs on the L4 global tier.
230
+ - [Counters](../database/counters.md), [Documents](../database/documents.md), [Events](../database/events.md): the data a daemon reads and writes.
@@ -0,0 +1,179 @@
1
+ # Derived views (`@derive`)
2
+
3
+ A `@derive` keeps a fast, precomputed **View** of your data in sync automatically. You write a method that reads your source data and publishes a summary; toiljs re-runs it whenever the source data changes, so your pages can read the summary with one cheap lookup instead of doing the work on every request.
4
+
5
+ ## The problem it solves
6
+
7
+ Some reads are expensive. "Show the 10 newest comments" means scanning an [events](../database/events.md) log. "Show the leaderboard" means totalling up scores. A **scan** (walking many rows to build a result) can fan out across an unbounded amount of data, and that is too slow and too unpredictable to do while a user waits.
8
+
9
+ So toiljs **bars scans on the request path**. A [route](../backend/rest.md) handler runs under a restricted mode:
10
+
11
+ - A `@get` runs as a **query** (reads only, no scans).
12
+ - A `@post` / `@put` / `@patch` / `@del` runs as an **action** (keyed reads and writes, still no scans).
13
+
14
+ If you cannot scan in a route, how do you show "the latest 10"? You precompute it. A `@derive` does the scan **off** the request path, folds the result into a [View](../database/views.md) (a read-optimized snapshot stored by key), and your route reads that View with a single keyed lookup, which is not a scan and so is allowed.
15
+
16
+ ```mermaid
17
+ flowchart LR
18
+ W["POST /guestbook<br/>(action: append + count)"] --> S[(Events + Counter<br/>source data)]
19
+ S -->|write triggers| D["@derive recompute()<br/>scans, builds, publishes"]
20
+ D --> V[(View<br/>precomputed snapshot)]
21
+ G["GET /guestbook<br/>(query: one keyed read)"] --> V
22
+ ```
23
+
24
+ ## A worked example: a guestbook
25
+
26
+ Here is the whole pattern in one database class: an [events](../database/events.md) log of signatures, a [counter](../database/counters.md) of how many there are, and a [View](../database/views.md) that holds the ready-to-serve page.
27
+
28
+ ```ts
29
+ @data
30
+ class GuestKey {
31
+ room: string = 'main';
32
+ constructor(room: string = 'main') { this.room = room; }
33
+ }
34
+
35
+ @database
36
+ class GuestbookDb {
37
+ @collection static entries: Events<GuestKey, GuestEntry>; // a source: the log
38
+ @collection static totals: Counter<GuestKey>; // a source: the count
39
+ @collection static book: View<GuestKey, GuestbookView>; // the view we publish
40
+
41
+ // Recompute the view from the sources. It MAY scan and publish; a route may not.
42
+ @derive
43
+ recompute(): void {
44
+ const key = new GuestKey('main');
45
+ const view = new GuestbookView();
46
+ view.total = GuestbookDb.totals.get(key); // a keyed read
47
+ view.entries = GuestbookDb.entries.latest(key, 10); // a scan: allowed here
48
+ GuestbookDb.book.publish(key, view); // publish the snapshot
49
+ }
50
+ }
51
+ ```
52
+
53
+ The route then writes to the sources and reads the view:
54
+
55
+ ```ts
56
+ @rest('guestbook')
57
+ class Guestbook {
58
+ @get('/')
59
+ list(): GuestbookView {
60
+ const key = new GuestKey('main');
61
+ const view = GuestbookDb.book.get(key); // one keyed read, not a scan
62
+ return view == null ? new GuestbookView() : view; // empty until first publish
63
+ }
64
+
65
+ @post('/')
66
+ sign(input: NewMessage): GuestbookView {
67
+ const key = new GuestKey('main');
68
+ GuestbookDb.entries.append(key, new GuestEntry(input.author, input.message, 0));
69
+ GuestbookDb.totals.add(key, 1);
70
+ // The @derive republishes `book` right after this action returns, so GET
71
+ // serves the new entry. The action just acks with the new total (a counter
72
+ // read is allowed here; scanning the entries list is not).
73
+ const view = new GuestbookView();
74
+ view.total = GuestbookDb.totals.get(key);
75
+ return view;
76
+ }
77
+ }
78
+ ```
79
+
80
+ Sign the guestbook twice and the total climbs across requests, because the data lives in the database (and its view), not in module memory.
81
+
82
+ ## What a `@derive` may do
83
+
84
+ A `@derive` runs under a special **derive** mode that is more powerful than a route:
85
+
86
+ | Ability | Query (`@get`) | Action (`@post` ...) | Derive |
87
+ | -------------------------------------------- | :------------: | :------------------: | :----------: |
88
+ | Keyed reads (`.get`, counter total) | yes | yes | yes |
89
+ | Writes (`create`, `patch`, `add`, `append`) | no | yes | yes |
90
+ | **Scans** (`events.latest`, membership list) | **no** | **no** | **yes** |
91
+ | `view.publish` / `view.append` | no | no | yes |
92
+
93
+ So a derive is exactly the place to do the reads and scans a route cannot, and to publish the result.
94
+
95
+ ## Declaring a derive
96
+
97
+ A `@derive` is a method on your [`@database`](../database/setup.md) class, next to the collections it reads and the View it writes.
98
+
99
+ ```ts
100
+ @database
101
+ class MyDb {
102
+ @collection static events: Events<Key, Fact>; // a source
103
+ @collection static home: View<Key, HomePage>; // the materialized view
104
+
105
+ @derive
106
+ rebuild(): void {
107
+ // read sources, build the value, publish it
108
+ }
109
+ }
110
+ ```
111
+
112
+ Rules:
113
+
114
+ - A `@derive` method takes **no arguments and returns `void`**.
115
+ - A database may declare **multiple** `@derive` methods; each runs independently.
116
+ - The View value and its key are ordinary [`@data`](../backend/data.md) types, so they round-trip through the codec like any other stored value.
117
+
118
+ ## When a derive runs
119
+
120
+ You never call a derive yourself. The runtime runs it for you at two moments:
121
+
122
+ 1. **After a write to a source.** When a request writes one of the database's source collections (an `events.append`, a `counter.add`, a document `create` or `patch`), that database's derives run **right after the response is produced**, so the view reflects the new data on the next read. Many writes to one database in a single request are **coalesced** into one recompute (it does not run once per write).
123
+
124
+ 2. **On box load.** When a server box starts, hot-reloads, or notices the underlying data changed out of band, the views are rebuilt from their sources **before the first read is served**. This is also where a value type's [`@migrate`](../backend/data.md) runs against old stored events, as the derive re-reads and republishes them.
125
+
126
+ A derive's own `view.publish` never re-triggers it, so there is no infinite loop.
127
+
128
+ The same code runs under `toiljs dev` (the in-process emulator) and on the production edge, with no flags or wiring to change.
129
+
130
+ ## Folding a growing log incrementally
131
+
132
+ The guestbook above uses `events.latest` and **recomputes** the view from scratch on every change. That is simple, correct, and the right default for a **bounded** read: the latest N, a counter total, a small set.
133
+
134
+ Some views instead fold an **unbounded** log: a running total over every event ever, an activity rollup, an audit summary. Rescanning the whole log on every change gets slower as it grows. For those, read the source with [`events.since`](../database/events.md) instead of `latest`. `since` hands you only the events you have **not folded yet**, so the derive folds forward incrementally.
135
+
136
+ ```ts
137
+ @derive
138
+ rollup(): void {
139
+ const key = new StatsKey('all');
140
+ const view = StatsDb.summary.get(key) ?? new Summary(); // the running view so far
141
+ let batch = StatsDb.events.since(key, 500);
142
+ while (batch.length > 0) { // drain the new events in bounded batches
143
+ for (let i = 0; i < batch.length; i++) view.apply(batch[i]);
144
+ batch = StatsDb.events.since(key, 500);
145
+ }
146
+ StatsDb.summary.publish(key, view);
147
+ }
148
+ ```
149
+
150
+ The host owns the cursor, you never manage it: it seeds `since` from a durable checkpoint, advances it as it hands you events, and saves it **only after** your `publish` lands (so a crash re-folds the batch instead of skipping it). On the next trigger the derive resumes exactly where it left off.
151
+
152
+ **One rule: the fold must be idempotent per event.** A rare crash-recovery case (an event that "heals" at an older position after the derive already passed it) makes the host re-read the whole log that one run, so applying an event twice must not change the result. Use set-style updates keyed by the event's id, not blind `count += 1` accumulation. If your fold cannot be idempotent, stay on the simple `latest` recompute path.
153
+
154
+ ## Guarantees and limitations
155
+
156
+ **Guarantees**
157
+
158
+ - **It converges to a correct snapshot.** Publishes are last-writer-wins (the host versions each publish so a later one always supersedes an earlier one), and a derive recomputes from the source of truth, so the view always ends up matching its sources.
159
+ - **Reads stay cheap.** A route serves the view with a single keyed lookup, never a scan.
160
+
161
+ **Limitations (read these)**
162
+
163
+ - **By default it recomputes from scratch each time.** A derive re-reads its sources and republishes on every trigger. That is the simple path and a great fit for a **bounded** read: the latest N, a counter total, a small set. To fold an **unbounded, ever-growing** log efficiently, read it with [`events.since`](../database/events.md) instead of `latest` (see [Folding a growing log incrementally](#folding-a-growing-log-incrementally) above); it folds only the new events since a checkpoint.
164
+ - **It is eventually consistent, by a moment.** The view is republished right after the writing request finishes, so there is a tiny window where a reader could see the pre-write view. For most pages (a feed, a leaderboard) that is invisible and fine.
165
+ - **`view.publish` is derive-only.** Routes cannot publish; they can only read the view. That is the whole point: the expensive build happens off the request path.
166
+
167
+ ## When not to use a derive
168
+
169
+ - **When the read is already cheap.** If a route can answer with a plain keyed `.get`, you do not need a view at all.
170
+ - **When you need it on a timer, not on a data change.** Recomputing "yesterday's report" at 2am is a [daemon](./daemons.md) job, not a derive (a derive is triggered by writes, not by the clock).
171
+ - **When the source is an unbounded full-history fold.** See the limitation above; keep derives to bounded reads.
172
+
173
+ ## Related
174
+
175
+ - [Views](../database/views.md): the `View<K, V>` family a derive publishes into, and how to read it.
176
+ - [Events](../database/events.md): the append-only log a derive commonly folds into a view.
177
+ - [Counters](../database/counters.md): running totals a derive can read.
178
+ - [Background overview](./README.md): `@derive` versus `@daemon`, and which to reach for.
179
+ - [Data types (`@data`)](../backend/data.md): the value and key types a view stores.