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,369 @@
1
+ # Streams (`@stream`)
2
+
3
+ A `@stream` is a server class that handles one live connection from start to finish. You mark a class with `@stream`, add lifecycle hooks, and the Dacely edge keeps one instance of that class alive for as long as the browser stays connected.
4
+
5
+ ## What a stream is (and why it is different)
6
+
7
+ When you write an [HTTP route](../backend/rest.md), the server builds a **fresh** handler for every request and throws it away afterward. Anything you stored on the handler's fields is gone the moment the response is sent. That is perfect for one-shot requests, but it means the handler cannot "remember" anything between requests on its own.
8
+
9
+ A `@stream` is the opposite. It is a **resident box**: one live instance, created when a connection opens and kept alive until it closes. Because it is the *same* instance for every message on that connection, values you store on its fields **persist across messages**. That is what makes it the right tool for a conversation, a game session, or anything stateful that lasts for the life of a connection.
10
+
11
+ The word "resident" just means "stays in memory and keeps running." The word "box" is toiljs's name for one sandboxed instance of your compiled server code.
12
+
13
+ ```ts
14
+ @stream('echo')
15
+ class Echo {
16
+ private count: i32 = 0;
17
+
18
+ @connect
19
+ onConnect(): void {
20
+ this.count = 0; // a fresh connection starts a fresh box, so count begins at 0
21
+ }
22
+
23
+ @message
24
+ onMessage(packet: StreamPacket): StreamOutbound {
25
+ this.count = this.count + 1; // survives across messages: same box every time
26
+ const text = 'pong #' + this.count.toString();
27
+ return StreamOutbound.reply(Uint8Array.wrap(String.UTF8.encode(text)));
28
+ }
29
+
30
+ @close
31
+ onClose(): void {
32
+ // the box is destroyed after this hook runs
33
+ }
34
+ }
35
+ ```
36
+
37
+ Connect, send three messages, and you get back `pong #1`, `pong #2`, `pong #3`. The advancing number is proof that the same box handled all three.
38
+
39
+ ## Declaring a stream
40
+
41
+ Mark a class with `@stream` and give it a **name**. The name becomes the route the browser connects to.
42
+
43
+ ```ts
44
+ @stream('echo') // mounted at /echo
45
+ class Echo { /* ... */ }
46
+
47
+ @stream // bare form: the route is the class name (/Echo)
48
+ class Echo { /* ... */ }
49
+ ```
50
+
51
+ There are three forms:
52
+
53
+ - `@stream('name')`: an explicit mount name (connect at `/name`).
54
+ - `@stream` (bare): the mount name is the class name.
55
+ - `@stream({ ... })`: a config object (see [Configuration](#configuration) below).
56
+
57
+ ## The four lifecycle hooks
58
+
59
+ A stream method becomes a lifecycle hook when you tag it with one of these decorators. Every hook is **optional**: declare only the ones you need, and a missing hook is simply a no-op (it does nothing, it never crashes).
60
+
61
+ | Decorator | Fires when... |
62
+ | ------------- | ----------------------------------------------------------------------- |
63
+ | `@connect` | the connection opens (the box has just been created). |
64
+ | `@message` | an inbound frame arrives from the browser. |
65
+ | `@close` | the connection closes cleanly (the box is destroyed after this hook). |
66
+ | `@disconnect` | the connection is lost abruptly (network dropped, browser killed). |
67
+
68
+ A **frame** is one message: one call to `send()` on the client becomes one `@message` on the server.
69
+
70
+ ```mermaid
71
+ sequenceDiagram
72
+ participant B as Browser
73
+ participant S as @stream box
74
+ B->>S: open
75
+ activate S
76
+ Note over S: @connect runs, box is created
77
+ B->>S: frame "a"
78
+ S-->>B: @message -> reply
79
+ B->>S: frame "b"
80
+ S-->>B: @message -> reply
81
+ B->>S: close
82
+ Note over S: @close runs, box is destroyed
83
+ deactivate S
84
+ ```
85
+
86
+ ### `@connect`
87
+
88
+ Runs once, right after the box is created. Use it to set up per-connection state (reset a counter, read the requested path, decide whether to accept the connection). It can return a `StreamOutbound` to accept or reject (see below). The Echo example uses it to zero its counter.
89
+
90
+ **What it receives.** `@connect` is handed a `StreamInbound`, a small read-only object the host fills in with the details of the connection that just opened. It lets you look at *where* the connection came from before you decide to keep it.
91
+
92
+ | Member | Type | What it gives you |
93
+ | ------------- | -------- | ----------------------------------------------------------------------- |
94
+ | `streamId` | `u64` | A unique id for this connection. |
95
+ | `transport` | `i32` | A numeric tag for the transport that carried the connection. |
96
+ | `authority()` | `string` | The host the browser connected to (for example `example.com`). |
97
+ | `path()` | `string` | The path the browser opened (for example `/echo`). |
98
+
99
+ Watch the shape: `authority()` and `path()` are **methods** (call them with `()`), while `streamId` and `transport` are plain properties (no parentheses).
100
+
101
+ ```ts
102
+ @connect
103
+ onConnect(info: StreamInbound): StreamOutbound {
104
+ // Inspect where the connection is headed, then decide whether to keep it.
105
+ if (info.path() != '/echo') {
106
+ return StreamOutbound.reject(1); // refuse: any u16 reason code
107
+ }
108
+ this.count = 0; // fresh connection, fresh state
109
+ return StreamOutbound.accept(); // keep the connection open
110
+ }
111
+ ```
112
+
113
+ You do not have to take the argument. If the hook needs none of these details, declare it with no parameter (`onConnect(): void`), exactly like the Echo example above.
114
+
115
+ ### `@message`
116
+
117
+ Runs for every inbound frame. This is where most of your logic lives. It receives the frame and may reply. Details in [Reading and replying](#reading-and-replying-to-messages).
118
+
119
+ ### `@close` and `@disconnect`
120
+
121
+ Both mean "the connection is over," and both are your chance to clean up. The difference is *how* it ended:
122
+
123
+ - `@close` is a **graceful** close: the browser (or your server) ended it on purpose.
124
+ - `@disconnect` is an **abrupt** loss: the network dropped or the tab was killed with no goodbye.
125
+
126
+ After either one, the box is destroyed.
127
+
128
+ **What they receive.** Both hooks are handed a `StreamConnectionEvent`, a read-only summary of the connection that just ended.
129
+
130
+ | Member | Type | What it gives you |
131
+ | -------------- | ----- | ---------------------------------------------------------- |
132
+ | `connectionId` | `u64` | The id of the connection that ended. |
133
+ | `reason` | `u16` | A numeric close code explaining why it ended. |
134
+ | `durationMs` | `u64` | How long the connection stayed open, in milliseconds. |
135
+
136
+ All three are plain properties (getters), so read them without `()`.
137
+
138
+ ```ts
139
+ @close
140
+ onClose(ev: StreamConnectionEvent): void {
141
+ // Last chance to clean up. `ev` tells you how the connection ended.
142
+ const heldSeconds = ev.durationMs / 1000;
143
+ // e.g. record the session length or flush a buffer here.
144
+ }
145
+
146
+ @disconnect
147
+ onDisconnect(ev: StreamConnectionEvent): void {
148
+ // Same shape, but this fired because the connection dropped abruptly.
149
+ // ev.reason carries the close code the edge assigned to the drop.
150
+ }
151
+ ```
152
+
153
+ As with `@connect`, the argument is optional: declare `onClose(): void` if you do not need it.
154
+
155
+ ## Per-connection state (and its limits)
156
+
157
+ State on the box's fields lasts for **one connection**. It does **not** survive:
158
+
159
+ - a **reconnect**: if the browser drops and reopens, it gets a brand-new box that starts clean.
160
+ - a **different user**: every connection gets its own box, so one connection's state can never leak into another. This is a safety property, not just a convenience.
161
+
162
+ So treat box fields as **per-connection scratch space** only. For anything that must outlive the connection (a saved message, a score, who a user is across reconnects), write it to [the database](../database/README.md), not to a class field.
163
+
164
+ ## Reading and replying to messages
165
+
166
+ By default, a `@message` receives a `StreamPacket`, which is a thin view over the raw bytes that arrived, and returns a `StreamOutbound`, which stages the reply.
167
+
168
+ ```ts
169
+ @message
170
+ onMessage(packet: StreamPacket): StreamOutbound {
171
+ const raw = packet.bytes(); // the inbound frame as bytes
172
+ return StreamOutbound.reply(raw); // echo the same bytes back
173
+ }
174
+ ```
175
+
176
+ `StreamPacket` (the inbound frame):
177
+
178
+ | Member | What it gives you |
179
+ | ------------ | ---------------------------------------------------------- |
180
+ | `bytes()` | the whole frame as a `Uint8Array` (copy it if you keep it). |
181
+ | `length` | the number of bytes in the frame. |
182
+ | `at(i)` | the byte at index `i`. |
183
+
184
+ `StreamOutbound` (what you return):
185
+
186
+ | Call | Meaning |
187
+ | ----------------------------- | ------------------------------------------------------------------ |
188
+ | `StreamOutbound.reply(bytes)` | send one frame back to the browser. |
189
+ | `StreamOutbound.empty()` | accept the frame and send nothing back. |
190
+ | `StreamOutbound.reject(code)` | refuse (used from `@connect` to turn a connection away). |
191
+ | `StreamOutbound.accept()` | accept a connection with no reply frame. |
192
+
193
+ A `@message` may also return `void` when it never replies.
194
+
195
+ > **Bytes, not strings.** A frame is raw bytes on the wire. To send text, encode it with `String.UTF8.encode(...)` (server) or `new TextEncoder().encode(...)` (browser), and decode it with `new TextDecoder().decode(...)` on the other side.
196
+
197
+ ### Typed messages
198
+
199
+ Raw bytes are flexible but fiddly. If your messages are structured, declare a [`@data`](../backend/data.md) class and pass it as the stream's `message` type. Your `@message` hook then receives the **decoded object** instead of raw bytes.
200
+
201
+ ```ts
202
+ @data
203
+ class ChatMsg {
204
+ text: string = '';
205
+ constructor(text: string = '') { this.text = text; }
206
+ }
207
+
208
+ @stream({ message: ChatMsg })
209
+ class Chat {
210
+ @message
211
+ onMessage(msg: ChatMsg): StreamOutbound { // decoded @data, not raw bytes
212
+ const echoed = 'you said: ' + msg.text;
213
+ return StreamOutbound.reply(Uint8Array.wrap(String.UTF8.encode(echoed)));
214
+ }
215
+ }
216
+ ```
217
+
218
+ The reply is still raw (`StreamOutbound` deals in bytes). Only the **inbound** side is decoded for you. On the client, a typed stream lets you `send(new ChatMsg('hi'))` and toiljs encodes it for you.
219
+
220
+ ## Games and interactive apps
221
+
222
+ A stream box remembers state for the life of a connection, which is exactly what a game session needs. Each connected player gets their **own** box, and that box holds the player's live state (position, health, score) in memory, right next to the core handling their packets. The lifecycle hooks map cleanly onto a session: `@connect` spawns the player, `@message` handles each input, `@close` and `@disconnect` remove them.
223
+
224
+ ```ts
225
+ // server/streams/Match.ts
226
+ @data
227
+ class Move { // the input a player sends each tick
228
+ dx: i32 = 0;
229
+ dy: i32 = 0;
230
+ constructor(dx: i32 = 0, dy: i32 = 0) { this.dx = dx; this.dy = dy; }
231
+ }
232
+
233
+ @stream({ message: Move, scope: StreamScope.Regional })
234
+ class Match {
235
+ // Per-connection state: this player's position. It lives in memory for the
236
+ // whole session, on the one core that owns this connection.
237
+ private x: i32 = 0;
238
+ private y: i32 = 0;
239
+ private moves: u32 = 0;
240
+
241
+ @connect
242
+ onConnect(info: StreamInbound): StreamOutbound {
243
+ this.x = 50; // spawn point
244
+ this.y = 50;
245
+ this.moves = 0;
246
+ return StreamOutbound.accept();
247
+ }
248
+
249
+ @message
250
+ onMove(move: Move): StreamOutbound {
251
+ // Apply the input to this player's live position, then confirm it.
252
+ this.x = this.x + move.dx;
253
+ this.y = this.y + move.dy;
254
+ this.moves = this.moves + 1;
255
+ const state = '{"x":' + this.x.toString() + ',"y":' + this.y.toString() + '}';
256
+ return StreamOutbound.reply(Uint8Array.wrap(String.UTF8.encode(state)));
257
+ }
258
+
259
+ @disconnect
260
+ onDrop(ev: StreamConnectionEvent): void {
261
+ // The player left (tab closed, network died). Their box is destroyed
262
+ // next. Persist a score here if it must outlive the session.
263
+ }
264
+ }
265
+ ```
266
+
267
+ Every input a player sends is one `@message`, applied to **their** box's position and confirmed straight back to them with low latency. Because the box is resident, the player's position is always there in memory, on the same core, with no database round trip per move. That is what makes fast input loops (a game tick, a cursor drag, a live editor) feel instant.
268
+
269
+ **What this version does, and does not, do.** Each player has their own box, so this handles per-player input, per-player state, and instant confirmation back to that player. To make players see **each other** (the other half of multiplayer), one player's move must fan out to everyone else in the match. That cross-connection broadcast is the `@channel` feature, which is **planned, not live yet** (see [Channels](./channels.md)). Until it ships, the common patterns are to write shared match state to [the database](../database/README.md) and have clients read it, or to keep a match to a single authoritative box. The per-connection pieces above (input, state, and presence via `@connect` / `@disconnect`) work today.
270
+
271
+ For **presence** ("who is online"), `@connect` and `@disconnect` are your join and leave signals: bump a counter or write a row when a player connects, and undo it when they drop.
272
+
273
+ ## The `main.stream.ts` file (a separate tier)
274
+
275
+ Streams live in their **own entry file**, `server/main.stream.ts`, separate from the request entry `server/main.ts`. Importing your `@stream` classes there pulls them into a **separate compiled artifact**, `build/server/release-stream.wasm`.
276
+
277
+ ```ts
278
+ // server/main.stream.ts
279
+ import { revertOnError } from 'toiljs/server/runtime/abort/abort';
280
+
281
+ import './streams/Echo'; // add each @stream module here as you grow
282
+ import './streams/Chat';
283
+
284
+ // Re-export the WASM entry points the host binds, exactly like main.ts.
285
+ export * from 'toiljs/server/runtime/exports';
286
+ export function abort(message: string, fileName: string, line: u32, column: u32): void {
287
+ revertOnError(message, fileName, line, column);
288
+ }
289
+ ```
290
+
291
+ Why a separate file? A stream box and a request handler are **different kinds of program** that run on different parts of the edge (see [Compute tiers](../concepts/tiers.md)). toiljs compiles each into its own `.wasm`:
292
+
293
+ ```sh
294
+ $ toiljs build
295
+ $ ls build/server/*.wasm
296
+ build/server/release.wasm # L1 request (@rest / @service)
297
+ build/server/release-stream.wasm # L2/L3 stream (@stream)
298
+ build/server/release-cold.wasm # L4 daemon (@daemon)
299
+ ```
300
+
301
+ You do not run this by hand. `toiljs build` produces `release-stream.wasm` automatically whenever your project has a `@stream` surface, and shared helper code and `@data` types are compiled into every artifact.
302
+
303
+ > **One file cannot be both a stream and an RPC surface.** A single source file may not declare both a `@stream` and a `@service` / `@remote` ([RPC](../backend/rpc.md)), because one compiled artifact cannot be two tiers at once. Keep them in separate files: `@stream` in `main.stream.ts`, `@rest` / `@service` in `main.ts`. They coexist happily side by side in the same project, just not in the same file.
304
+
305
+ ## Configuration
306
+
307
+ The config-object form lets you tune a stream:
308
+
309
+ ```ts
310
+ @stream({
311
+ scope: StreamScope.Regional, // where the box runs (see below)
312
+ message: ChatMsg, // decode inbound frames into this @data type
313
+ maxFrameBytes: 65536, // reject frames larger than this
314
+ ingressRingBytes: 262144 // size of the inbound buffer
315
+ })
316
+ class Chat { /* ... */ }
317
+ ```
318
+
319
+ - **`scope`** picks how close to the user the box runs. `StreamScope.Regional` (the default) runs it at a regional node; `StreamScope.Continental` runs it at a wider continental node. See [Compute tiers](../concepts/tiers.md) for what L2 and L3 mean.
320
+ - **`message`** is the typed-message shortcut described above.
321
+ - **`maxFrameBytes`** and **`ingressRingBytes`** cap frame size and buffer size to protect the box from oversized or flooding input.
322
+
323
+ ## Reaching a stream from the browser
324
+
325
+ Every `@stream` class gets a generated, typed client at `Server.Stream.<ClassName>`, wired up for you in `shared/server.ts` (the same place the [RPC](../backend/rpc.md) client lands). Call `connect()` to open the connection:
326
+
327
+ ```ts
328
+ import '../shared/server'; // attaches globalThis.Server (browser-only)
329
+
330
+ const chat = await Server.Stream.Echo.connect();
331
+ chat.onMessage((bytes) => { /* a reply frame, always raw bytes */ });
332
+ chat.send(new TextEncoder().encode('hello'));
333
+ chat.onClose((code) => { /* the connection ended */ });
334
+ chat.close();
335
+ ```
336
+
337
+ The client is keyed by the **class name** (`Server.Stream.Echo`) and connects to the class's **mount route** (`/echo`). Inbound replies are always raw bytes. The full client walkthrough, plus the lower-level `useChannel` hook, is in [Channels](./channels.md).
338
+
339
+ ## How placement works (you do not manage it)
340
+
341
+ On the production edge, your box lives in **one place** for the connection's whole life. Every message from that connection is routed to the exact instance holding your box, so its in-memory state is always there. You never configure this; the edge does it automatically. In `toiljs dev` there is only one process, so this is a non-issue.
342
+
343
+ ## Why this scales
344
+
345
+ A `@stream` box is deliberately cheap to run at scale, and the edge is built to spread a very large number of them across many machines and cities. Three design choices do the heavy lifting:
346
+
347
+ - **Connections spread across the fleet in parallel.** Each connection is handled on its own, with no shared lock in the middle for connections to fight over. There is no central bottleneck every connection has to pass through, so adding machines adds capacity in a straight line.
348
+ - **The connection survives network changes.** If a client's network changes (Wi-Fi to cellular, or a NAT rebind), the edge keeps the connection attached to the same box. The user keeps their session and their in-memory state; the box is never orphaned.
349
+ - **Each box is isolated and bounded.** Every connection gets its own sandboxed box with its own linear memory. One tenant's boxes are capped to a slice of the node's RAM (a noisy-neighbor guard), and a single box is hard-capped (64 MiB) so a runaway connection can only fill itself, then it traps. Each lifecycle hook also runs under a hard compute cap, so a hook that loops forever is stopped instead of hogging the machine.
350
+
351
+ Put together: connections spread across the fleet, run in parallel, the session sticks to its box even when the network moves, and each box is walled off from the others. That is what lets one deployment hold a very large number of live connections at once. How large depends on your hardware and message sizes; these docs do not publish a benchmarked number.
352
+
353
+ > **The wider picture.** For how the edge spreads connections across the fleet and the mesh adds up to world-wide fan-out, see [Built for massive fan-out and world-wide sync](./README.md#built-for-massive-fan-out-and-world-wide-sync) in the overview.
354
+
355
+ ## Gotchas
356
+
357
+ - **Box fields are per-connection only.** They reset on reconnect and are never shared between users. Persist anything durable to [the database](../database/README.md).
358
+ - **Frames are bytes.** Encode and decode text yourself, or use a typed `message` so toiljs does it.
359
+ - **Copy `packet.bytes()` if you keep it.** The inbound buffer is reused after the hook returns, so store a copy if you need the bytes later.
360
+ - **A file cannot mix `@stream` with `@service` / `@remote`.** Keep streams in `main.stream.ts`.
361
+ - **`@channel` is not live yet.** A stream that declares a `@channel` hook is rejected by the edge today. Broadcasting to many subscribers is a planned feature; see [Channels](./channels.md).
362
+
363
+ ## Related
364
+
365
+ - [Realtime overview](./README.md): the big picture and when to reach for realtime.
366
+ - [Channels](./channels.md): the client `useChannel` hook and a chat-style example.
367
+ - [Compute tiers (L1 to L4)](../concepts/tiers.md): where the stream artifact runs.
368
+ - [Data types (`@data`)](../backend/data.md): typed messages.
369
+ - [The database (ToilDB)](../database/README.md): where to keep state that outlives a connection.
@@ -0,0 +1,60 @@
1
+ # Platform services
2
+
3
+ Your toiljs backend runs on the **Dacely edge**: a fleet of servers spread around the world that sit close to your users and run your compiled backend (a small, sandboxed WebAssembly program). "Platform services" are the batteries the edge includes so you do not have to run your own: response caching, rate limiting, secrets, email, analytics, cryptography, cookies, and a clock.
4
+
5
+ Each service is either a **route decorator** (an annotation you put above a route, like `@cache`) or an **ambient global** (an object you can use without importing it, like `Environment` or `Time`). Nothing here needs a separate server, a database you manage, or a third-party account for the basics.
6
+
7
+ ## What each service is (and when you reach for it)
8
+
9
+ - **[Caching](./caching.md)** stores a copy of a response so the edge can hand it back instantly, without re-running your code. Reach for it when a route returns the same public data to many users (a leaderboard, a product listing, a marketing page). Skip it for anything personalized.
10
+ - **[Rate limiting](./ratelimit.md)** caps how often one client can hit a route. Reach for it on anything abusable: login, signup, "send me a code", a public write API. It stops brute-force and spam before your code even runs.
11
+ - **[Environment and secrets](./environment.md)** gives your app configuration (like a public API base URL) and secrets (like a payment provider key) that are set outside your code, so your compiled program never carries a credential. Reach for it any time you call a third party or need a value that differs between dev and production.
12
+ - **[Email](./email.md)** sends transactional email (verification codes, password resets, receipts) through a provider you configure once. Reach for it when your app needs to email a user.
13
+ - **[Analytics](./analytics.md)** counts requests, statuses, bytes, and cache hits per site, with no code changes. Reach for it to see traffic and cache effectiveness.
14
+ - **[Crypto](./crypto.md)** is a small, safe cryptography toolkit (hashing, HMAC, AES, random bytes) available with no import. Reach for it to sign or encrypt your own data. It is the engine under signed cookies.
15
+ - **[Cookies](./cookies.md)** is a complete cookie layer: read incoming cookies, set them on a response, and optionally sign or encrypt their values. Reach for it for preferences, feature flags, and any small piece of state you keep in the browser.
16
+ - **[Time](./time.md)** is the edge's clock: the current wall-clock time, read through a single blessed API. Reach for it whenever you need a timestamp or need to compute an expiry.
17
+
18
+ ## Pick a service by what you need
19
+
20
+ | I want to... | Use | Shape |
21
+ | --- | --- | --- |
22
+ | Serve the same public response fast, without re-running code | [Caching](./caching.md) | `@cache(...)` / `Response.cache(...)` |
23
+ | Stop brute-force logins or spammy writes | [Rate limiting](./ratelimit.md) | `@ratelimit(...)` |
24
+ | Read a config value or a secret set outside my code | [Environment](./environment.md) | `Environment.get` / `getSecure` |
25
+ | Email a user a code or a receipt | [Email](./email.md) | `EmailService` |
26
+ | See traffic and cache-hit numbers per site | [Analytics](./analytics.md) | `Analytics` |
27
+ | Hash, sign, encrypt, or make random bytes | [Crypto](./crypto.md) | `crypto` |
28
+ | Remember a preference or flag in the browser | [Cookies](./cookies.md) | `Cookie` / `Cookies` / `SecureCookies` |
29
+ | Stamp or compare an instant in time | [Time](./time.md) | `Time.nowMillis()` / `Time.nowSeconds()` |
30
+ | Know who is logged in and protect routes | [Auth](../auth/README.md) | `@auth` / `@user` |
31
+
32
+ ## How they fit together
33
+
34
+ Decorators and globals compose. A single route can be rate-limited, auth-guarded, and cached at once, and it can read a secret and stamp the time inside the handler:
35
+
36
+ ```ts
37
+ import { Response, RouteContext } from 'toiljs/server/runtime';
38
+
39
+ @rest('api')
40
+ class Api {
41
+ @ratelimit(RateLimit.SlidingWindow, 100, 60) // at most 100/min per client IP
42
+ @cache(1) // edge-cache for 1 minute
43
+ @get('/status')
44
+ status(ctx: RouteContext): Response {
45
+ const region = Environment.get('REGION'); // config value, or null
46
+ const now = Time.nowSeconds(); // seconds since 1970
47
+ return Response.json(`{"region":${JSON.stringify(region)},"at":${now}}`);
48
+ }
49
+ }
50
+ ```
51
+
52
+ The order of guards is fixed by the framework: rate limiting runs first (so floods are rejected cheaply), then auth, then your handler, and caching is applied to whatever your handler returns. You do not have to think about that order; it is safe by construction.
53
+
54
+ ## Related
55
+
56
+ - [Every decorator, in one place](../concepts/decorators.md)
57
+ - [Configuration: build-time config vs runtime environment](../concepts/config.md)
58
+ - [Compute tiers (where your code runs)](../concepts/tiers.md)
59
+ - [Backend overview](../backend/README.md) and [HTTP routes](../backend/rest.md)
60
+ - [Auth, sessions, and `@user`](../auth/README.md)