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
package/docs/daemon.md
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
# Daemon
|
|
2
|
-
|
|
3
|
-
`@daemon` declares a single, leader-elected background worker for your domain -
|
|
4
|
-
the **L4** (global) coordination tier of the Toil edge. Where a `@rest` handler
|
|
5
|
-
is a fresh instance per request and a `@stream` box is one instance per
|
|
6
|
-
connection, there is exactly **one** daemon per domain at a time. The edge keeps
|
|
7
|
-
a warm standby ready and fails over at-most-once, so the daemon is the right
|
|
8
|
-
place for work that must happen once globally rather than once per request.
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
@daemon
|
|
12
|
-
class Jobs {
|
|
13
|
-
@scheduled('1h')
|
|
14
|
-
hourly(): void {
|
|
15
|
-
// Runs once an hour on the elected leader. Put recurring background work
|
|
16
|
-
// here (rollups, cleanup, polling an upstream, ...).
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## `@daemon` classes
|
|
22
|
-
|
|
23
|
-
`@daemon` marks a class as the domain's background worker. The class is resident:
|
|
24
|
-
it is created once on the elected leader and lives for as long as that leader
|
|
25
|
-
holds the lease, so its fields persist across scheduled runs (a `@rest`
|
|
26
|
-
handler's fields would reset every request).
|
|
27
|
-
|
|
28
|
-
Exactly one daemon instance runs per domain at any moment. A second node stays a
|
|
29
|
-
warm standby and only becomes active if the current leader's lease lapses. You do
|
|
30
|
-
not start, stop, or place the daemon yourself - the edge elects the leader and
|
|
31
|
-
drives it.
|
|
32
|
-
|
|
33
|
-
## `@scheduled`
|
|
34
|
-
|
|
35
|
-
A `@scheduled` method declares a task that fires on a cadence, always on the
|
|
36
|
-
**elected leader**. The single string argument is the cadence:
|
|
37
|
-
|
|
38
|
-
```ts
|
|
39
|
-
@scheduled('1h')
|
|
40
|
-
hourly(): void { /* ... */ }
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
- **Interval strings** like `'1h'` fire on that fixed period.
|
|
44
|
-
- **Cron expressions** are also supported when you need a wall-clock schedule
|
|
45
|
-
rather than a fixed interval.
|
|
46
|
-
|
|
47
|
-
A class can declare several `@scheduled` methods; each runs on its own cadence.
|
|
48
|
-
Because only the leader fires them, a task runs once per domain per tick, not
|
|
49
|
-
once per node.
|
|
50
|
-
|
|
51
|
-
## The daemon entry
|
|
52
|
-
|
|
53
|
-
The daemon surface has its own entry module, `server/main.daemon.ts`. It imports
|
|
54
|
-
the `@daemon` classes so the compiler-generated `daemon_start` / `scheduled_tick`
|
|
55
|
-
exports are pulled into the artifact:
|
|
56
|
-
|
|
57
|
-
```ts
|
|
58
|
-
// server/main.daemon.ts
|
|
59
|
-
import { revertOnError } from 'toiljs/server/runtime/abort/abort';
|
|
60
|
-
|
|
61
|
-
import './daemon/Jobs';
|
|
62
|
-
|
|
63
|
-
// The abort hook (the daemon box reports a trap through it). NOTE: unlike main.ts /
|
|
64
|
-
// main.stream.ts, the daemon entry does NOT re-export the request runtime - a cold
|
|
65
|
-
// artifact exposes daemon_start/scheduled_tick, not the request `handle`.
|
|
66
|
-
export function abort(message: string, fileName: string, line: u32, column: u32): void {
|
|
67
|
-
revertOnError(message, fileName, line, column);
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
Note what is **not** here: unlike `main.ts` and `main.stream.ts`, the daemon
|
|
72
|
-
entry does not `export * from 'toiljs/server/runtime/exports'`. A daemon (cold)
|
|
73
|
-
artifact exposes `daemon_start` and `scheduled_tick`, not the request `handle`.
|
|
74
|
-
Add a daemon as you grow by importing its module here.
|
|
75
|
-
|
|
76
|
-
## Build
|
|
77
|
-
|
|
78
|
-
`toiljs build` runs one toilscript pass per tier and hands each pass only the
|
|
79
|
-
entries that belong to it. When the project declares a `@daemon` / `@scheduled`
|
|
80
|
-
surface, the daemon pass compiles `server/main.daemon.ts` into its own artifact,
|
|
81
|
-
`build/server/release-cold.wasm`:
|
|
82
|
-
|
|
83
|
-
```sh
|
|
84
|
-
$ ls build/server/*.wasm
|
|
85
|
-
build/server/release.wasm # L1 request (exports: handle)
|
|
86
|
-
build/server/release-stream.wasm # L2/L3 stream (exports: stream_dispatch)
|
|
87
|
-
build/server/release-cold.wasm # L4 daemon (exports: daemon_start, scheduled_tick)
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
So `release.wasm` never contains `scheduled_tick` and the daemon artifact never
|
|
91
|
-
contains the request `handle`. Plain `@data` and helper modules are shared into
|
|
92
|
-
every artifact. See [Tiers](./tiers.md) for how the three artifacts are produced
|
|
93
|
-
from one source tree.
|
|
94
|
-
|
|
95
|
-
## Use cases
|
|
96
|
-
|
|
97
|
-
The daemon is the once-per-domain tier, so it fits work you want to happen
|
|
98
|
-
globally on a cadence rather than per request:
|
|
99
|
-
|
|
100
|
-
- **Periodic rollups** - aggregate counters or events into summaries.
|
|
101
|
-
- **Cleanup** - expire stale rows, prune logs, reclaim resources.
|
|
102
|
-
- **Polling an upstream** - pull from an external API on a schedule.
|
|
103
|
-
- **Global coordination** - any task that must run exactly once across the
|
|
104
|
-
domain, not once per node.
|
|
105
|
-
|
|
106
|
-
## Failover
|
|
107
|
-
|
|
108
|
-
Scheduling is **at-most-once**. A `@scheduled` task fires on whichever node
|
|
109
|
-
currently holds the leader lease. If that leader fails, the warm standby takes
|
|
110
|
-
over and fires the **subsequent** runs; the edge does not retry or duplicate the
|
|
111
|
-
tick that was in flight when the leader was lost. This trades exactly-once
|
|
112
|
-
delivery for the guarantee that two nodes never run the same scheduled tick at
|
|
113
|
-
once, so design tasks to be safe to skip an occasional run and to be idempotent
|
|
114
|
-
where a missed run matters.
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
See also:
|
|
119
|
-
|
|
120
|
-
- [Tiers](./tiers.md) - the three deployment tiers and how one source tree
|
|
121
|
-
compiles to a separate artifact per tier.
|
|
122
|
-
- [Streams](./streams.md) - the L2/L3 `@stream` tier (one resident box per
|
|
123
|
-
connection).
|
package/docs/data.md
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# Data codec (`@data`)
|
|
2
|
-
|
|
3
|
-
`@data` turns a plain class into a typed, versionable value with a deterministic
|
|
4
|
-
binary codec and a JSON codec. It is the backbone of request/response bodies,
|
|
5
|
-
RPC arguments, sessions, and anything you persist. The same class becomes a
|
|
6
|
-
fully typed client type in the generated `shared/server.ts` (see
|
|
7
|
-
[RPC](./rpc.md)).
|
|
8
|
-
|
|
9
|
-
```ts
|
|
10
|
-
@data
|
|
11
|
-
class Player {
|
|
12
|
-
username: string = '';
|
|
13
|
-
admin: bool = false;
|
|
14
|
-
score: u64 = 0;
|
|
15
|
-
}
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
From that the compiler synthesizes, on the class:
|
|
19
|
-
|
|
20
|
-
- `encode(): Uint8Array` / `static decode(buf): T`, the binary codec (with a
|
|
21
|
-
4-byte type id prefix).
|
|
22
|
-
- `encodeInto(w: DataWriter)` / `static decodeFrom(r: DataReader)`, the codec
|
|
23
|
-
without the type-id frame, for nesting.
|
|
24
|
-
- `toJSON()` / `static fromJSON(v)`, the JSON codec (64-bit-and-larger integers
|
|
25
|
-
as decimal strings, so they survive `JSON.parse` exactly).
|
|
26
|
-
- `static dataId(): u32`, a stable FNV-1a hash of the class name, written as the
|
|
27
|
-
type-id prefix by `encode()`.
|
|
28
|
-
|
|
29
|
-
Fields may be scalars (`u8`..`u256`, `i8`..`i256`, `f32`, `f64`, `bool`),
|
|
30
|
-
`string`, a nested `@data` class, or an array `T[]` of any of these. Give every
|
|
31
|
-
field a default; the generated decoder and the client constructor use them.
|
|
32
|
-
|
|
33
|
-
## Using `@data` in routes
|
|
34
|
-
|
|
35
|
-
In a **JSON** route, a `@data` parameter is revived from the parsed body and a
|
|
36
|
-
`@data` return value is serialized with `toJSON()`. In a **Binary** route, the
|
|
37
|
-
parameter is `decode`d from the raw body and the return value is `encode`d. The
|
|
38
|
-
route's stream mode (see [Routing](./routing.md#data-streams)) picks which.
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
@post('/') // JSON route
|
|
42
|
-
public create(input: NewPlayer): Player { /* input from JSON, Player to JSON */ }
|
|
43
|
-
|
|
44
|
-
@route({ method: Methods.POST, path: '/blob', stream: DataStream.Binary })
|
|
45
|
-
public blob(input: FileData): FileResult { /* input.decode, result.encode */ }
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## The binary codec: `DataWriter` / `DataReader`
|
|
49
|
-
|
|
50
|
-
When you need to lay out bytes yourself, custom bodies, session payloads,
|
|
51
|
-
challenge messages, use the codec directly. It lives in the `data` module:
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
import { DataWriter, DataReader } from 'data';
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
The codec has a byte-for-byte identical TypeScript implementation in
|
|
58
|
-
`toiljs/io` (`src/io/codec.ts`), so the client can read and write the exact same
|
|
59
|
-
wire format the wasm guest does.
|
|
60
|
-
|
|
61
|
-
### `DataWriter`
|
|
62
|
-
|
|
63
|
-
Every writer method returns the writer for chaining.
|
|
64
|
-
|
|
65
|
-
| Method | Signature | Wire format |
|
|
66
|
-
| --- | --- | --- |
|
|
67
|
-
| `writeU8` / `writeI8` | `(v): DataWriter` | 1 byte |
|
|
68
|
-
| `writeU16` / `writeI16` | `(v): DataWriter` | 2 bytes, little-endian |
|
|
69
|
-
| `writeU32` / `writeI32` | `(v): DataWriter` | 4 bytes, LE |
|
|
70
|
-
| `writeU64` / `writeI64` | `(v): DataWriter` | 8 bytes, LE |
|
|
71
|
-
| `writeF32` / `writeF64` | `(v): DataWriter` | 4 / 8 bytes, IEEE-754 LE |
|
|
72
|
-
| `writeBool` | `(v): DataWriter` | 1 byte (`1`/`0`) |
|
|
73
|
-
| `writeBytes` | `(b: Uint8Array): DataWriter` | `u32` length (LE) + raw bytes |
|
|
74
|
-
| `writeString` | `(s: string): DataWriter` | `u32` length (LE) + UTF-8 bytes |
|
|
75
|
-
| `writeU128` / `writeI128` | `(v): DataWriter` | two `u64` limbs (lo, hi) |
|
|
76
|
-
| `writeU256` / `writeI256` | `(v): DataWriter` | four `u64` limbs (lo1, lo2, hi1, hi2) |
|
|
77
|
-
| `length` | `(): i32` | bytes written so far |
|
|
78
|
-
| `toBytes` | `(): Uint8Array` | an exact-length copy of the buffer |
|
|
79
|
-
|
|
80
|
-
### `DataReader`
|
|
81
|
-
|
|
82
|
-
Reads are bounds-safe: an over-read never traps. It returns a zero/empty default
|
|
83
|
-
and sets the public `ok` flag to `false`. Check `ok` after a sequence of reads
|
|
84
|
-
to detect a truncated or malformed buffer.
|
|
85
|
-
|
|
86
|
-
| Method | Signature | On over-read |
|
|
87
|
-
| --- | --- | --- |
|
|
88
|
-
| `readU8` / `readI8` | `(): u8 / i8` | `0` |
|
|
89
|
-
| `readU16`..`readU64`, `readI16`..`readI64` | `(): integer` | `0` |
|
|
90
|
-
| `readF32` / `readF64` | `(): f32 / f64` | `0` |
|
|
91
|
-
| `readBool` | `(): bool` | `false` |
|
|
92
|
-
| `readBytes` | `(): Uint8Array` | empty array |
|
|
93
|
-
| `readString` | `(): string` | `""` |
|
|
94
|
-
| `readU128`/`readI128`/`readU256`/`readI256` | `(): bignum` | `0` |
|
|
95
|
-
| `remaining` | `(): i32` | bytes left unread |
|
|
96
|
-
| `ok` | `bool` (field) | `false` once any read over-ran |
|
|
97
|
-
|
|
98
|
-
### Example
|
|
99
|
-
|
|
100
|
-
```ts
|
|
101
|
-
import { DataWriter, DataReader } from 'data';
|
|
102
|
-
|
|
103
|
-
// Write: u8 version, str name, u64 score, bytes blob
|
|
104
|
-
const out = new DataWriter()
|
|
105
|
-
.writeU8(1)
|
|
106
|
-
.writeString('alice')
|
|
107
|
-
.writeU64(1234)
|
|
108
|
-
.writeBytes(payload)
|
|
109
|
-
.toBytes();
|
|
110
|
-
|
|
111
|
-
// Read it back
|
|
112
|
-
const r = new DataReader(out);
|
|
113
|
-
const version = r.readU8();
|
|
114
|
-
const name = r.readString();
|
|
115
|
-
const score = r.readU64();
|
|
116
|
-
const blob = r.readBytes();
|
|
117
|
-
if (!r.ok) return Response.badRequest('truncated');
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## Notes
|
|
121
|
-
|
|
122
|
-
- **Endianness.** The AS guest codec is little-endian. The TypeScript `toiljs/io`
|
|
123
|
-
codec defaults to little-endian and also accepts a per-call `be` flag for
|
|
124
|
-
big-endian network formats; keep both ends on the same setting.
|
|
125
|
-
- **Field order is the format.** The binary layout is exactly the field
|
|
126
|
-
declaration order. Reordering fields, or changing a type, is a breaking format
|
|
127
|
-
change. Add new fields at the end and bump a leading version byte if you need
|
|
128
|
-
to evolve a hand-rolled payload.
|
|
129
|
-
- **`encode()` carries a type id.** The 4-byte `dataId()` prefix lets a decoder
|
|
130
|
-
confirm it is reading the type it expects. `encodeInto`/`decodeFrom` skip the
|
|
131
|
-
frame for nesting one `@data` value inside another.
|
package/docs/derive.md
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
# Derive (materialized views)
|
|
2
|
-
|
|
3
|
-
`@derive` precomputes a read-optimized **view** from your data so reads stay
|
|
4
|
-
fast and never scan. A request handler (`@get` runs as a *query*, `@post`/`@put`/
|
|
5
|
-
`@delete` as an *action*) is not allowed to scan, reading "the latest N events"
|
|
6
|
-
or "every member of a set" could fan out across unbounded rows, so those scans
|
|
7
|
-
are barred on the request path. A `@derive` does the scan **off** the request
|
|
8
|
-
path: it folds your event log / counters into a `View`, and your route serves
|
|
9
|
-
that view with a single keyed read.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
@database
|
|
13
|
-
class GuestbookDb {
|
|
14
|
-
@collection static entries: Events<GuestKey, GuestEntry>;
|
|
15
|
-
@collection static totals: Counter<GuestKey>;
|
|
16
|
-
@collection static book: View<GuestKey, GuestbookView>;
|
|
17
|
-
|
|
18
|
-
// Recompute the view from the sources. Runs after a signature is written
|
|
19
|
-
// (and when a box first loads). A derive MAY scan + publish; a route may not.
|
|
20
|
-
@derive
|
|
21
|
-
recompute(): void {
|
|
22
|
-
const key = new GuestKey('main');
|
|
23
|
-
const view = new GuestbookView();
|
|
24
|
-
view.total = GuestbookDb.totals.get(key); // counter read
|
|
25
|
-
view.entries = GuestbookDb.entries.latest(key, 10); // scan, allowed here
|
|
26
|
-
GuestbookDb.book.publish(key, view); // publish the materialized view
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Why a derive
|
|
32
|
-
|
|
33
|
-
ToilDB gates every data op by the *function kind* it runs under:
|
|
34
|
-
|
|
35
|
-
- **query** (`@get`/`@head`) and **action** (`@post`/`@put`/`@patch`/`@delete`)
|
|
36
|
-
may do keyed reads and (actions only) writes, but **not scans**
|
|
37
|
-
(`events.latest`, `membership.list`).
|
|
38
|
-
- **derive** may do everything a read can, **plus** scans, plus
|
|
39
|
-
`view.publish`/`append`/`counter.add`.
|
|
40
|
-
|
|
41
|
-
So if a page needs "the 10 newest entries" or "the leaderboard", you cannot read
|
|
42
|
-
that directly in the `@get`. Instead a `@derive` builds it once into a `View`,
|
|
43
|
-
and the `@get` reads the view by key, which is not a scan.
|
|
44
|
-
|
|
45
|
-
## Declaring a derive
|
|
46
|
-
|
|
47
|
-
A derive is a method on your `@database` class, alongside the collections it
|
|
48
|
-
reads and the `View` it writes:
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
@database
|
|
52
|
-
class MyDb {
|
|
53
|
-
@collection static events: Events<Key, Fact>; // a source
|
|
54
|
-
@collection static home: View<Key, HomePage>; // the materialized view
|
|
55
|
-
|
|
56
|
-
@derive
|
|
57
|
-
rebuild(): void {
|
|
58
|
-
// read sources, build the value, publish it
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
Rules:
|
|
64
|
-
|
|
65
|
-
- A `@derive` method takes **no arguments and returns `void`**.
|
|
66
|
-
- A `@database` may declare **multiple** `@derive` methods; each is run
|
|
67
|
-
independently.
|
|
68
|
-
- The view value (`HomePage` above) and the key are ordinary `@data` types, so
|
|
69
|
-
they round-trip through the codec like any other stored value.
|
|
70
|
-
|
|
71
|
-
## `View<K, V>`
|
|
72
|
-
|
|
73
|
-
A `View` is a published, read-optimized projection. Its API:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
view.get(key) // V | null - the published view, or null if none yet
|
|
77
|
-
view.require(key) // V - like get, but traps if nothing is published
|
|
78
|
-
view.publish(key, value) // void - overwrite the view (derive/job only)
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
`publish` is only allowed from a `@derive` (or a `@job`); the host assigns the
|
|
82
|
-
version so a later publish always supersedes an earlier one. `get`/`require` are
|
|
83
|
-
plain keyed reads, allowed from any handler, including a `@get` route.
|
|
84
|
-
|
|
85
|
-
## When derives run
|
|
86
|
-
|
|
87
|
-
You never call a derive yourself. The runtime runs it for you:
|
|
88
|
-
|
|
89
|
-
- **After a write to a source.** When a request writes one of a database's
|
|
90
|
-
source collections (an `events.append`/`append_once`, a `counter.add`, or a
|
|
91
|
-
record `create`/`patch`), that database's derives run right after the response
|
|
92
|
-
is produced, so the view reflects the new data on the next read. Many writes to
|
|
93
|
-
one database in a single request coalesce into one recompute.
|
|
94
|
-
- **On box load.** When a server box starts or hot-reloads (or the underlying
|
|
95
|
-
source data changed out of band), the views are rebuilt from their sources
|
|
96
|
-
before the first read is served. This is also where a value type's `@migrate`
|
|
97
|
-
runs against old stored events, as the derive re-reads and republishes them.
|
|
98
|
-
|
|
99
|
-
A derive's own writes (its `view.publish`) never re-trigger it.
|
|
100
|
-
|
|
101
|
-
The same code runs under `toiljs dev` (the in-process emulator) and on the
|
|
102
|
-
production edge, no flags or wiring to change.
|
|
103
|
-
|
|
104
|
-
## Reading a view from a route
|
|
105
|
-
|
|
106
|
-
The route just reads the view by key, which is a non-scan read and so is legal in
|
|
107
|
-
a `@get`:
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
@rest('guestbook')
|
|
111
|
-
class Guestbook {
|
|
112
|
-
@get('/')
|
|
113
|
-
list(): GuestbookView {
|
|
114
|
-
const key = new GuestKey('main');
|
|
115
|
-
const view = GuestbookDb.book.get(key);
|
|
116
|
-
return view == null ? new GuestbookView() : view; // empty until first publish
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
@post('/')
|
|
120
|
-
sign(input: NewMessage): GuestbookView {
|
|
121
|
-
const key = new GuestKey('main');
|
|
122
|
-
GuestbookDb.entries.append(key, new GuestEntry(input.author, input.message, 0));
|
|
123
|
-
GuestbookDb.totals.add(key, 1);
|
|
124
|
-
// The @derive republishes `book` right after this action returns, so the
|
|
125
|
-
// entries list is served by GET. The action just acks with the new total
|
|
126
|
-
// (a counter read is allowed here; a scan is not).
|
|
127
|
-
const view = new GuestbookView();
|
|
128
|
-
view.total = GuestbookDb.totals.get(key);
|
|
129
|
-
return view;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## How it fits together (the guestbook)
|
|
135
|
-
|
|
136
|
-
The `examples/basic` guestbook is the end-to-end demo:
|
|
137
|
-
|
|
138
|
-
1. `POST /guestbook` (an action) appends the signature to an `Events` stream and
|
|
139
|
-
bumps a `Counter`. It returns the running total, but it does **not** read the
|
|
140
|
-
entry list (that would be a scan).
|
|
141
|
-
2. The runtime then runs `@derive recompute()` under the derive kind: it scans
|
|
142
|
-
`entries.latest(...)`, reads the `totals` counter, and `publish`es a fresh
|
|
143
|
-
`GuestbookView`.
|
|
144
|
-
3. `GET /guestbook` (a query) reads `book.get(...)`, a single keyed read, and
|
|
145
|
-
returns the precomputed total + newest entries.
|
|
146
|
-
|
|
147
|
-
Sign twice and the total climbs across requests, because the data lives in
|
|
148
|
-
ToilDB (and its view), not in module memory.
|
|
149
|
-
|
|
150
|
-
## Notes
|
|
151
|
-
|
|
152
|
-
- A derive **recomputes** the view from whatever its method reads (here, the
|
|
153
|
-
latest 10 events). It is a fresh recompute on each trigger, so it suits views
|
|
154
|
-
built from a bounded read (latest N, a counter total, a small set). Folding an
|
|
155
|
-
unbounded full event log incrementally is a separate, more advanced pattern.
|
|
156
|
-
- Because publishes are last-writer-wins and a derive recomputes from the source
|
|
157
|
-
of truth, a view always converges to a correct snapshot of its sources.
|
|
158
|
-
- See also: [`data.md`](data.md) for `@data` value types, and the ToilDB host
|
|
159
|
-
ABI for the exact `derive_run` / `toildb.derives` contract.
|