toiljs 0.0.60 → 0.0.62

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 (120) hide show
  1. package/.github/workflows/ci.yml +31 -0
  2. package/CHANGELOG.md +17 -0
  3. package/build/cli/.tsbuildinfo +1 -1
  4. package/build/cli/index.js +2 -2
  5. package/build/client/.tsbuildinfo +1 -1
  6. package/build/client/index.d.ts +1 -1
  7. package/build/client/index.js +1 -1
  8. package/build/client/routing/mount.js +11 -26
  9. package/build/client/ssr/markers.d.ts +1 -0
  10. package/build/client/ssr/markers.js +9 -2
  11. package/build/compiler/.tsbuildinfo +1 -1
  12. package/build/compiler/config.d.ts +21 -0
  13. package/build/compiler/config.js +35 -0
  14. package/build/compiler/docs.d.ts +2 -1
  15. package/build/compiler/docs.js +33 -304
  16. package/build/compiler/index.d.ts +13 -0
  17. package/build/compiler/index.js +113 -21
  18. package/build/compiler/template-build.d.ts +23 -3
  19. package/build/compiler/template-build.js +120 -30
  20. package/build/compiler/toil-docs.generated.d.ts +1 -0
  21. package/build/compiler/toil-docs.generated.js +20 -0
  22. package/build/devserver/.tsbuildinfo +1 -1
  23. package/build/devserver/daemon/catalog.d.ts +26 -0
  24. package/build/devserver/daemon/catalog.js +48 -0
  25. package/build/devserver/daemon/cron.d.ts +4 -0
  26. package/build/devserver/daemon/cron.js +50 -0
  27. package/build/devserver/daemon/host.d.ts +37 -0
  28. package/build/devserver/daemon/host.js +94 -0
  29. package/build/devserver/daemon/index.d.ts +34 -0
  30. package/build/devserver/daemon/index.js +241 -0
  31. package/build/devserver/db/catalog.d.ts +2 -1
  32. package/build/devserver/db/catalog.js +44 -44
  33. package/build/devserver/db/database.d.ts +27 -11
  34. package/build/devserver/db/database.js +539 -169
  35. package/build/devserver/db/index.d.ts +1 -1
  36. package/build/devserver/db/index.js +1 -1
  37. package/build/devserver/db/routeKinds.d.ts +8 -0
  38. package/build/devserver/db/routeKinds.js +139 -0
  39. package/build/devserver/db/types.d.ts +64 -1
  40. package/build/devserver/db/types.js +33 -1
  41. package/build/devserver/index.d.ts +10 -0
  42. package/build/devserver/index.js +7 -0
  43. package/build/devserver/mstore/store.d.ts +18 -0
  44. package/build/devserver/mstore/store.js +82 -0
  45. package/build/devserver/runtime/host.d.ts +6 -0
  46. package/build/devserver/runtime/host.js +45 -1
  47. package/build/devserver/runtime/module.d.ts +1 -0
  48. package/build/devserver/runtime/module.js +27 -1
  49. package/build/devserver/server.d.ts +6 -0
  50. package/build/devserver/server.js +59 -0
  51. package/build/devserver/ssr.d.ts +25 -0
  52. package/build/devserver/ssr.js +114 -0
  53. package/build/devserver/wasm/sections.d.ts +2 -0
  54. package/build/devserver/wasm/sections.js +42 -0
  55. package/build/devserver/wasm/surface.d.ts +18 -0
  56. package/build/devserver/wasm/surface.js +41 -0
  57. package/docs/README.md +4 -4
  58. package/docs/auth-todo.md +6 -6
  59. package/docs/caching.md +5 -5
  60. package/docs/cli.md +15 -0
  61. package/docs/client.md +40 -0
  62. package/docs/crypto.md +4 -4
  63. package/docs/data.md +6 -6
  64. package/docs/email.md +28 -28
  65. package/docs/environment.md +10 -10
  66. package/docs/index.md +26 -0
  67. package/docs/ratelimit.md +10 -10
  68. package/docs/routing.md +2 -2
  69. package/docs/server.md +61 -0
  70. package/docs/ssr.md +561 -113
  71. package/docs/styling.md +22 -0
  72. package/docs/time.md +1 -1
  73. package/eslint.config.js +10 -1
  74. package/examples/basic/client/components/Header.tsx +3 -0
  75. package/examples/basic/client/routes/features/actions.tsx +0 -2
  76. package/examples/basic/client/routes/hello.tsx +89 -19
  77. package/examples/basic/client/styles/main.css +48 -0
  78. package/examples/basic/server/SsrHelloRender.ts +97 -0
  79. package/examples/basic/server/main.ts +5 -0
  80. package/examples/basic/server/streams/Echo.ts +49 -0
  81. package/package.json +12 -10
  82. package/scripts/gen-toil-docs.mjs +96 -0
  83. package/src/cli/create.ts +2 -2
  84. package/src/client/index.ts +1 -1
  85. package/src/client/routing/mount.tsx +19 -31
  86. package/src/client/ssr/markers.tsx +33 -4
  87. package/src/compiler/config.ts +88 -2
  88. package/src/compiler/docs.ts +47 -308
  89. package/src/compiler/index.ts +236 -32
  90. package/src/compiler/ssr-codegen.ts +1 -1
  91. package/src/compiler/template-build.ts +271 -53
  92. package/src/compiler/toil-docs.generated.ts +26 -0
  93. package/src/devserver/daemon/catalog.ts +120 -0
  94. package/src/devserver/daemon/cron.ts +87 -0
  95. package/src/devserver/daemon/host.ts +224 -0
  96. package/src/devserver/daemon/index.ts +349 -0
  97. package/src/devserver/db/catalog.ts +61 -53
  98. package/src/devserver/db/database.ts +613 -149
  99. package/src/devserver/db/index.ts +1 -1
  100. package/src/devserver/db/routeKinds.ts +147 -0
  101. package/src/devserver/db/types.ts +65 -2
  102. package/src/devserver/index.ts +12 -0
  103. package/src/devserver/mstore/store.ts +121 -0
  104. package/src/devserver/runtime/host.ts +92 -1
  105. package/src/devserver/runtime/module.ts +35 -1
  106. package/src/devserver/server.ts +101 -0
  107. package/src/devserver/ssr.ts +166 -0
  108. package/src/devserver/wasm/sections.ts +59 -0
  109. package/src/devserver/wasm/surface.ts +88 -0
  110. package/test/daemon-build.test.ts +198 -0
  111. package/test/daemon-catalog.test.ts +265 -0
  112. package/test/daemon-emulation.test.ts +216 -0
  113. package/test/devserver-database.test.ts +396 -5
  114. package/test/email-preview.test.ts +6 -1
  115. package/test/fixtures/daemon-app.ts +56 -0
  116. package/test/global-setup.ts +17 -0
  117. package/test/ssr-hydration.test.tsx +107 -0
  118. package/test/ssr-render.test.ts +96 -27
  119. package/test/ssr-template.test.tsx +47 -2
  120. package/vitest.config.ts +3 -0
package/docs/email.md CHANGED
@@ -5,17 +5,17 @@ toiljs can send transactional email from a route handler. A handler calls
5
5
  `emails/` folder, or the stateless `TwoFactor` helper); the edge hands the
6
6
  message to a single off-core mailer thread that talks to the provider over the
7
7
  kernel network (never the worker cores), and **suspends** the wasm call until the
8
- provider responds so a slow send never blocks the worker.
8
+ provider responds, so a slow send never blocks the worker.
9
9
 
10
10
  Everything here is an ambient **global** (no import), like `crypto` and
11
11
  `AuthService`. A tenant that never sends email pulls none of it into its build.
12
12
 
13
- - **`EmailService`** send one email.
14
- - **`EmailTemplate`** a reusable template with `{{placeholder}}` substitution
13
+ - **`EmailService`**, send one email.
14
+ - **`EmailTemplate`**, a reusable template with `{{placeholder}}` substitution
15
15
  (plain text and/or HTML).
16
- - **`emails/*.tsx`** author emails as React components; the build renders them
16
+ - **`emails/*.tsx`**, author emails as React components; the build renders them
17
17
  to static HTML and generates a typed `Emails.<Name>.send(...)`.
18
- - **`TwoFactor`** stateless email verification codes (2FA / confirm), no DB.
18
+ - **`TwoFactor`**, stateless email verification codes (2FA / confirm), no DB.
19
19
 
20
20
  > **The one rule of HTML email:** email clients run **no JavaScript** and strip
21
21
  > `<style>`/external CSS. So HTML email is a static, inline-styled string, and
@@ -24,7 +24,7 @@ Everything here is an ambient **global** (no import), like `crypto` and
24
24
 
25
25
  ## Configure email
26
26
 
27
- Email is a **framework-reserved namespace of the tenant's environment** the
27
+ Email is a **framework-reserved namespace of the tenant's environment**, the
28
28
  same out-of-band [Environment](./environment.md) store that backs
29
29
  `Environment.get` / `getSecure`, but the `[email]` block is **host-only**: it is
30
30
  read and used in Rust (the off-core mailer) and is **never exposed to the
@@ -35,7 +35,7 @@ On the edge today it lives in the tenant's env secrets file,
35
35
  `$TOIL_ENV_DIR/<host>.env.secrets` (default dir `/run/toil/env`), kept `0600` and
36
36
  **out of `hosts/`** so the config watcher never sees a credential (the dashboard /
37
37
  edge DB replaces this file later). Email config is a set of **reserved
38
- `TOIL_EMAIL_*` keys** host-only, stripped from the guest buckets, so a tenant
38
+ `TOIL_EMAIL_*` keys**, host-only, stripped from the guest buckets, so a tenant
39
39
  can never read them via `Environment.getSecure`:
40
40
 
41
41
  ```bash
@@ -57,15 +57,15 @@ reserved namespace the framework consumes.
57
57
  When `enabled` is `false` (the default) the host has no email capability and
58
58
  `EmailService.send` returns `Disabled`. The env is loaded **lazily** (on the
59
59
  first send) and the `api_key` is materialized into a zeroizing secret in host
60
- memory never written to logs or `/_admin`. A malformed `[email]` block is
60
+ memory, never written to logs or `/_admin`. A malformed `[email]` block is
61
61
  treated as "no email" (the host returns `Disabled`); validate config on the
62
62
  dashboard before deploying.
63
63
 
64
64
  ### Providers
65
65
 
66
- **Resend** (`provider = "resend"`) a JSON API; `api_key` holds the API key.
66
+ **Resend** (`provider = "resend"`), a JSON API; `api_key` holds the API key.
67
67
 
68
- **Gmail** (`TOIL_EMAIL_PROVIDER=gmail`) SMTP with Gmail defaults:
68
+ **Gmail** (`TOIL_EMAIL_PROVIDER=gmail`), SMTP with Gmail defaults:
69
69
  `smtp.gmail.com`, port `587` (STARTTLS), username = `from`. `TOIL_EMAIL_API_KEY`
70
70
  holds a Gmail **App Password** (create one at
71
71
  <https://myaccount.google.com/apppasswords>; the account needs 2-Step
@@ -78,7 +78,7 @@ TOIL_EMAIL_PROVIDER=gmail
78
78
  TOIL_EMAIL_API_KEY=abcd efgh ijkl mnop
79
79
  ```
80
80
 
81
- **Generic SMTP** (`TOIL_EMAIL_PROVIDER=smtp`) any submission server (Outlook,
81
+ **Generic SMTP** (`TOIL_EMAIL_PROVIDER=smtp`), any submission server (Outlook,
82
82
  SendGrid/Mailgun SMTP, your own). Requires `TOIL_EMAIL_SMTP_HOST`; port defaults
83
83
  to `587` (STARTTLS), or set `465` for implicit TLS. `TOIL_EMAIL_SMTP_USER`
84
84
  defaults to `from`.
@@ -95,9 +95,9 @@ TOIL_EMAIL_SMTP_USER=noreply@example.com
95
95
 
96
96
  ### In dev
97
97
 
98
- `toiljs dev` runs the **full email pipeline** in Node recipient validation,
98
+ `toiljs dev` runs the **full email pipeline** in Node, recipient validation,
99
99
  dedup, and the per-minute / per-day / per-recipient caps all behave exactly like
100
- the edge and **really sends** once you configure a provider. Configure it in
100
+ the edge, and **really sends** once you configure a provider. Configure it in
101
101
  `toil.config.ts` (non-secret) with the API key in `.env.secrets`:
102
102
 
103
103
  ```ts
@@ -153,7 +153,7 @@ class Notify {
153
153
 
154
154
  | Status | Meaning | Retry? |
155
155
  | ----------------- | ----------------------------------------------------------- | ---------------- |
156
- | `Sent` | Accepted by the provider | |
156
+ | `Sent` | Accepted by the provider |, |
157
157
  | `Deduped` | An identical recent `(recipient, purpose)` was collapsed | treat as sent |
158
158
  | `Budget` | The host's per-minute budget is exhausted | yes, later |
159
159
  | `TryLater` | The mailer was saturated / a queue was full | yes, back off |
@@ -193,7 +193,7 @@ const status = welcome.send('alice@example.com', vars, 'welcome');
193
193
  - `template.render(vars)` returns the rendered `{ subject, body, html }` without
194
194
  sending (useful for preview/testing).
195
195
 
196
- For anything richer than `{{token}}` substitution real layout, CSS, brand
196
+ For anything richer than `{{token}}` substitution, real layout, CSS, brand,
197
197
  author the email as a React component instead.
198
198
 
199
199
  ## React email templates
@@ -247,7 +247,7 @@ Authoring notes:
247
247
  email name), `export const text` (a plain-text alternative; otherwise derived
248
248
  from the HTML), `export const purpose`.
249
249
  - **Build-time, field substitution only.** Because the component renders once at
250
- build, per-send data is `{{token}}` substitution a runtime `{items.map(...)}`
250
+ build, per-send data is `{{token}}` substitution, a runtime `{items.map(...)}`
251
251
  or conditional bakes in at build, it does not re-run per recipient. That covers
252
252
  transactional / 2FA / confirmation email; dynamic lists need a different
253
253
  approach.
@@ -265,7 +265,7 @@ editor. It refreshes live as you edit the template or its CSS.
265
265
  ## Email verification codes (`TwoFactor`)
266
266
 
267
267
  `TwoFactor` is a **stateless** email-code primitive (2FA, email confirmation,
268
- magic codes) no database. It emails a random code and returns a signed
268
+ magic codes), no database. It emails a random code and returns a signed
269
269
  **token** that commits to the code via HMAC, without putting the code in the
270
270
  token (the code is only in the email). Verification recomputes the HMAC from the
271
271
  token plus the user-entered code, so a valid `(token, code)` pair can only come
@@ -281,18 +281,18 @@ const challenge = TwoFactor.send('alice@example.com', 'login'); // emails the co
281
281
  const ok: bool = TwoFactor.verify(challenge.token, 'alice@example.com', userEntered);
282
282
  ```
283
283
 
284
- - **`send(recipient, purpose, ttlSecs = 600, digits = 6)`** issues a code,
284
+ - **`send(recipient, purpose, ttlSecs = 600, digits = 6)`**, issues a code,
285
285
  emails it with a built-in template, returns `{ token, status }`.
286
- - **`issue(recipient, purpose, ttlSecs, digits)`** returns `{ code, token }`
286
+ - **`issue(recipient, purpose, ttlSecs, digits)`**, returns `{ code, token }`
287
287
  **without** sending, so you can email `code` with your own `EmailTemplate` /
288
288
  `Emails.*` for a branded message.
289
- - **`verify(token, recipient, code)`** `true` only for a code issued for that
289
+ - **`verify(token, recipient, code)`**, `true` only for a code issued for that
290
290
  recipient that hasn't expired. Constant-time compare.
291
- - **`TwoFactor.setSecret(secret)`** the HMAC secret for the tokens. Call once
291
+ - **`TwoFactor.setSecret(secret)`**, the HMAC secret for the tokens. Call once
292
292
  at startup in `main.ts`; it must be identical on every edge instance and out of
293
293
  any client bundle. (This is separate from the provider `api_key`.)
294
294
 
295
- **Limitation:** this gives integrity + expiry but **not single-use** a valid
295
+ **Limitation:** this gives integrity + expiry but **not single-use**, a valid
296
296
  code verifies repeatedly within its TTL, because there is no server state to burn
297
297
  it. Keep the TTL short; for true single-use, store a per-recipient
298
298
  last-verified-at and reject at or before it.
@@ -302,13 +302,13 @@ last-verified-at and reject at or before it.
302
302
  All enforced authoritatively in the single mailer (so the counts are exact across
303
303
  all workers):
304
304
 
305
- - **Per-host budget** two rolling windows, both enforced: a 1-minute cap
305
+ - **Per-host budget**, two rolling windows, both enforced: a 1-minute cap
306
306
  (`max_per_min`) and a 24-hour cap (`max_per_day`, `0` = unlimited). Over either
307
307
  one → `Budget`. Each host's caps, in-window sends, and reject counts are visible
308
308
  per host at `GET /_admin/email`.
309
- - **Per-recipient cap** `max_per_recipient_per_hour`. Over it →
309
+ - **Per-recipient cap**, `max_per_recipient_per_hour`. Over it →
310
310
  `RecipientCapped`.
311
- - **Dedup** identical `(host, recipient, purpose)` within ~30s → `Deduped`.
311
+ - **Dedup**, identical `(host, recipient, purpose)` within ~30s → `Deduped`.
312
312
 
313
313
  Editing these in the host config takes effect on the next send (no restart).
314
314
 
@@ -316,11 +316,11 @@ Editing these in the host config takes effect on the next send (no restart).
316
316
 
317
317
  `GET /_admin/email` returns process-wide counters by reason (JSON), e.g.
318
318
  `submitted`, `sent`, `deduped`, `budget`, `recipient_capped`, `try_later`,
319
- `bad_recipient`, `provider_error`. It exposes **counts only** never a
319
+ `bad_recipient`, `provider_error`. It exposes **counts only**, never a
320
320
  recipient, code, subject, body, or secret.
321
321
 
322
322
  ## See also
323
323
 
324
- - [Rate limiting](./ratelimit.md) protect your routes (including any email
324
+ - [Rate limiting](./ratelimit.md), protect your routes (including any email
325
325
  trigger) with `@ratelimit`.
326
- - [Web Crypto](./crypto.md) the `crypto` global `TwoFactor` builds on.
326
+ - [Web Crypto](./crypto.md), the `crypto` global `TwoFactor` builds on.
@@ -2,7 +2,7 @@
2
2
 
3
3
  `Environment` gives a tenant **per-app environment variables and secrets**, set
4
4
  out of band (a dashboard, like GitHub Actions) so the deployed `.wasm` carries
5
- **no credentials**. It is read-only from app code there is no `set`; values are
5
+ **no credentials**. It is read-only from app code, there is no `set`; values are
6
6
  configured on the deployment side, never from the module.
7
7
 
8
8
  ```ts
@@ -20,15 +20,15 @@ class Cfg {
20
20
  }
21
21
  ```
22
22
 
23
- `Environment` is a global no import needed (like `EmailService` / `AuthService`).
23
+ `Environment` is a global, no import needed (like `EmailService` / `AuthService`).
24
24
 
25
25
  ## Two disjoint buckets
26
26
 
27
27
  Just like GitHub Actions' `vars` vs `secrets`:
28
28
 
29
- - **`Environment.get(key)`** reads **plain vars** non-sensitive config (a public
29
+ - **`Environment.get(key)`** reads **plain vars**, non-sensitive config (a public
30
30
  API base URL, a feature flag, a region). Returns the string, or `null`.
31
- - **`Environment.getSecure(key)`** reads **secrets** sensitive values (a
31
+ - **`Environment.getSecure(key)`** reads **secrets**, sensitive values (a
32
32
  third-party API key). Returns the string, or `null`.
33
33
 
34
34
  The buckets are **disjoint**: a secret is **never** returned by `get()`, and a
@@ -37,13 +37,13 @@ through a code path that logs the result of a `get()`. Keys are case-sensitive,
37
37
  exact-match.
38
38
 
39
39
  > Secrets you read with `getSecure` are plaintext in your module at runtime
40
- > (that's the point you need them to call out). Don't log them, don't put them
40
+ > (that's the point, you need them to call out). Don't log them, don't put them
41
41
  > in a response, and don't copy them into a client bundle.
42
42
 
43
43
  ## What is NOT here
44
44
 
45
- Framework-reserved namespaces (today: **email** provider config) are **host-only**
46
- resolved and used in Rust where the framework needs them, and **never exposed to
45
+ Framework-reserved namespaces (today: **email** provider config) are **host-only**,
46
+ resolved and used in Rust where the framework needs them, and **never exposed to
47
47
  the `.wasm`**. There is no `Environment.email`; you configure email in the
48
48
  `[email]` block of the same env file and the platform uses it for you (see
49
49
  [Email](./email.md)). The env imports only ever see your own `vars` / `secrets`.
@@ -53,7 +53,7 @@ the `.wasm`**. There is no `Environment.email`; you configure email in the
53
53
  Vars and secrets live in **two separate dotenv (`.env`) files**, so the disjoint
54
54
  split is structural and the secrets file can be locked down on its own. On the
55
55
  edge they are per host, **out of `hosts/`** (so the config watcher never sees a
56
- credential) the dashboard / edge database replaces them later:
56
+ credential), the dashboard / edge database replaces them later:
57
57
 
58
58
  ```bash
59
59
  # $TOIL_ENV_DIR/<host>.env (default dir /run/toil/env)
@@ -63,7 +63,7 @@ REGION=eu
63
63
  # $TOIL_ENV_DIR/<host>.env.secrets (mode 0600)
64
64
  STRIPE_KEY=sk_live_xxx # -> Environment.getSecure("STRIPE_KEY")
65
65
 
66
- # host-only email config reserved TOIL_EMAIL_* keys, NEVER exposed to the .wasm
66
+ # host-only email config, reserved TOIL_EMAIL_* keys, NEVER exposed to the .wasm
67
67
  TOIL_EMAIL_ENABLED=true
68
68
  TOIL_EMAIL_PROVIDER=resend
69
69
  TOIL_EMAIL_FROM=noreply@example.com
@@ -72,7 +72,7 @@ TOIL_EMAIL_API_KEY=re_xxx
72
72
 
73
73
  Each file is plain dotenv: `KEY=value` per line, `#` comments, optional `export`,
74
74
  optional quotes. Keys with the reserved **`TOIL_`** prefix are framework/host-only
75
- and are stripped from BOTH guest buckets a tenant can never read them via
75
+ and are stripped from BOTH guest buckets, a tenant can never read them via
76
76
  `get`/`getSecure` (see [Email](./email.md) for `TOIL_EMAIL_*`).
77
77
 
78
78
  On the edge, env is loaded **lazily** (the first time your code reads it) into a
package/docs/index.md ADDED
@@ -0,0 +1,26 @@
1
+ # toiljs
2
+
3
+ A full-stack React framework: a Vite-bundled client SPA with file-based routing, plus a
4
+ toilscript-to-WebAssembly server target.
5
+
6
+ ## Project layout
7
+
8
+ - `client/`, the app: `routes/` (file-based), `layout.tsx`, `components/`, `styles/`,
9
+ `public/`, and `toil.tsx` (the entry that calls `Toil.mount`).
10
+ - `server/`, the toilscript → WASM target (`@main` entry), compiled by `toilscript`.
11
+ `@data`/`@remote`/`@service` here generate the typed client `Server` API (see [server.md](./server.md)).
12
+ - `toil.config.ts`, configuration via `defineConfig` (`toiljs.config.ts` also works).
13
+ - Generated, gitignored, do not edit: `.toil/` (working dir), `toil-env.d.ts` (ambient
14
+ globals), `toil-routes.d.ts` (typed routes), `shared/server.ts` (the typed RPC module,
15
+ emitted by the server build; import `@data` classes from `shared/server`).
16
+
17
+ ## Key ideas
18
+
19
+ - `Toil` is a native global (no import): `Toil.Link`, `Toil.useRouter`, `Toil.useLoaderData`,
20
+ etc. The IO classes (`FastMap`, `FastSet`, `DataWriter`, `DataReader`), `parseError`, and the
21
+ generated `Server` RPC surface are globals too.
22
+ - Scripts: `npm run dev` (HMR), `npm run build` (→ `build/client` + `build/server`),
23
+ `npm start` (self-host the build).
24
+
25
+ See [routing.md](./routing.md), [client.md](./client.md), [styling.md](./styling.md),
26
+ [server.md](./server.md), [ssr.md](./ssr.md), [cli.md](./cli.md).
package/docs/ratelimit.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Rate limiting
2
2
 
3
- The `@ratelimit` decorator throttles **any** `@rest` route a login, a signup, a
3
+ The `@ratelimit` decorator throttles **any** `@rest` route, a login, a signup, a
4
4
  public API, an email trigger, anything. It is enforced at the edge, before your
5
5
  handler runs, and keyed by default on the connecting client's **unspoofable** IP,
6
6
  so it works as an abuse / brute-force control out of the box.
@@ -28,9 +28,9 @@ class Auth {
28
28
 
29
29
  `@ratelimit(strategy, limit, window)`:
30
30
 
31
- - **`strategy`** a `RateLimit` value (ambient global, no import):
31
+ - **`strategy`**, a `RateLimit` value (ambient global, no import):
32
32
  `RateLimit.FixedWindow`, `RateLimit.SlidingWindow`, or `RateLimit.TokenBucket`.
33
- - **`limit`** and **`window`** integer literals whose meaning depends on the
33
+ - **`limit`** and **`window`**, integer literals whose meaning depends on the
34
34
  strategy (see below).
35
35
 
36
36
  When a request is over the limit the edge returns **`429 Too Many Requests`**
@@ -39,7 +39,7 @@ guard runs **before `@auth`**, so unauthenticated floods are limited too.
39
39
 
40
40
  > Both arguments must be **integer literals** and the strategy a `RateLimit`
41
41
  > member (or a bare integer tag). A malformed decorator emits no guard rather
42
- > than miscompiling the same fail-safe rule as `@cache`.
42
+ > than miscompiling, the same fail-safe rule as `@cache`.
43
43
 
44
44
  ## Strategies
45
45
 
@@ -64,17 +64,17 @@ Examples:
64
64
 
65
65
  ## How requests are keyed
66
66
 
67
- By default the limiter keys on the **client IP** specifically the TCP peer
67
+ By default the limiter keys on the **client IP**, specifically the TCP peer
68
68
  address the edge observed (`ctx.clientIp()`), **not** a header like
69
69
  `X-Forwarded-For`, which a client can forge. That makes it a real abuse control:
70
70
  a caller can't reset their bucket by spoofing a header.
71
71
 
72
72
  The count is **exact across all 14 edge workers** (a given IP always maps to one
73
73
  authoritative shard), so the limit is global per route, not per worker. Only
74
- routes that opt in with `@ratelimit` ever pay anything the lock-free fast path
74
+ routes that opt in with `@ratelimit` ever pay anything, the lock-free fast path
75
75
  for everything else is untouched.
76
76
 
77
- > Each rate-limited route has its own independent limiter a limit on `/login`
77
+ > Each rate-limited route has its own independent limiter, a limit on `/login`
78
78
  > does not consume the budget of `/signup`.
79
79
 
80
80
  ## Notes and limits
@@ -82,14 +82,14 @@ for everything else is untouched.
82
82
  - **Route-level only.** Put `@ratelimit` on each route you want limited; there is
83
83
  no controller-wide form yet (unlike `@auth`).
84
84
  - **Keyed on IP.** The decorator keys on the peer IP today. (A per-user / custom
85
- key limiting by account instead of IP exists in the runtime but is not yet
85
+ key, limiting by account instead of IP, exists in the runtime but is not yet
86
86
  exposed through the decorator.)
87
87
  - **In dev.** `toiljs dev` runs a single-process mirror of the same three
88
88
  strategies, so a limited route behaves the same locally as on the edge.
89
89
 
90
90
  ## See also
91
91
 
92
- - [Email](./email.md) `@ratelimit` pairs well with email triggers (verification
92
+ - [Email](./email.md), `@ratelimit` pairs well with email triggers (verification
93
93
  codes, password resets) to blunt abuse.
94
- - [Auth, sessions, and `@user`](./auth.md) `@ratelimit` runs before the `@auth`
94
+ - [Auth, sessions, and `@user`](./auth.md), `@ratelimit` runs before the `@auth`
95
95
  guard, so it protects the login itself.
package/docs/routing.md CHANGED
@@ -134,11 +134,11 @@ serialized.
134
134
 
135
135
  Each route is either **JSON** (default) or **Binary**:
136
136
 
137
- - **JSON** the body is `JSON.parse`d and revived via the `@data` type's
137
+ - **JSON**, the body is `JSON.parse`d and revived via the `@data` type's
138
138
  `fromJSON`; the response is the type's `toJSON()`. 64-bit-and-larger integers
139
139
  cross the wire as decimal strings (exact at any size). Best for endpoints a
140
140
  browser or third party calls directly.
141
- - **Binary** the body is `Body.decode(bytes)` and the response is
141
+ - **Binary**, the body is `Body.decode(bytes)` and the response is
142
142
  `value.encode()`, using the deterministic `DataWriter`/`DataReader` codec. No
143
143
  precision loss, smaller, faster. Best for app-to-app and anything
144
144
  security-sensitive.
package/docs/server.md ADDED
@@ -0,0 +1,61 @@
1
+ # Server (toilscript → WebAssembly)
2
+
3
+ `server/` is the toilscript source, compiled to WebAssembly by `toilscript`.
4
+
5
+ - `server/main.ts`, the `@main` entry, exported as the WASM `main`.
6
+ - `server/index.ts`, your functions.
7
+ - `server/tsconfig.json`, extends `toilscript/std/assembly.json` (AssemblyScript/toilscript
8
+ globals like `i32`, not the DOM), so editors resolve server types correctly.
9
+ - `npm run build:server` (or `npm run build`) emits `build/server/release.wasm` and
10
+ regenerates `shared/server.ts` (the typed client RPC module).
11
+
12
+ ## Typed RPC (`@data` / `@remote` / `@service`)
13
+
14
+ Tag server code and the build generates a typed client `Server` surface:
15
+
16
+ - `@data class X {}`, a serializable struct. Generates a client class with the same fields
17
+ plus `encode`/`decode`; construct it on the client: `import { X } from "shared/server"`.
18
+ - `@remote function f(a: T): R`, a client-callable endpoint, becomes `Server.f(a)`.
19
+ - `@service class S { @remote m(...) {} }`, namespaces methods: `Server.s.m(...)`.
20
+
21
+ On the client, `Server` is a global (no import) and fully typed; every call is async
22
+ (`Promise<R>`). Inputs/outputs are scalars, arrays, or `@data` classes, both directions.
23
+
24
+ Note: the client↔server transport is not wired yet, so calling a `Server` method throws
25
+ until it lands; the typed surface + codec are generated and ready.
26
+
27
+ ## HTTP REST (`@rest` / `@route`)
28
+
29
+ Tag a class `@rest` and its methods with a verb to expose a real HTTP API. Unlike RPC,
30
+ the generated client is working `fetch` code (it is just HTTP).
31
+
32
+ - `@rest("api") class Todos {}`, mounts the controller at `/api` (bare `@rest` → `/`).
33
+ - `@get("/todos/:id")` / `@post` / `@del` / `@put` / `@patch` / `@head` / `@options`, verb
34
+ shortcuts; or `@route({ method: Methods.GET, path: "/todos", stream: DataStream.JSON })`.
35
+ - A method takes an optional `@data` body + an optional `ctx: RouteContext` (path params via
36
+ `ctx.param("id")`, `ctx.query(...)`, `ctx.header(...)`). It returns either a `@data` type,
37
+ which the compiler encodes per `stream` (`DataStream.JSON` default, or `DataStream.Binary`,
38
+ lossless for large `u64`/bignum), or a `Response` for full control - custom status and
39
+ headers, e.g. `Response.json(value.toJSON().toString()).setHeader("cache-control", "no-store")`
40
+ or `Response.notFound()`. (The editor sees the compiler-injected `@data` `toJSON`/`encode`
41
+ members via the toilscript plugin, so serializing into a `Response` is editor-clean.)
42
+
43
+ Each `@rest` class self-registers; dispatch them from your handler - it composes, it never
44
+ takes over `handle()`:
45
+
46
+ ```ts
47
+ import { ToilHandler, Request, Response, Rest } from "toiljs/server/runtime";
48
+ export class App extends ToilHandler {
49
+ public handle(req: Request): Response {
50
+ const hit = Rest.dispatch(req); // try every @rest controller
51
+ if (hit != null) return hit;
52
+ return Response.notFound(); // your own logic / static fallback
53
+ }
54
+ }
55
+ ```
56
+
57
+ For a REST-only project, `Server.handler = () => new RestHandler()` does the same with no
58
+ boilerplate. On the client: `Server.REST.todos.getTodo({ params: { id } })` (see [client.md](./client.md)).
59
+
60
+ For the full reference (`@rest`/verb decorators, `RouteContext`, `Request`, `Response`,
61
+ dispatch + the 404 fallback) see [routing.md](./routing.md).