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.
- package/CHANGELOG.md +5 -0
- package/README.md +2 -2
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +303 -293
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +2 -0
- package/build/compiler/config.js +1 -0
- package/build/compiler/docs.js +8 -24
- package/build/compiler/generate.js +4 -2
- package/build/compiler/index.d.ts +1 -1
- package/build/compiler/index.js +69 -6
- package/build/compiler/toil-docs.generated.js +64 -22
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/analytics/index.js +7 -3
- package/build/devserver/db/database.d.ts +4 -0
- package/build/devserver/db/database.js +43 -1
- package/build/devserver/db/index.d.ts +1 -1
- package/build/devserver/db/index.js +1 -1
- package/build/devserver/db/types.d.ts +3 -0
- package/build/devserver/db/types.js +2 -0
- package/build/devserver/runtime/module.js +4 -1
- package/docs/README.md +104 -65
- package/docs/auth/README.md +102 -0
- package/docs/auth/configuration.md +94 -0
- package/docs/auth/extending.md +202 -0
- package/docs/auth/how-it-works.md +138 -0
- package/docs/auth/usage.md +188 -0
- package/docs/backend/README.md +143 -0
- package/docs/backend/data.md +351 -0
- package/docs/backend/rest.md +402 -0
- package/docs/backend/rpc.md +226 -0
- package/docs/background/README.md +114 -0
- package/docs/background/daemons.md +230 -0
- package/docs/background/derive.md +179 -0
- package/docs/cli/README.md +377 -0
- package/docs/concepts/config.md +416 -0
- package/docs/concepts/decorators.md +127 -0
- package/docs/concepts/security.md +108 -0
- package/docs/concepts/tiers.md +166 -0
- package/docs/concepts/types.md +216 -0
- package/docs/database/README.md +143 -0
- package/docs/database/capacity.md +350 -0
- package/docs/database/counters.md +174 -0
- package/docs/database/documents.md +255 -0
- package/docs/database/events.md +307 -0
- package/docs/database/membership.md +246 -0
- package/docs/database/setup.md +155 -0
- package/docs/database/unique.md +216 -0
- package/docs/database/views.md +246 -0
- package/docs/frontend/README.md +101 -0
- package/docs/frontend/data-fetching.md +243 -0
- package/docs/frontend/images.md +148 -0
- package/docs/frontend/metadata.md +344 -0
- package/docs/frontend/rendering.md +236 -0
- package/docs/frontend/routing.md +344 -0
- package/docs/frontend/scripts.md +118 -0
- package/docs/frontend/search.md +191 -0
- package/docs/frontend/styling.md +147 -0
- package/docs/getting-started/README.md +81 -0
- package/docs/getting-started/create-project.md +131 -0
- package/docs/getting-started/deploy.md +101 -0
- package/docs/getting-started/first-app.md +215 -0
- package/docs/getting-started/installation.md +106 -0
- package/docs/getting-started/migrating.md +125 -0
- package/docs/getting-started/project-structure.md +163 -0
- package/docs/introduction/README.md +41 -0
- package/docs/introduction/design-principles.md +55 -0
- package/docs/introduction/distributed.md +74 -0
- package/docs/introduction/how-it-works.md +74 -0
- package/docs/introduction/hyperscale.md +51 -0
- package/docs/introduction/modern-stack.md +36 -0
- package/docs/introduction/vs-other-frameworks.md +48 -0
- package/docs/introduction/why-toil.md +93 -0
- package/docs/llms.txt +90 -0
- package/docs/realtime/README.md +102 -0
- package/docs/realtime/channels.md +211 -0
- package/docs/realtime/streams.md +369 -0
- package/docs/services/README.md +60 -0
- package/docs/services/analytics.md +268 -0
- package/docs/services/caching.md +175 -0
- package/docs/services/cookies.md +235 -0
- package/docs/services/crypto.md +209 -0
- package/docs/services/email.md +289 -0
- package/docs/services/environment.md +141 -0
- package/docs/services/ratelimit.md +174 -0
- package/docs/services/time.md +85 -0
- package/examples/basic/client/routes/analytics.tsx +13 -12
- package/examples/basic/server/models/SiteAnalytics.ts +29 -17
- package/examples/basic/server/routes/Analytics.ts +15 -17
- package/examples/basic/server/routes/UserId.ts +22 -0
- package/package.json +15 -11
- package/scripts/gen-toil-docs.mjs +24 -35
- package/server/auth/AuthController.ts +336 -0
- package/server/auth/AuthUser.ts +23 -0
- package/server/auth/index.ts +16 -0
- package/server/globals/auth.ts +31 -0
- package/server/globals/userid.ts +107 -0
- package/src/compiler/config.ts +13 -0
- package/src/compiler/docs.ts +16 -33
- package/src/compiler/generate.ts +6 -2
- package/src/compiler/index.ts +114 -6
- package/src/compiler/toil-docs.generated.ts +64 -22
- package/src/devserver/analytics/index.ts +10 -4
- package/src/devserver/db/database.ts +67 -1
- package/src/devserver/db/index.ts +1 -0
- package/src/devserver/db/types.ts +13 -0
- package/src/devserver/runtime/module.ts +7 -0
- package/test/analytics-dev.test.ts +2 -1
- package/test/devserver-database.test.ts +113 -0
- package/docs/auth-todo.md +0 -149
- package/docs/auth.md +0 -322
- package/docs/caching.md +0 -115
- package/docs/cli.md +0 -17
- package/docs/client.md +0 -39
- package/docs/cookies.md +0 -457
- package/docs/crypto.md +0 -130
- package/docs/daemon.md +0 -123
- package/docs/data.md +0 -131
- package/docs/derive.md +0 -159
- package/docs/email.md +0 -326
- package/docs/environment.md +0 -97
- package/docs/getting-started.md +0 -128
- package/docs/index.md +0 -30
- package/docs/ratelimit.md +0 -95
- package/docs/routing.md +0 -259
- package/docs/rpc.md +0 -149
- package/docs/server.md +0 -61
- package/docs/ssr.md +0 -632
- package/docs/streams.md +0 -178
- package/docs/styling.md +0 -22
- package/docs/tiers.md +0 -133
- 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.
|