toiljs 0.0.85 → 0.0.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +2 -2
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +303 -293
  5. package/build/compiler/.tsbuildinfo +1 -1
  6. package/build/compiler/config.d.ts +2 -0
  7. package/build/compiler/config.js +1 -0
  8. package/build/compiler/docs.js +8 -24
  9. package/build/compiler/generate.js +4 -2
  10. package/build/compiler/index.d.ts +1 -1
  11. package/build/compiler/index.js +69 -6
  12. package/build/compiler/toil-docs.generated.js +64 -22
  13. package/build/devserver/.tsbuildinfo +1 -1
  14. package/build/devserver/analytics/index.js +7 -3
  15. package/build/devserver/db/database.d.ts +4 -0
  16. package/build/devserver/db/database.js +43 -1
  17. package/build/devserver/db/index.d.ts +1 -1
  18. package/build/devserver/db/index.js +1 -1
  19. package/build/devserver/db/types.d.ts +3 -0
  20. package/build/devserver/db/types.js +2 -0
  21. package/build/devserver/runtime/module.js +4 -1
  22. package/docs/README.md +104 -65
  23. package/docs/auth/README.md +102 -0
  24. package/docs/auth/configuration.md +94 -0
  25. package/docs/auth/extending.md +202 -0
  26. package/docs/auth/how-it-works.md +138 -0
  27. package/docs/auth/usage.md +188 -0
  28. package/docs/backend/README.md +143 -0
  29. package/docs/backend/data.md +351 -0
  30. package/docs/backend/rest.md +402 -0
  31. package/docs/backend/rpc.md +226 -0
  32. package/docs/background/README.md +114 -0
  33. package/docs/background/daemons.md +230 -0
  34. package/docs/background/derive.md +179 -0
  35. package/docs/cli/README.md +377 -0
  36. package/docs/concepts/config.md +416 -0
  37. package/docs/concepts/decorators.md +127 -0
  38. package/docs/concepts/security.md +108 -0
  39. package/docs/concepts/tiers.md +166 -0
  40. package/docs/concepts/types.md +216 -0
  41. package/docs/database/README.md +143 -0
  42. package/docs/database/capacity.md +350 -0
  43. package/docs/database/counters.md +174 -0
  44. package/docs/database/documents.md +255 -0
  45. package/docs/database/events.md +307 -0
  46. package/docs/database/membership.md +246 -0
  47. package/docs/database/setup.md +155 -0
  48. package/docs/database/unique.md +216 -0
  49. package/docs/database/views.md +246 -0
  50. package/docs/frontend/README.md +101 -0
  51. package/docs/frontend/data-fetching.md +243 -0
  52. package/docs/frontend/images.md +148 -0
  53. package/docs/frontend/metadata.md +344 -0
  54. package/docs/frontend/rendering.md +236 -0
  55. package/docs/frontend/routing.md +344 -0
  56. package/docs/frontend/scripts.md +118 -0
  57. package/docs/frontend/search.md +191 -0
  58. package/docs/frontend/styling.md +147 -0
  59. package/docs/getting-started/README.md +81 -0
  60. package/docs/getting-started/create-project.md +131 -0
  61. package/docs/getting-started/deploy.md +101 -0
  62. package/docs/getting-started/first-app.md +215 -0
  63. package/docs/getting-started/installation.md +106 -0
  64. package/docs/getting-started/migrating.md +125 -0
  65. package/docs/getting-started/project-structure.md +163 -0
  66. package/docs/introduction/README.md +41 -0
  67. package/docs/introduction/design-principles.md +55 -0
  68. package/docs/introduction/distributed.md +74 -0
  69. package/docs/introduction/how-it-works.md +74 -0
  70. package/docs/introduction/hyperscale.md +51 -0
  71. package/docs/introduction/modern-stack.md +36 -0
  72. package/docs/introduction/vs-other-frameworks.md +48 -0
  73. package/docs/introduction/why-toil.md +93 -0
  74. package/docs/llms.txt +90 -0
  75. package/docs/realtime/README.md +102 -0
  76. package/docs/realtime/channels.md +211 -0
  77. package/docs/realtime/streams.md +369 -0
  78. package/docs/services/README.md +60 -0
  79. package/docs/services/analytics.md +268 -0
  80. package/docs/services/caching.md +175 -0
  81. package/docs/services/cookies.md +235 -0
  82. package/docs/services/crypto.md +209 -0
  83. package/docs/services/email.md +289 -0
  84. package/docs/services/environment.md +141 -0
  85. package/docs/services/ratelimit.md +174 -0
  86. package/docs/services/time.md +85 -0
  87. package/examples/basic/client/routes/analytics.tsx +13 -12
  88. package/examples/basic/server/models/SiteAnalytics.ts +29 -17
  89. package/examples/basic/server/routes/Analytics.ts +15 -17
  90. package/examples/basic/server/routes/UserId.ts +22 -0
  91. package/package.json +15 -11
  92. package/scripts/gen-toil-docs.mjs +24 -35
  93. package/server/auth/AuthController.ts +336 -0
  94. package/server/auth/AuthUser.ts +23 -0
  95. package/server/auth/index.ts +16 -0
  96. package/server/globals/auth.ts +31 -0
  97. package/server/globals/userid.ts +107 -0
  98. package/src/compiler/config.ts +13 -0
  99. package/src/compiler/docs.ts +16 -33
  100. package/src/compiler/generate.ts +6 -2
  101. package/src/compiler/index.ts +114 -6
  102. package/src/compiler/toil-docs.generated.ts +64 -22
  103. package/src/devserver/analytics/index.ts +10 -4
  104. package/src/devserver/db/database.ts +67 -1
  105. package/src/devserver/db/index.ts +1 -0
  106. package/src/devserver/db/types.ts +13 -0
  107. package/src/devserver/runtime/module.ts +7 -0
  108. package/test/analytics-dev.test.ts +2 -1
  109. package/test/devserver-database.test.ts +113 -0
  110. package/docs/auth-todo.md +0 -149
  111. package/docs/auth.md +0 -322
  112. package/docs/caching.md +0 -115
  113. package/docs/cli.md +0 -17
  114. package/docs/client.md +0 -39
  115. package/docs/cookies.md +0 -457
  116. package/docs/crypto.md +0 -130
  117. package/docs/daemon.md +0 -123
  118. package/docs/data.md +0 -131
  119. package/docs/derive.md +0 -159
  120. package/docs/email.md +0 -326
  121. package/docs/environment.md +0 -97
  122. package/docs/getting-started.md +0 -128
  123. package/docs/index.md +0 -30
  124. package/docs/ratelimit.md +0 -95
  125. package/docs/routing.md +0 -259
  126. package/docs/rpc.md +0 -149
  127. package/docs/server.md +0 -61
  128. package/docs/ssr.md +0 -632
  129. package/docs/streams.md +0 -178
  130. package/docs/styling.md +0 -22
  131. package/docs/tiers.md +0 -133
  132. package/docs/time.md +0 -43
@@ -0,0 +1,166 @@
1
+ # Compute tiers
2
+
3
+ Your toiljs backend runs across four **compute tiers**, from a per-request handler at the very edge (L1) up to a single worldwide coordinator (L4). You write one project; the build decides which code belongs to which tier.
4
+
5
+ ## What a tier is
6
+
7
+ A **tier** is a place your server code runs, defined by two things: how *close* it sits to the user, and how *long* one copy of it lives. The Dacely **edge** is a fleet of servers in many cities. Some work wants to run as close to the user as possible for speed. Other work wants exactly one copy in the whole world so it stays coordinated. A tier is the framework's answer to "where and for how long should this piece of code live?"
8
+
9
+ There are four tiers, named L1 through L4. The number is the **scope**: L1 is the smallest and nearest (one edge node), and each higher number widens the scope until L4 covers the entire globe.
10
+
11
+ ```mermaid
12
+ flowchart TB
13
+ subgraph L4["L4 - Global (one leader worldwide)"]
14
+ direction TB
15
+ D["@daemon / @scheduled<br/>rollups, cleanup, polling"]
16
+ subgraph L3["L3 - Continental (per continent)"]
17
+ direction TB
18
+ C["@stream (Continental scope)<br/>cross-region coordination"]
19
+ subgraph L2["L2 - Regional (per region)"]
20
+ direction TB
21
+ R["@stream (Regional scope)<br/>chat, presence, live cursors"]
22
+ subgraph L1["L1 - Hot / edge (per request)"]
23
+ H["@rest, @service, @remote<br/>HTTP + RPC, the default"]
24
+ end
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ Read the diagram as concentric rings: L1 is the inner ring nearest the user, and each outer ring is a wider, longer-lived scope. Most of your code lives in the innermost ring.
31
+
32
+ ## The four tiers at a glance
33
+
34
+ | Tier | Name | How close | How many copies | Lives for | Runs |
35
+ | --- | --- | --- | --- | --- | --- |
36
+ | **L1** | Hot / edge | The exact node nearest the user | A fresh one per request | One request | `@rest`, `@service` / `@remote` |
37
+ | **L2** | Regional | A region (a group of nearby cities) | One box per connection | The connection | `@stream` (Regional scope) |
38
+ | **L3** | Continental | A continent | One box per connection | The connection | `@stream` (Continental scope) |
39
+ | **L4** | Global / daemon | One place worldwide | Exactly one leader (plus a warm standby) | Until redeployed | `@daemon`, `@scheduled` |
40
+
41
+ The tier names come straight from the edge runtime (its node roles are `Hot`, `Regional`, `Continental`, and `Daemon`).
42
+
43
+ ## L1: Hot (request/response)
44
+
45
+ **What:** a plain request-in, response-out handler. This is the default tier and where most code lives.
46
+
47
+ **How it behaves:** a fresh copy of your handler serves each request, on whatever node is nearest the caller. Nothing you store on a field survives to the next request, because the next request may be a brand-new copy on the other side of the planet. This is called being **stateless**. It is what lets your app scale to the whole world with no coordination. When you need something to persist, you write it to [ToilDB](../database/README.md), the shared database.
48
+
49
+ L1 is the lowest latency tier: the code runs on the node the user already reached, so there is no extra hop.
50
+
51
+ ```ts
52
+ // L1: a fresh box per request. `hits` does NOT persist across requests.
53
+ @rest('hello')
54
+ class Hello {
55
+ private hits: i32 = 0; // reset every request
56
+
57
+ @get('/')
58
+ public greet(): Response {
59
+ this.hits = this.hits + 1; // always 1
60
+ return Response.text('hi\n');
61
+ }
62
+ }
63
+ ```
64
+
65
+ See [REST](../backend/rest.md) and [RPC](../backend/rpc.md).
66
+
67
+ ## L2 and L3: streams (long-lived connections)
68
+
69
+ **What:** a **stream** is a long-lived connection (think a chat socket) where the server keeps state *per connected client* for as long as they stay connected.
70
+
71
+ **How it behaves:** when a client connects, the edge creates one resident **box** (one running copy of your `@stream` class) and pins it to a single worker. That same box handles every message from that client and is torn down when they leave. Because the box stays alive, its fields *do* persist across events, unlike L1. This is what makes a running message count, a game session, or a live cursor possible.
72
+
73
+ A `@stream` picks its scope with `@stream({ scope })`:
74
+
75
+ - **`StreamScope.Regional`** puts the box in the user's region (L2). Closest, lowest latency, smallest blast radius. Use it for per-connection state that only that one client cares about.
76
+ - **`StreamScope.Continental`** puts the box at a continent-wide node (L3). A little farther, but many clients across a continent can be steered to the *same* box, which is what you want when connected users must share state (a shared room, a leaderboard everyone updates).
77
+
78
+ The trade-off is the theme of this whole page: nearer (L2) is faster; wider (L3) lets more clients coordinate on one box.
79
+
80
+ ```ts
81
+ // L2/L3: the box is resident, so `count` survives every message.
82
+ @stream({ scope: StreamScope.Regional })
83
+ class Echo {
84
+ private count: i32 = 0;
85
+
86
+ @connect onConnect(): StreamOutbound {
87
+ this.count = 0;
88
+ return StreamOutbound.accept();
89
+ }
90
+ @message onMessage(pkt: StreamPacket): StreamOutbound {
91
+ this.count = this.count + 1; // persists across events
92
+ return StreamOutbound.empty();
93
+ }
94
+ @close onClose(): void { /* box torn down after this */ }
95
+ }
96
+ ```
97
+
98
+ See [Realtime streams](../realtime/streams.md).
99
+
100
+ ## L4: the daemon (global coordination)
101
+
102
+ **What:** the daemon is a single, elected leader for your whole app, running recurring background work on a schedule.
103
+
104
+ **How it behaves:** unlike every other tier, there is exactly **one** live daemon box in the entire world for your domain (with a warm standby ready to take over if the leader fails). It is not tied to any request or connection. It wakes on a cadence (an interval like `"1h"`, or a cron schedule) and does coordination work: rolling up analytics, cleaning up expired rows, polling an upstream API. Because there is only ever one leader, a `@scheduled` task runs **at most once** per due tick across the planet, which is exactly what you want for work that must not double-fire.
105
+
106
+ L4 is the highest-latency, most-consistent tier: it is far from any single user, but it is the one place where "do this exactly once, globally" is true.
107
+
108
+ ```ts
109
+ // L4: one leader worldwide runs this hourly.
110
+ @daemon
111
+ class Jobs {
112
+ @scheduled('1h')
113
+ hourly(): void {
114
+ // rollups, cleanup, polling an upstream, ...
115
+ }
116
+ }
117
+ ```
118
+
119
+ See [Daemons](../background/daemons.md).
120
+
121
+ ## How code is routed to a tier
122
+
123
+ You do not configure tiers. You opt into one by adding an **entry file** with the right name and using the matching **surface decorator**. A surface decorator is the top-level decorator that says what kind of endpoint a class is (`@rest`, `@stream`, `@daemon`).
124
+
125
+ At build time, `toiljs build` runs the compiler once per tier and hands each pass only the code that belongs to it. Each tier compiles into its own WebAssembly file (its own `.wasm` **artifact**), so the request build never carries stream or daemon code, and vice versa.
126
+
127
+ | Entry file (`server/`) | Surface decorator | Artifact | Tier |
128
+ | --- | --- | --- | --- |
129
+ | `main.ts` | `@rest`, `@service` / `@remote` | `release.wasm` | L1 request |
130
+ | `main.stream.ts` | `@stream` | `release-stream.wasm` | L2 / L3 stream |
131
+ | `main.daemon.ts` | `@daemon`, `@scheduled` | `release-cold.wasm` | L4 daemon |
132
+
133
+ Shared building blocks carry no tier of their own. Your [`@data` classes](../backend/data.md) and your [`@database` schema](../database/README.md) are compiled into *every* artifact, because any tier may need to read or write the database. The stream and daemon tiers are opt-in: a project with no `@stream` and no `@daemon` just builds the single `release.wasm` and behaves exactly like a request-only app.
134
+
135
+ For the entry-file conventions and the full build flow, see [Project structure](../getting-started/project-structure.md).
136
+
137
+ ## Why tiers matter: latency vs consistency
138
+
139
+ Picking a tier is a trade-off between two things a junior developer should keep in mind:
140
+
141
+ - **Latency** (how fast a single user gets an answer): lower tiers win. L1 runs on the node the user already reached.
142
+ - **Consistency and shared state** (many actors agreeing on one thing): higher tiers win. L4 has exactly one copy in the world, so it is trivially consistent, but it is far from everyone.
143
+
144
+ A rule of thumb:
145
+
146
+ - Answering one request as fast as possible: **L1**.
147
+ - Keeping per-connection state for one live client: **L2**.
148
+ - Letting many connected clients across a continent share one box's state: **L3**.
149
+ - Doing a job exactly once, globally, on a schedule: **L4**.
150
+
151
+ ## Gotchas
152
+
153
+ - **L1 fields reset every request.** If you set a field in one request and read it in the next, it will be gone. Persist to [ToilDB](../database/README.md) instead.
154
+ - **A project using `@stream` may not also declare `@service` or `@remote`** anywhere (the compiler enforces this). Streams and RPC live in different artifacts. Keep them in separate entry files (`main.stream.ts` vs `main.ts`).
155
+ - **There is at most one `@daemon` class per project.** It compiles only into the cold artifact; a `@daemon` in the request build is a compile error.
156
+ - **Higher tier is not "better".** Do not reach for L4 to hold shared state you read on every request; that adds a long hop. Use the database for shared reads, and the daemon only for scheduled, run-once work.
157
+
158
+ ## Related
159
+
160
+ - [Backend overview](../backend/README.md): the L1 request model in depth.
161
+ - [REST](../backend/rest.md) and [RPC](../backend/rpc.md): the L1 surfaces.
162
+ - [Realtime streams](../realtime/streams.md): the L2 / L3 surface and `StreamScope`.
163
+ - [Daemons](../background/daemons.md) and [@derive](../background/derive.md): scheduled and background work.
164
+ - [The database (ToilDB)](../database/README.md): where shared, persistent state lives.
165
+ - [Decorators](./decorators.md): the full list of surface decorators and their tiers.
166
+ - [Project structure](../getting-started/project-structure.md): entry files and the build.
@@ -0,0 +1,216 @@
1
+ # The server type system
2
+
3
+ Your backend is TypeScript, but a stricter, faster dialect of it. Numbers have an exact bit width (`u32`, `i64`, `f64`, and friends) instead of one loose `number`, and a few TypeScript features do not exist. This page explains the types you write in `server/` code.
4
+
5
+ ## Why the server types are different
6
+
7
+ Your frontend TypeScript runs in a browser, where a JavaScript engine figures out types as it goes. Your **server** code is different: the **toilscript** compiler turns it into [WebAssembly](../backend/README.md) (WASM), a compact machine-like binary. To emit that binary, the compiler must know the *exact* size of every value ahead of time. "A number" is not enough; it needs to know "a 32-bit unsigned integer" so it can pick the right machine instruction and lay out memory.
8
+
9
+ toilscript is built on **AssemblyScript**, a strict subset of TypeScript designed to compile to WASM. So the server language looks and reads like TypeScript, but every number is a fixed-width type, and there is no room for the fuzzy parts of JavaScript. The payoff is speed and safety: your code compiles to something close to hand-written native code, and whole classes of bugs cannot occur.
10
+
11
+ You already know the syntax. You only need to learn the number types and a handful of rules.
12
+
13
+ ## The number types
14
+
15
+ There is no plain `number` in server code. Instead you pick the exact type. The name tells you three things: signed or unsigned, how many bits, integer or float.
16
+
17
+ - The letter: `u` = unsigned integer (zero and up), `i` = signed integer (can be negative), `f` = floating-point (has a fractional part).
18
+ - The number: how many bits wide it is.
19
+
20
+ | Type | Kind | Bits | Range | Use it for |
21
+ | --- | --- | --- | --- | --- |
22
+ | `bool` | boolean | 1 | `true` / `false` | flags, yes/no |
23
+ | `u8` | unsigned int | 8 | 0 to 255 | a byte, a small enum |
24
+ | `u16` | unsigned int | 16 | 0 to 65,535 | ports, small counts |
25
+ | `u32` | unsigned int | 32 | 0 to ~4.29 billion | sizes, ids, most counts |
26
+ | `u64` | unsigned int | 64 | 0 to ~1.8e19 | timestamps (ms), big counters, large ids |
27
+ | `i8` | signed int | 8 | -128 to 127 | tiny signed values |
28
+ | `i16` | signed int | 16 | -32,768 to 32,767 | small signed values |
29
+ | `i32` | signed int | 32 | ~-2.1 to 2.1 billion | the default integer; loop counters, deltas |
30
+ | `i64` | signed int | 64 | ~-9.2e18 to 9.2e18 | large signed counts, database counters |
31
+ | `f32` | float | 32 | ~7 decimal digits | low-precision decimals (rare) |
32
+ | `f64` | float | 64 | ~15-16 decimal digits | any value with a fraction: prices, ratios, math |
33
+
34
+ There are also 128-bit and 256-bit integers (`u128`, `i128`, `u256`, `i256`) for cryptography and very large ids. See [Big integers](#big-integers-u128-to-u256) below.
35
+
36
+ ### Which one should I pick?
37
+
38
+ When in doubt, use these defaults and you will rarely be wrong:
39
+
40
+ - A counter, a size, a loop index, an id you do not do math on: **`u32`** (or `i32` if it can go negative).
41
+ - A millisecond timestamp, a like count that might grow huge, or a value that must never wrap: **`u64`** / **`i64`**.
42
+ - Anything with a decimal point (money, averages, percentages): **`f64`**.
43
+ - A true/false flag: **`bool`**.
44
+ - One raw byte, or an element of a byte buffer: **`u8`**.
45
+
46
+ Smaller types do not make your program meaningfully faster; they exist to match binary layouts and save memory in big arrays. Default to 32-bit unless a value is genuinely large or genuinely a byte.
47
+
48
+ ## Integer overflow: it wraps, it does not throw
49
+
50
+ This is the single most important difference from everyday JavaScript. Integer math is **modular**: if a value goes past the top of its range, it silently wraps around to the bottom (two's complement). It never throws an error and never turns into a bigger type on its own.
51
+
52
+ ```ts
53
+ let x: u8 = 255;
54
+ x = x + 1; // x is now 0, not 256 (wrapped around)
55
+
56
+ let y: u8 = 0;
57
+ y = y - 1; // y is now 255 (wrapped the other way)
58
+
59
+ let z: i32 = 2147483647; // the largest i32
60
+ z = z + 1; // z is now -2147483648 (wrapped to the minimum)
61
+ ```
62
+
63
+ The lesson: choose a type wide enough that your values cannot reach its edge. A counter that could exceed ~4.29 billion needs `u64`, not `u32`. Timestamps in milliseconds always use `u64`.
64
+
65
+ Floating-point (`f32` / `f64`) does not wrap; it follows the usual IEEE rules (very large values become `Infinity`, `0/0` is `NaN`).
66
+
67
+ ## Casting between number types
68
+
69
+ Because types are explicit, the compiler will not silently mix them. To convert, cast. There are two equivalent spellings; use whichever reads better:
70
+
71
+ ```ts
72
+ const big: u64 = 300;
73
+ const small: u8 = big as u8; // "as" form
74
+ const small2 = u8(big); // call the type like a function
75
+ ```
76
+
77
+ Casting a wider integer into a narrower one **truncates**: it keeps only the low bits, so `u8(300)` is `44` (300 wraps modulo 256). Casting a float to an integer drops the fraction toward zero (`i32(3.9)` is `3`). None of these throw, so cast deliberately.
78
+
79
+ ```ts
80
+ const f: f64 = 3.9;
81
+ const i = i32(f); // 3 (fraction dropped)
82
+ const n = u8(300); // 44 (truncated to 8 bits)
83
+ ```
84
+
85
+ Integer division also truncates (it is not float division):
86
+
87
+ ```ts
88
+ const half = 5 / 2; // both operands are i32 -> result is 2, not 2.5
89
+ const real = 5.0 / 2.0; // f64 division -> 2.5
90
+ ```
91
+
92
+ ## `bool`
93
+
94
+ `bool` is a real 1-bit type, not "any truthy value". Comparisons (`==`, `<`, `>=`) produce a `bool`, and `if`/`while` expect one. It stores as 0 or 1.
95
+
96
+ ## Big integers: `u128` to `u256`
97
+
98
+ For values too large for 64 bits, toilscript provides `u128`, `i128`, `u256`, and `i256` as native global types (no import needed). They support the normal operators (`+`, `-`, `*`, `/`, `==`, `<`, `<<`, and so on) through operator overloading, plus static helpers like `u256.fromString(...)` and instance methods like `.toString()`, `.toBytes()`, and `.toUint8Array()`.
99
+
100
+ ```ts
101
+ const a = u256.fromString('123456789012345678901234567890');
102
+ const b = a + u256.One;
103
+ const hex = b.toString(16);
104
+ ```
105
+
106
+ These are mainly for cryptography and very large ids. Everyday counts and sizes should stay in `u32` / `u64`.
107
+
108
+ ### `ToilUserId`: a 256-bit identity
109
+
110
+ The built-in auth system represents a logged-in user as a **`ToilUserId`**, a stable 256-bit value (four `u64` words) derived from their key, identifier, and your domain. It is a global type, like `crypto`. It is the right key to store per-user data on. Comparison is overloaded and O(1), and `.toHex()` gives you a 64-character string key.
111
+
112
+ ```ts
113
+ const id: ToilUserId = AuthService.userId()!; // the current user's stable id
114
+ const key = id.toHex(); // a convenient string key
115
+ ```
116
+
117
+ Full reference (including the `==` null-check gotcha) is in [Extending auth](../auth/extending.md).
118
+
119
+ ## Strings
120
+
121
+ Strings work as you expect: `'hello'`, template literals, `.length`, `.substring(...)`, `+` to concatenate. Under the hood a server string is **UTF-16** (the same 16-bit code units as JavaScript), so `.length` counts code units, not visible characters or bytes.
122
+
123
+ When you need the **bytes** of a string (to hash it, write it to a binary body, or send it over a stream), encode it explicitly. UTF-8 is almost always what you want on the wire:
124
+
125
+ ```ts
126
+ const buf: ArrayBuffer = String.UTF8.encode('hello'); // UTF-8 bytes
127
+ const text: string = String.UTF8.decode(buf); // back to a string
128
+ ```
129
+
130
+ `String.UTF8.byteLength(s)` gives the encoded byte length. There is a matching `String.UTF16` namespace when you need raw UTF-16 bytes.
131
+
132
+ ## Binary data: `Uint8Array`
133
+
134
+ Raw bytes are a `Uint8Array` (a fixed-length array of `u8`), exactly like the browser type. It is the standard currency for request bodies, hashes, crypto keys, and stream packets.
135
+
136
+ ```ts
137
+ const bytes = new Uint8Array(32);
138
+ bytes[0] = 0xff; // each element is a u8
139
+ const n: i32 = bytes.length;
140
+ ```
141
+
142
+ You will also see `StaticArray<u8>` (a fixed-size, lower-overhead byte array) and `ArrayBuffer` (a raw buffer that `Uint8Array` and the encoders view). For most app code, `Uint8Array` is all you need.
143
+
144
+ ## Arrays and collections
145
+
146
+ Typed arrays and the familiar collection types are available and must be typed:
147
+
148
+ ```ts
149
+ const nums: i32[] = [1, 2, 3]; // Array<i32>
150
+ const names = new Array<string>();
151
+ const seen = new Map<string, u32>();
152
+ const tags = new Set<string>();
153
+ ```
154
+
155
+ `Array`, `Map`, and `Set` behave like their JavaScript counterparts (`.push`, `.get`, `.has`, `.forEach`), but every element type is fixed at compile time. There is no mixed-type array.
156
+
157
+ ## Objects: `@data` classes, not object literals
158
+
159
+ Server code has no free-form object type. A structured value is a **class**, and to send it between the browser and the server (or in and out of the database) you tag it `@data`. That makes the compiler generate a binary codec plus a matching client type. See [Data types](../backend/data.md).
160
+
161
+ ```ts
162
+ @data
163
+ class Player {
164
+ username: string = '';
165
+ score: u64 = 0;
166
+ constructor(username: string = '', score: u64 = 0) {
167
+ this.username = username;
168
+ this.score = score;
169
+ }
170
+ }
171
+ ```
172
+
173
+ ## Key differences from normal TypeScript
174
+
175
+ | Thing | Normal TypeScript | Server (toilscript) |
176
+ | --- | --- | --- |
177
+ | Numbers | one `number` | explicit `u8` / `i32` / `u64` / `f64` / ... |
178
+ | `number` type | everywhere | avoid it; pick a fixed-width type |
179
+ | `any` | allowed | not allowed: everything is typed |
180
+ | Integer overflow | numbers just get bigger | wraps around silently |
181
+ | `/` on integers | `5 / 2` is `2.5` | `5 / 2` is `2` (truncates) |
182
+ | Mixing number types | implicit | explicit cast (`x as u8` / `u8(x)`) |
183
+ | npm packages | `import` anything | only toilscript's standard library and toiljs APIs |
184
+ | `undefined` | common | use `null` (a `T | null`), not `undefined` |
185
+ | Objects | `{ a: 1 }` literals | a `class` (usually `@data`) |
186
+ | Strings | UTF-16 | UTF-16 (same), encode to bytes for the wire |
187
+
188
+ A few rules worth stating plainly:
189
+
190
+ - **No `any`.** Every value has a concrete type. This is what makes the code compile to WASM at all.
191
+ - **No arbitrary npm.** Server code cannot pull in npm packages; it uses the toilscript standard library (the types on this page) plus the toiljs host APIs (database, crypto, email, and so on). This is the sandbox that keeps the edge safe.
192
+ - **No plain `number`.** If you write `number`, it resolves to `f64` (a float), which is almost never what you want for an id or a count. Always pick an explicit type.
193
+ - **Use `null`, not `undefined`,** for "no value": a nullable is written `T | null` and you narrow it with an `if (x != null)` check or a `!` assertion when you are sure.
194
+
195
+ ## How server types cross to the browser
196
+
197
+ When a `@data` value or an RPC result travels to your frontend, the compiler maps each server type to a matching TypeScript type in the generated client. You do not write this mapping; it is generated. It matters because it tells you what your React code receives.
198
+
199
+ | Server type | Browser (generated TS) |
200
+ | --- | --- |
201
+ | `u8`, `u16`, `u32`, `i8`, `i16`, `i32`, `f32`, `f64` | `number` |
202
+ | `u64`, `i64`, `u128`, `i128`, `u256`, `i256` | `bigint` |
203
+ | `bool` | `boolean` |
204
+ | `string` | `string` |
205
+ | a `@data` class `T` | the generated class `T` |
206
+ | `T[]` | `T[]` |
207
+
208
+ The important row is the 64-bit-and-larger integers: they become `bigint` on the client and travel as decimal strings on the JSON wire, so they stay **exact at any size** (they never lose precision the way a giant JavaScript `number` would). See [RPC](../backend/rpc.md) for the generated client.
209
+
210
+ ## Related
211
+
212
+ - [Data types (`@data`)](../backend/data.md): defining structs that cross the wire and the database.
213
+ - [RPC and the generated client](../backend/rpc.md): how server types map to browser types.
214
+ - [Extending auth](../auth/extending.md): the `ToilUserId` 256-bit identity in full.
215
+ - [The database (ToilDB)](../database/README.md): the collections your typed keys and values live in.
216
+ - [Decorators](./decorators.md): the decorators referenced throughout this page.
@@ -0,0 +1,143 @@
1
+ # ToilDB
2
+
3
+ ToilDB is the database built into toiljs. It is **global**, it needs **no setup or connection string**, and your backend talks to it with plain typed method calls.
4
+
5
+ If you have used Postgres, MySQL, or MongoDB before, ToilDB will feel a little different, and this page explains why. If you have never used a database, that is fine too: read on and every term is defined as it appears.
6
+
7
+ ## What "a globally distributed edge database" means
8
+
9
+ Let us take that phrase one word at a time.
10
+
11
+ - A **database** is a place your program stores data so it is still there on the next request. Your handler runs, finishes, and forgets everything in memory (a fresh WebAssembly instance runs each request), so anything you want to keep, a user account, a like count, a guestbook entry, has to go into a database.
12
+ - The **edge** is a network of servers spread around the world, each one physically close to some of your users. Your compiled backend runs on the edge server nearest whoever is calling it. (For the full picture of where code runs, see [Tiers](../concepts/tiers.md).)
13
+ - **Globally distributed** means the database is not one machine in one city. Its data lives on many machines in many regions at once. A user in Tokyo and a user in Paris each read from a copy near them, so reads are fast everywhere instead of fast in one place and slow everywhere else.
14
+
15
+ Put together: ToilDB is storage that lives out on that same worldwide edge, next to your code, so a database read does not have to fly across an ocean and back.
16
+
17
+ ```mermaid
18
+ flowchart LR
19
+ subgraph edge["Dacely edge (worldwide)"]
20
+ direction LR
21
+ H1["Your backend<br/>(Tokyo)"] --> D1[("ToilDB<br/>copy near Tokyo")]
22
+ H2["Your backend<br/>(Paris)"] --> D2[("ToilDB<br/>copy near Paris")]
23
+ end
24
+ D1 <-->|"replication<br/>(keeps copies in sync)"| D2
25
+ UA["User in Tokyo"] --> H1
26
+ UB["User in Paris"] --> H2
27
+ ```
28
+
29
+ You never pick a region, open a connection, or run a migration script. You declare what you want to store (see [Setup](./setup.md)) and ToilDB is there.
30
+
31
+ ## Why seven families instead of one big "table"
32
+
33
+ Most databases give you one general-purpose tool: a table (or a collection) that you read and write however you like. That is flexible, but on a system spread across the whole planet, the flexible tool is also the slow and error-prone tool. The classic example is a counter. If two servers on opposite sides of the world both try to do "read the number, add one, write it back" at the same moment, one of the two increments is silently lost, because each read the same starting value.
34
+
35
+ ToilDB solves this by giving you **seven specialized collection types**, called **families**. Each family is tuned for one job and exposes only the operations that are safe and fast for that job. A counter family, for instance, has no "set to this value" operation at all: you can only `add` a delta, and the database merges concurrent deltas from around the world without losing any. You cannot misuse it, because the unsafe operation does not exist.
36
+
37
+ So instead of one generic table you reach for, you pick the family that matches what you are doing. Picking the right one is the main skill, and the guide below walks you through it.
38
+
39
+ The seven families are:
40
+
41
+ | Family | It stores | Read a page |
42
+ | --- | --- | --- |
43
+ | **Documents** | Records you look up by id (users, posts, orders). | [Documents](./documents.md) |
44
+ | **Unique** | A claim on a value that must be one-of-a-kind (usernames, emails, slugs). | [Unique](./unique.md) |
45
+ | **Counter** | A running total that many callers increment at once (likes, views). | [Counters](./counters.md) |
46
+ | **Events** | An append-only log of things that happened (feeds, audit trails). | [Events](./events.md) |
47
+ | **Capacity** | A limited quantity you hand out without overselling (tickets, seats). | [Capacity](./capacity.md) |
48
+ | **Membership** | Sets of "who belongs to what" (followers, tags, room members). | [Membership](./membership.md) |
49
+ | **View** | A precomputed, read-optimized snapshot (home pages, leaderboards). | [Views](./views.md) |
50
+
51
+ ## The one idea shared by all seven: key then value
52
+
53
+ Every family, underneath, works the same way: it maps a **key** to a **value**.
54
+
55
+ - A **key** is how you find your data. Think of it as the label on a drawer: a user id, a username, a room name.
56
+ - A **value** is what is in the drawer: the user record, the owner of a username, the members of a room.
57
+
58
+ In ToilDB, both the key and the value are ordinary TypeScript classes that you tag with `@data`. The `@data` tag tells toilscript (the compiler that turns your backend into WebAssembly) how to pack that class into bytes for storage and unpack it again. You write a normal class with normal fields, give each field a default, and the compiler does the rest.
59
+
60
+ ```ts
61
+ // A key: how you address one record.
62
+ @data
63
+ class UserId {
64
+ id: string = '';
65
+ constructor(id: string = '') { this.id = id; }
66
+ }
67
+
68
+ // A value: what you store under that key.
69
+ @data
70
+ class User {
71
+ id: string = '';
72
+ name: string = '';
73
+ score: u64 = 0;
74
+ }
75
+ ```
76
+
77
+ You then declare a collection that maps that key type to that value type, for example `Documents<UserId, User>`. Every family is generic over its key and value types in exactly this way, so once you understand `@data` keys and values you understand all seven. The full `@data` reference is on the [data types page](../backend/data.md); how to declare collections is on [Setup](./setup.md).
78
+
79
+ ## Eventual consistency, in plain words
80
+
81
+ Because ToilDB keeps copies of your data in many regions, there is one honest trade-off you need to understand: **eventual consistency**.
82
+
83
+ Every key has one **home**: a single region that officially owns that key's data. All **writes** to a key travel to its home, where they are applied one at a time, in order. That is what makes writes safe: even if a thousand servers write the same key at once, the home lines them up so nothing is lost or corrupted.
84
+
85
+ **Reads**, on the other hand, are served from the copy nearest the reader, which is fast but may be a beat behind. After a write lands at the home, it takes a brief moment (usually milliseconds) to fan out to the other regions' copies. During that moment, a reader in another region might still see the previous value. The copies always catch up; they are *eventually* consistent, not *instantly* consistent everywhere.
86
+
87
+ What this means in practice:
88
+
89
+ - **It is usually invisible.** The lag is tiny, and most apps never notice.
90
+ - **Do not assume a write is visible everywhere the instant it returns.** For example, right after you create a record, a read from a far-away region might briefly not see it yet.
91
+ - **Some families are stronger.** Because writes to a single key are serialized at its home, operations that must never race, claiming a unique username, reserving the last ticket, are decided at the home and are safe. The Unique and Capacity families lean on this so two callers can never both win. See [Unique](./unique.md) and [Capacity](./capacity.md).
92
+
93
+ Each family page spells out its own consistency behavior, so you always know what to expect.
94
+
95
+ ## Choosing a family: the decision guide
96
+
97
+ Start from what you are trying to do, not from a data structure. This table maps a real need to the family that was built for it.
98
+
99
+ | I need to... | Use | Why |
100
+ | --- | --- | --- |
101
+ | Store and update a thing I look up by id (a user, a post, an order). | **Documents** | The general-purpose record store: create, read, update, delete by key. |
102
+ | Guarantee a value is used by only one owner across the whole world (username, email, slug). | **Unique** | Claims are decided at the key's home, so two people cannot claim the same name. |
103
+ | Count something that many people bump at the same time (likes, page views, stock tally). | **Counter** | Concurrent `add`s from anywhere merge without ever losing an increment. |
104
+ | Keep a growing list of things that happened, in order (activity feed, audit log). | **Events** | Append-only: you add events and read the newest, but never edit history. |
105
+ | Hand out a limited quantity and never sell more than exist (tickets, seats, inventory). | **Capacity** | Reserve/confirm/cancel holds at the home prevent overselling. |
106
+ | Track which members belong to a set (followers, tags, room members, permissions). | **Membership** | Add/remove/check membership without loading a whole record to edit a list. |
107
+ | Serve a precomputed, ready-to-render result quickly (leaderboard, home page snapshot). | **View** | A background job computes it once; requests read it with a single fast lookup. |
108
+
109
+ If two families seem to fit, this flowchart resolves it. Read it top to bottom.
110
+
111
+ ```mermaid
112
+ flowchart TD
113
+ START["What are you storing?"] --> Q1{"Is it a running<br/>total (a number many<br/>callers increase)?"}
114
+ Q1 -->|Yes| COUNTER["Counter"]
115
+ Q1 -->|No| Q2{"Must a value be<br/>one-of-a-kind across<br/>everyone (username,<br/>email, slug)?"}
116
+ Q2 -->|Yes| UNIQUE["Unique"]
117
+ Q2 -->|No| Q3{"Is it a limited stock<br/>you must not oversell<br/>(tickets, seats)?"}
118
+ Q3 -->|Yes| CAPACITY["Capacity"]
119
+ Q3 -->|No| Q4{"Is it an append-only<br/>log of events that<br/>happened, kept in order?"}
120
+ Q4 -->|Yes| EVENTS["Events"]
121
+ Q4 -->|No| Q5{"Is it a set of members<br/>you add/remove/check<br/>(followers, tags)?"}
122
+ Q5 -->|Yes| MEMBERSHIP["Membership"]
123
+ Q5 -->|No| Q6{"Is it a precomputed<br/>read-only snapshot a<br/>background job builds?"}
124
+ Q6 -->|Yes| VIEW["View"]
125
+ Q6 -->|No| DOCUMENTS["Documents<br/>(the default record store)"]
126
+ ```
127
+
128
+ A quick sanity check when you land somewhere: **Documents is the default.** If you are storing "a thing with fields that I update by id," it is almost always Documents. The other six exist for the specific jobs above where Documents would be slow, unsafe under concurrency, or awkward.
129
+
130
+ Real apps mix families freely. The demo guestbook, for example, uses **Events** for the log of signatures, a **Counter** for the running total, and a **View** for the ready-to-serve snapshot, all in one small feature.
131
+
132
+ ## Where to go next
133
+
134
+ - [Setup](./setup.md): declare a database, its collections, and reach them from a handler.
135
+ - [Documents](./documents.md): the general-purpose record store (start here).
136
+ - [Unique](./unique.md), [Counters](./counters.md), [Events](./events.md), [Views](./views.md), [Membership](./membership.md), [Capacity](./capacity.md): the specialized families.
137
+
138
+ ## Related
139
+
140
+ - [Setup](./setup.md): how to declare `@database`, `@collection`, and `@data` types.
141
+ - [Data types (`@data`)](../backend/data.md): the typed keys and values every family uses.
142
+ - [Tiers](../concepts/tiers.md): where your backend and its data run.
143
+ - [Decorators](../concepts/decorators.md): `@query`, `@action`, `@derive`, and friends.