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
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.