toiljs 0.0.115 → 0.0.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +16 -7
- package/TYPESCRIPT_LAW.md +1946 -2073
- package/build/backend/.tsbuildinfo +1 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2375 -258
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.js +1 -1
- package/build/client/dev/error-overlay.js +1 -1
- package/build/client/routing/mount.js +1 -1
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/auth-emails.js +7 -3
- package/build/compiler/emails.js +1 -1
- package/build/compiler/index.d.ts +1 -0
- package/build/compiler/index.js +25 -4
- package/build/compiler/pages.js +2 -3
- package/build/compiler/prerender.d.ts +3 -8
- package/build/compiler/prerender.js +88 -127
- package/build/compiler/ssg.js +3 -4
- package/build/compiler/template-build.js +3 -4
- package/build/compiler/toil-docs.generated.js +6 -5
- package/build/compiler/vite.js +3 -6
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/db/database.js +7 -3
- package/build/devserver/db/types.js +1 -1
- package/build/devserver/production.js +4 -3
- package/build/devserver/runtime/crypto.d.ts +1 -1
- package/build/io/.tsbuildinfo +1 -1
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/examples/basic/client/routes/analytics.tsx +7 -5
- package/examples/basic/server/tsconfig.json +1 -3
- package/package.json +45 -44
- package/presets/no-uint8array-tostring.js +120 -153
- package/presets/oxlint.d.ts +5 -0
- package/presets/oxlint.js +224 -0
- package/server/runtime/tsconfig.json +1 -3
- package/tsconfig.json +2 -11
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -90
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -43
- package/.github/changelog-config.json +0 -45
- package/.github/dependabot.yml +0 -27
- package/.github/workflows/ci.yml +0 -222
- package/.vscode/settings.json +0 -10
- package/docs/README.md +0 -107
- package/docs/auth/README.md +0 -104
- package/docs/auth/configuration.md +0 -105
- package/docs/auth/emails.md +0 -92
- package/docs/auth/extending.md +0 -202
- package/docs/auth/how-it-works.md +0 -138
- package/docs/auth/usage.md +0 -188
- package/docs/backend/README.md +0 -143
- package/docs/backend/data.md +0 -351
- package/docs/backend/rest.md +0 -402
- package/docs/backend/rpc.md +0 -226
- package/docs/background/README.md +0 -114
- package/docs/background/daemons.md +0 -276
- package/docs/background/derive.md +0 -179
- package/docs/cli/README.md +0 -379
- package/docs/concepts/ai-guide.md +0 -132
- package/docs/concepts/config.md +0 -416
- package/docs/concepts/decorators.md +0 -127
- package/docs/concepts/security.md +0 -108
- package/docs/concepts/tiers.md +0 -166
- package/docs/concepts/types.md +0 -216
- package/docs/database/README.md +0 -143
- package/docs/database/capacity.md +0 -350
- package/docs/database/counters.md +0 -174
- package/docs/database/documents.md +0 -277
- package/docs/database/events.md +0 -307
- package/docs/database/membership.md +0 -246
- package/docs/database/setup.md +0 -155
- package/docs/database/unique.md +0 -216
- package/docs/database/views.md +0 -246
- package/docs/frontend/README.md +0 -104
- package/docs/frontend/components.md +0 -286
- package/docs/frontend/data-fetching.md +0 -260
- package/docs/frontend/images.md +0 -148
- package/docs/frontend/metadata.md +0 -344
- package/docs/frontend/navigation.md +0 -296
- package/docs/frontend/rendering.md +0 -236
- package/docs/frontend/routing.md +0 -346
- package/docs/frontend/scripts.md +0 -118
- package/docs/frontend/search.md +0 -191
- package/docs/frontend/styling.md +0 -147
- package/docs/frontend/toil-global.md +0 -216
- package/docs/getting-started/README.md +0 -81
- package/docs/getting-started/create-project.md +0 -131
- package/docs/getting-started/deploy.md +0 -101
- package/docs/getting-started/first-app.md +0 -215
- package/docs/getting-started/installation.md +0 -124
- package/docs/getting-started/migrating.md +0 -125
- package/docs/getting-started/project-structure.md +0 -163
- package/docs/introduction/README.md +0 -66
- package/docs/introduction/design-principles.md +0 -81
- package/docs/introduction/distributed.md +0 -91
- package/docs/introduction/how-it-works.md +0 -121
- package/docs/introduction/hyperscale.md +0 -70
- package/docs/introduction/modern-stack.md +0 -85
- package/docs/introduction/vs-other-frameworks.md +0 -64
- package/docs/introduction/why-toil.md +0 -99
- package/docs/llms.txt +0 -95
- package/docs/realtime/README.md +0 -102
- package/docs/realtime/channels.md +0 -211
- package/docs/realtime/streams.md +0 -369
- package/docs/services/README.md +0 -60
- package/docs/services/analytics.md +0 -279
- package/docs/services/caching.md +0 -175
- package/docs/services/cookies.md +0 -235
- package/docs/services/crypto.md +0 -209
- package/docs/services/email.md +0 -289
- package/docs/services/environment.md +0 -141
- package/docs/services/ratelimit.md +0 -174
- package/docs/services/time.md +0 -85
- package/eslint.config.js +0 -57
- package/examples/basic/client/routes/docs/[...slug].tsx +0 -12
- package/presets/eslint.js +0 -88
- package/src/backend/index.ts +0 -204
- package/src/cli/configure.ts +0 -363
- package/src/cli/create.ts +0 -924
- package/src/cli/db.ts +0 -158
- package/src/cli/diagnostics.ts +0 -665
- package/src/cli/doctor.ts +0 -982
- package/src/cli/features.ts +0 -170
- package/src/cli/index.ts +0 -326
- package/src/cli/notify.ts +0 -120
- package/src/cli/proc.ts +0 -56
- package/src/cli/ui.ts +0 -169
- package/src/cli/update.ts +0 -248
- package/src/cli/updates.ts +0 -92
- package/src/cli/validate.ts +0 -31
- package/src/cli/version-check.ts +0 -125
- package/src/client/auth.ts +0 -869
- package/src/client/channel/channel.ts +0 -146
- package/src/client/components/Form.tsx +0 -65
- package/src/client/components/Image.tsx +0 -160
- package/src/client/components/Script.tsx +0 -113
- package/src/client/components/Slot.tsx +0 -21
- package/src/client/dev/devtools.tsx +0 -1089
- package/src/client/dev/error-overlay.tsx +0 -225
- package/src/client/errors.ts +0 -11
- package/src/client/head/head.ts +0 -182
- package/src/client/head/metadata.ts +0 -110
- package/src/client/index.ts +0 -130
- package/src/client/navigation/Link.tsx +0 -108
- package/src/client/navigation/NavLink.tsx +0 -86
- package/src/client/navigation/navigation.ts +0 -235
- package/src/client/navigation/prefetch.ts +0 -169
- package/src/client/navigation/scroll.ts +0 -53
- package/src/client/routing/Router.tsx +0 -216
- package/src/client/routing/action.ts +0 -122
- package/src/client/routing/error-boundary.tsx +0 -43
- package/src/client/routing/hooks.ts +0 -140
- package/src/client/routing/lazy.ts +0 -77
- package/src/client/routing/loader.ts +0 -381
- package/src/client/routing/match.ts +0 -47
- package/src/client/routing/mount.tsx +0 -90
- package/src/client/routing/params-context.ts +0 -10
- package/src/client/routing/slot-context.ts +0 -7
- package/src/client/rpc.ts +0 -97
- package/src/client/search/search.ts +0 -189
- package/src/client/search/use-page-search.ts +0 -73
- package/src/client/ssr/markers.tsx +0 -172
- package/src/client/stream/client.ts +0 -237
- package/src/client/types.ts +0 -73
- package/src/compiler/auth-emails.ts +0 -312
- package/src/compiler/config.ts +0 -369
- package/src/compiler/docs.ts +0 -127
- package/src/compiler/email-preview.ts +0 -314
- package/src/compiler/emails.ts +0 -404
- package/src/compiler/fonts.ts +0 -89
- package/src/compiler/generate.ts +0 -471
- package/src/compiler/image-blur.ts +0 -42
- package/src/compiler/image-report.ts +0 -88
- package/src/compiler/index.ts +0 -1362
- package/src/compiler/pages.ts +0 -49
- package/src/compiler/plugin.ts +0 -391
- package/src/compiler/prerender.ts +0 -263
- package/src/compiler/routes.ts +0 -170
- package/src/compiler/seo.ts +0 -415
- package/src/compiler/sri.ts +0 -144
- package/src/compiler/ssg.ts +0 -177
- package/src/compiler/ssr-codegen.ts +0 -85
- package/src/compiler/template-build.ts +0 -676
- package/src/compiler/template.ts +0 -262
- package/src/compiler/toil-docs.generated.ts +0 -77
- package/src/compiler/vite.ts +0 -245
- package/src/devserver/analytics/index.ts +0 -317
- package/src/devserver/config/dotenv.ts +0 -102
- package/src/devserver/config/env.ts +0 -30
- package/src/devserver/config/ratelimit.ts +0 -123
- package/src/devserver/daemon/catalog.ts +0 -120
- package/src/devserver/daemon/cron.ts +0 -87
- package/src/devserver/daemon/host.ts +0 -134
- package/src/devserver/daemon/index.ts +0 -369
- package/src/devserver/daemon/runtime.ts +0 -48
- package/src/devserver/db/catalog.ts +0 -109
- package/src/devserver/db/database.ts +0 -1834
- package/src/devserver/db/derives.ts +0 -121
- package/src/devserver/db/index.ts +0 -20
- package/src/devserver/db/routeKinds.ts +0 -191
- package/src/devserver/db/types.ts +0 -168
- package/src/devserver/email/caps.ts +0 -0
- package/src/devserver/email/config.ts +0 -141
- package/src/devserver/email/console.ts +0 -348
- package/src/devserver/email/index.ts +0 -155
- package/src/devserver/email/providers.ts +0 -131
- package/src/devserver/email/status.ts +0 -23
- package/src/devserver/email/validate.ts +0 -37
- package/src/devserver/email/wire.ts +0 -55
- package/src/devserver/http/cache.ts +0 -0
- package/src/devserver/http/envelope.ts +0 -157
- package/src/devserver/http/proxy.ts +0 -172
- package/src/devserver/http/runtime.ts +0 -387
- package/src/devserver/index.ts +0 -43
- package/src/devserver/production-ipc.ts +0 -63
- package/src/devserver/production-worker.ts +0 -83
- package/src/devserver/production.ts +0 -718
- package/src/devserver/runtime/crypto.ts +0 -587
- package/src/devserver/runtime/host.ts +0 -427
- package/src/devserver/runtime/module.ts +0 -442
- package/src/devserver/server.ts +0 -251
- package/src/devserver/ssr.ts +0 -208
- package/src/devserver/stream/catalog.ts +0 -106
- package/src/devserver/stream/host.ts +0 -42
- package/src/devserver/stream/index.ts +0 -301
- package/src/devserver/stream/manager.ts +0 -167
- package/src/devserver/stream/router.ts +0 -101
- package/src/devserver/stream/wire.ts +0 -58
- package/src/devserver/stream/ws.ts +0 -78
- package/src/devserver/wasm/sections.ts +0 -59
- package/src/devserver/wasm/surface.ts +0 -115
- package/src/io/FastMap.ts +0 -151
- package/src/io/FastSet.ts +0 -113
- package/src/io/codec.ts +0 -362
- package/src/io/index.ts +0 -10
- package/src/io/lengths.ts +0 -14
- package/src/io/types.ts +0 -19
- package/src/logger/index.ts +0 -22
- package/src/shared/index.ts +0 -46
- package/test/analytics-dev.test.ts +0 -113
- package/test/assembly/aspect-shim.ts +0 -33
- package/test/assembly/cookie.spec.ts +0 -303
- package/test/assembly/example.spec.ts +0 -22
- package/test/assembly/ssr.spec.ts +0 -95
- package/test/assembly.test.ts +0 -59
- package/test/auth-emails.test.ts +0 -118
- package/test/built-ssr.test.ts +0 -148
- package/test/channel.test.ts +0 -21
- package/test/config-host-port.test.ts +0 -59
- package/test/configure.test.ts +0 -94
- package/test/daemon-build.test.ts +0 -278
- package/test/daemon-catalog.test.ts +0 -349
- package/test/daemon-emulation.test.ts +0 -256
- package/test/db.test.ts +0 -0
- package/test/devserver-database.test.ts +0 -1102
- package/test/devserver-email.test.ts +0 -306
- package/test/devserver-pqauth.test.ts +0 -93
- package/test/devserver-secrets.test.ts +0 -63
- package/test/devserver.test.ts +0 -259
- package/test/doctor.test.ts +0 -298
- package/test/dom/Image.test.tsx +0 -144
- package/test/dom/Link.test.tsx +0 -67
- package/test/dom/NavLink.test.tsx +0 -37
- package/test/dom/Script.test.tsx +0 -48
- package/test/dom/action.test.tsx +0 -146
- package/test/dom/error-overlay.test.tsx +0 -44
- package/test/dom/loader.test.tsx +0 -121
- package/test/dom/navigation.test.ts +0 -59
- package/test/dom/revalidate.test.tsx +0 -38
- package/test/dom/route-head.test.tsx +0 -56
- package/test/dom/router-loading.test.tsx +0 -44
- package/test/dom/scroll.test.ts +0 -56
- package/test/dom/slot.test.tsx +0 -131
- package/test/dom/use-metadata.test.tsx +0 -58
- package/test/dom/view-transitions.test.tsx +0 -53
- package/test/dynamic-routing.test.ts +0 -132
- package/test/email-console.test.ts +0 -86
- package/test/email-preview.test.ts +0 -73
- package/test/emails.test.ts +0 -58
- package/test/errors.test.ts +0 -21
- package/test/example-guestbook.test.ts +0 -123
- package/test/features.test.ts +0 -149
- package/test/fixtures/bignum-wire/spec.ts +0 -25
- package/test/fixtures/daemon-app.ts +0 -65
- package/test/fixtures/stream-echo.ts +0 -26
- package/test/fixtures/stream-gate.ts +0 -28
- package/test/fixtures/stream-trap.ts +0 -18
- package/test/fixtures/stream-typed.ts +0 -41
- package/test/fonts.test.ts +0 -28
- package/test/global-setup.ts +0 -17
- package/test/head.test.ts +0 -42
- package/test/image-blur.test.ts +0 -50
- package/test/io.test.ts +0 -117
- package/test/metadata.test.ts +0 -40
- package/test/navlink.test.ts +0 -28
- package/test/pages.test.ts +0 -105
- package/test/placeholder.test.ts +0 -9
- package/test/pqauth-e2e.test.ts +0 -216
- package/test/pqauth-email.test.ts +0 -801
- package/test/prerender.test.ts +0 -116
- package/test/prettier-plugin.test.ts +0 -46
- package/test/routes.test.ts +0 -76
- package/test/rpc-bignum-wire.test.ts +0 -164
- package/test/rpc-dispatch.test.ts +0 -132
- package/test/rpc-kinds.test.ts +0 -18
- package/test/rpc.test.ts +0 -66
- package/test/search.test.ts +0 -114
- package/test/seo.test.ts +0 -175
- package/test/slot-layouts.test.ts +0 -69
- package/test/sri.test.ts +0 -129
- package/test/ssg.test.ts +0 -36
- package/test/ssr-hydration.test.tsx +0 -122
- package/test/ssr-render.test.ts +0 -197
- package/test/ssr-template.test.tsx +0 -482
- package/test/stream-emulation.test.ts +0 -479
- package/test/ui.test.ts +0 -40
- package/test/update.test.ts +0 -58
- package/test/validate.test.ts +0 -42
- package/test/version-check.test.ts +0 -130
- package/tests/data-parity/generated-parity.ts +0 -99
- package/tests/data-parity/parity.ts +0 -80
- package/tests/data-parity/spec.ts +0 -46
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dev-only source for `Environment.get` / `getSecure`, mirroring the edge's
|
|
3
|
-
* per-tenant env store. Reads two optional dotenv files at the project root via
|
|
4
|
-
* the shared loader (`./dotenv.ts`):
|
|
5
|
-
*
|
|
6
|
-
* - `.env` -> plain vars (`Environment.get`); `process.env` overlays
|
|
7
|
-
* - `.env.secrets` -> secrets (`Environment.getSecure`); keep gitignored
|
|
8
|
-
*
|
|
9
|
-
* DISJOINT like the edge: `get` sees only vars, `getSecure` only secrets, so a
|
|
10
|
-
* secret never comes back through `get`. Framework-reserved `TOIL_*` keys are
|
|
11
|
-
* host-only (the email backend config reads them) and never a tenant var/secret.
|
|
12
|
-
*
|
|
13
|
-
* Cached after first read; restart `toiljs dev` to pick up edits. The edge
|
|
14
|
-
* resolves this PER TENANT from `$TOIL_ENV_DIR/<host>.env` + `<host>.env.secrets`
|
|
15
|
-
* (and the edge DB later) through a lazy, bounded cache; dev has a single
|
|
16
|
-
* project, so two files are enough and no eviction is needed.
|
|
17
|
-
*/
|
|
18
|
-
import { loadEnvFiles } from './dotenv.js';
|
|
19
|
-
|
|
20
|
-
/** A plain var by exact key, or `null`. Reads ONLY the `.env` (vars) bucket. */
|
|
21
|
-
export function devEnvGet(key: string): string | null {
|
|
22
|
-
const v = loadEnvFiles(process.cwd()).vars.get(key);
|
|
23
|
-
return v === undefined ? null : v;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/** A secret by exact key, or `null`. Reads ONLY the `.env.secrets` bucket. */
|
|
27
|
-
export function devEnvGetSecure(key: string): string | null {
|
|
28
|
-
const v = loadEnvFiles(process.cwd()).secrets.get(key);
|
|
29
|
-
return v === undefined ? null : v;
|
|
30
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Dev-server rate limiter: a single-process mirror of the edge's
|
|
3
|
-
* `toil-backend/src/ratelimit.rs` strategies, so a tenant using the
|
|
4
|
-
* `@ratelimit(...)` decorator behaves the same under `toiljs dev` as on the
|
|
5
|
-
* edge. The edge runs an EXACT limiter shared across 14 workers; dev is one
|
|
6
|
-
* process, so a plain module-level registry is already "global".
|
|
7
|
-
*
|
|
8
|
-
* State lives here (module scope), NOT in the per-request DispatchState, because
|
|
9
|
-
* a limiter must persist across requests (the dev server builds a fresh wasm
|
|
10
|
-
* instance per request, exactly like the edge pools).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
/** Mirror of the host's strategy tags (`Strategy` in `ratelimit.rs`). */
|
|
14
|
-
export const STRATEGY_FIXED = 0;
|
|
15
|
-
export const STRATEGY_SLIDING = 1;
|
|
16
|
-
export const STRATEGY_TOKEN_BUCKET = 2;
|
|
17
|
-
|
|
18
|
-
interface KeyState {
|
|
19
|
-
/** Token bucket: tokens * 1000 (sub-token refill without floats). */
|
|
20
|
-
tokensMilli: number;
|
|
21
|
-
/** Window strategies: aligned index of the current bucket. */
|
|
22
|
-
window: number;
|
|
23
|
-
cur: number;
|
|
24
|
-
prev: number;
|
|
25
|
-
lastMs: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface RouteLimiter {
|
|
29
|
-
strategy: number;
|
|
30
|
-
/** `limit`/`window_secs` for windows; `burst`/`refill_per_sec` for the bucket. */
|
|
31
|
-
a: number;
|
|
32
|
-
b: number;
|
|
33
|
-
keys: Map<string, KeyState>;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** `(routeId) -> limiter`, created lazily with the route's first-seen params. */
|
|
37
|
-
const registry = new Map<number, RouteLimiter>();
|
|
38
|
-
|
|
39
|
-
/** Drop all limiter state (tests). Each dev wasm dispatch shares this module-level
|
|
40
|
-
* registry, so a test that fires many `@ratelimit`-decorated routes in one file
|
|
41
|
-
* would otherwise carry counts across cases; reset between cases for isolation. */
|
|
42
|
-
export function __resetRatelimitForTests(): void {
|
|
43
|
-
registry.clear();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface DevDecision {
|
|
47
|
-
allowed: boolean;
|
|
48
|
-
/** Whole seconds to wait before retrying (>= 1 when denied, 0 when allowed). */
|
|
49
|
-
retryAfterSecs: number;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Account one event for `identity` against route `routeId` and return the
|
|
54
|
-
* verdict. Mirrors `SharedLimiter::check` semantics. `now` is wall-clock ms
|
|
55
|
-
* (`Date.now()`), which is fine for a single dev process.
|
|
56
|
-
*/
|
|
57
|
-
export function ratelimitCheck(
|
|
58
|
-
routeId: number,
|
|
59
|
-
strategy: number,
|
|
60
|
-
limit: number,
|
|
61
|
-
window: number,
|
|
62
|
-
identity: string,
|
|
63
|
-
now: number,
|
|
64
|
-
): DevDecision {
|
|
65
|
-
let rl = registry.get(routeId);
|
|
66
|
-
if (rl === undefined) {
|
|
67
|
-
rl = { strategy, a: Math.max(1, limit), b: Math.max(1, window), keys: new Map() };
|
|
68
|
-
registry.set(routeId, rl);
|
|
69
|
-
}
|
|
70
|
-
if (rl.strategy === STRATEGY_TOKEN_BUCKET) return checkBucket(rl, identity, now);
|
|
71
|
-
return checkWindow(rl, identity, now, rl.strategy === STRATEGY_SLIDING);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function checkBucket(rl: RouteLimiter, key: string, now: number): DevDecision {
|
|
75
|
-
const capMilli = rl.a * 1000;
|
|
76
|
-
const refillPerSec = rl.b;
|
|
77
|
-
let st = rl.keys.get(key);
|
|
78
|
-
if (st === undefined) {
|
|
79
|
-
st = { tokensMilli: capMilli, window: 0, cur: 0, prev: 0, lastMs: now };
|
|
80
|
-
rl.keys.set(key, st);
|
|
81
|
-
}
|
|
82
|
-
const elapsed = Math.max(0, now - st.lastMs);
|
|
83
|
-
st.tokensMilli = Math.min(capMilli, st.tokensMilli + elapsed * refillPerSec);
|
|
84
|
-
st.lastMs = now;
|
|
85
|
-
if (st.tokensMilli >= 1000) {
|
|
86
|
-
st.tokensMilli -= 1000;
|
|
87
|
-
return { allowed: true, retryAfterSecs: 0 };
|
|
88
|
-
}
|
|
89
|
-
const needed = 1000 - st.tokensMilli;
|
|
90
|
-
const waitMs = Math.ceil(needed / refillPerSec);
|
|
91
|
-
return { allowed: false, retryAfterSecs: Math.max(1, Math.ceil(waitMs / 1000)) };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function checkWindow(rl: RouteLimiter, key: string, now: number, sliding: boolean): DevDecision {
|
|
95
|
-
const limit = rl.a;
|
|
96
|
-
const windowMs = rl.b * 1000;
|
|
97
|
-
const curWindow = Math.floor(now / windowMs);
|
|
98
|
-
let st = rl.keys.get(key);
|
|
99
|
-
if (st === undefined) {
|
|
100
|
-
st = { tokensMilli: 0, window: curWindow, cur: 0, prev: 0, lastMs: now };
|
|
101
|
-
rl.keys.set(key, st);
|
|
102
|
-
}
|
|
103
|
-
if (curWindow === st.window + 1) {
|
|
104
|
-
st.prev = st.cur;
|
|
105
|
-
st.cur = 0;
|
|
106
|
-
st.window = curWindow;
|
|
107
|
-
} else if (curWindow > st.window) {
|
|
108
|
-
st.prev = 0;
|
|
109
|
-
st.cur = 0;
|
|
110
|
-
st.window = curWindow;
|
|
111
|
-
}
|
|
112
|
-
st.lastMs = now;
|
|
113
|
-
const posInWindow = now % windowMs;
|
|
114
|
-
const effective = sliding
|
|
115
|
-
? Math.floor((st.prev * (windowMs - posInWindow)) / windowMs) + st.cur
|
|
116
|
-
: st.cur;
|
|
117
|
-
if (effective < limit) {
|
|
118
|
-
st.cur += 1;
|
|
119
|
-
return { allowed: true, retryAfterSecs: 0 };
|
|
120
|
-
}
|
|
121
|
-
const waitMs = windowMs - posInWindow;
|
|
122
|
-
return { allowed: false, retryAfterSecs: Math.max(1, Math.ceil(waitMs / 1000)) };
|
|
123
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse a compiled COLD server wasm's `toildaemon.catalog` custom section into the
|
|
3
|
-
* dev daemon scheduler's task list. Emitted by the toilscript cold pass
|
|
4
|
-
* (`buildToilDaemonCatalog`) into `release-cold.wasm` when a `@daemon` class
|
|
5
|
-
* exists. Cron schedules are PRECOMPUTED BITMASKS (RECONCILIATION F6): the reader
|
|
6
|
-
* does bit tests, NEVER a runtime cron-string parse.
|
|
7
|
-
*
|
|
8
|
-
* Byte layout (RECONCILIATION Part 5, all little-endian; mirrors the toilscript
|
|
9
|
-
* `CatWriter` emitter byte-for-byte):
|
|
10
|
-
*
|
|
11
|
-
* u16 format_version = 1
|
|
12
|
-
* u8 has_daemon
|
|
13
|
-
* u16 n_scheduled
|
|
14
|
-
* per task:
|
|
15
|
-
* str name (u32 len + UTF-8)
|
|
16
|
-
* u16 task_index (the scheduled_tick(task_id) argument)
|
|
17
|
-
* u8 schedule_kind (0 = interval, 1 = cron)
|
|
18
|
-
* u64 interval_ms (used when schedule_kind = 0, else 0)
|
|
19
|
-
* u64 cron_minute_mask (bits 0..59)
|
|
20
|
-
* u32 cron_hour_mask (bits 0..23)
|
|
21
|
-
* u32 cron_dom_mask (bits 1..31)
|
|
22
|
-
* u16 cron_month_mask (bits 1..12)
|
|
23
|
-
* u8 cron_dow_mask (bits 0..6)
|
|
24
|
-
* u8 overlap_policy (0 = skip-if-running)
|
|
25
|
-
* u8 catchup_policy (0 = no-backfill)
|
|
26
|
-
* u64 gas_hint
|
|
27
|
-
*
|
|
28
|
-
* Fails closed via `DataReader.ok`: a short read mid-record stops the loop and
|
|
29
|
-
* yields only the cleanly-decoded prefix, never over-reading. An absent or wholly
|
|
30
|
-
* unparseable section returns `null`, so the daemon emulator simply does not start
|
|
31
|
-
* (fail-closed = never run an unknown daemon).
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
import { DataReader } from 'toiljs/io';
|
|
35
|
-
|
|
36
|
-
import { customSection } from '../wasm/sections.js';
|
|
37
|
-
|
|
38
|
-
/** The five precomputed cron field bitmasks (RECONCILIATION Part 5 / F6). */
|
|
39
|
-
export interface CronMasks {
|
|
40
|
-
/** u64, bits 0..59 (one per minute). */
|
|
41
|
-
readonly minute: bigint;
|
|
42
|
-
/** u32, bits 0..23 (one per hour). */
|
|
43
|
-
readonly hour: number;
|
|
44
|
-
/** u32, bits 1..31 (one per day-of-month). */
|
|
45
|
-
readonly dom: number;
|
|
46
|
-
/** u16, bits 1..12 (one per month). */
|
|
47
|
-
readonly month: number;
|
|
48
|
-
/** u8, bits 0..6 (one per day-of-week, 0 = Sunday). */
|
|
49
|
-
readonly dow: number;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** One `@scheduled` task the dev scheduler drives. */
|
|
53
|
-
export interface ScheduledTask {
|
|
54
|
-
readonly name: string;
|
|
55
|
-
/** The `scheduled_tick(task_id)` argument; equals declaration order. */
|
|
56
|
-
readonly taskIndex: number;
|
|
57
|
-
readonly schedule:
|
|
58
|
-
| { readonly kind: 'interval'; readonly ms: number }
|
|
59
|
-
| { readonly kind: 'cron'; readonly masks: CronMasks };
|
|
60
|
-
/** 0 = skip-if-running (the at-most-once default). */
|
|
61
|
-
readonly overlapPolicy: number;
|
|
62
|
-
/** 0 = no-backfill (the at-most-once default). */
|
|
63
|
-
readonly catchupPolicy: number;
|
|
64
|
-
readonly gasHint: bigint;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export interface DaemonCatalog {
|
|
68
|
-
readonly hasDaemon: boolean;
|
|
69
|
-
readonly tasks: readonly ScheduledTask[];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function parseDaemonCatalog(wasm: Buffer): DaemonCatalog | null {
|
|
73
|
-
let sec: Buffer | null;
|
|
74
|
-
try {
|
|
75
|
-
sec = customSection(wasm, 'toildaemon.catalog');
|
|
76
|
-
} catch {
|
|
77
|
-
return null; // garbage section table (mid-rebuild) -> no daemon
|
|
78
|
-
}
|
|
79
|
-
if (sec === null) return null;
|
|
80
|
-
|
|
81
|
-
const r = new DataReader(sec);
|
|
82
|
-
r.readU16(); // format_version
|
|
83
|
-
const hasDaemon = r.readU8() === 1;
|
|
84
|
-
const n = r.readU16(); // n_scheduled
|
|
85
|
-
// The 5-byte header must read cleanly; a short read here is a garbage section
|
|
86
|
-
// (fail closed -> no daemon). A `hasDaemon` byte salvaged from a too-short
|
|
87
|
-
// version field is not trustworthy.
|
|
88
|
-
if (!r.ok) return null;
|
|
89
|
-
const tasks: ScheduledTask[] = [];
|
|
90
|
-
for (let i = 0; i < n && r.ok; i++) {
|
|
91
|
-
const name = r.readString();
|
|
92
|
-
const taskIndex = r.readU16();
|
|
93
|
-
const kind = r.readU8(); // schedule_kind: 0 = interval, 1 = cron
|
|
94
|
-
const intervalMs = r.readU64(); // bigint; 0 when kind = 1
|
|
95
|
-
const minute = r.readU64(); // cron_minute_mask (bits 0..59)
|
|
96
|
-
const hour = r.readU32(); // cron_hour_mask
|
|
97
|
-
const dom = r.readU32(); // cron_dom_mask
|
|
98
|
-
const month = r.readU16(); // cron_month_mask
|
|
99
|
-
const dow = r.readU8(); // cron_dow_mask
|
|
100
|
-
const overlapPolicy = r.readU8();
|
|
101
|
-
const catchupPolicy = r.readU8();
|
|
102
|
-
const gasHint = r.readU64();
|
|
103
|
-
if (r.ok)
|
|
104
|
-
tasks.push({
|
|
105
|
-
name,
|
|
106
|
-
taskIndex,
|
|
107
|
-
schedule:
|
|
108
|
-
kind === 1
|
|
109
|
-
? { kind: 'cron', masks: { minute, hour, dom, month, dow } }
|
|
110
|
-
: { kind: 'interval', ms: Number(intervalMs) },
|
|
111
|
-
overlapPolicy,
|
|
112
|
-
catchupPolicy,
|
|
113
|
-
gasHint,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
// A section that decoded nothing AND claims no daemon is indistinguishable
|
|
117
|
-
// from garbage -> fail closed (no daemon).
|
|
118
|
-
if (!r.ok && tasks.length === 0 && !hasDaemon) return null;
|
|
119
|
-
return { hasDaemon, tasks };
|
|
120
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cron BITMASK evaluation for the dev daemon scheduler. The catalog carries five
|
|
3
|
-
* precomputed bitmasks (RECONCILIATION F6 / Part 5); this module does bit tests
|
|
4
|
-
* against a wall-clock minute, NEVER a cron-string parse. The bit semantics match
|
|
5
|
-
* the toilscript emitter (`expandCronField` in `dbcatalog.ts`):
|
|
6
|
-
*
|
|
7
|
-
* minute bits 0..59 hour bits 0..23 dom bits 1..31
|
|
8
|
-
* month bits 1..12 dow bits 0..6 (0 = Sunday)
|
|
9
|
-
*
|
|
10
|
-
* Standard cron semantics for day-of-month vs day-of-week: when BOTH fields are
|
|
11
|
-
* restricted (not all bits set) the match is the UNION (either matches); when one
|
|
12
|
-
* is unrestricted it does not constrain. The dev evaluator walks forward
|
|
13
|
-
* minute-by-minute from "now" to the next matching minute, exactly as the edge
|
|
14
|
-
* evaluates the same masks.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { CronMasks } from './catalog.js';
|
|
18
|
-
|
|
19
|
-
/** All 60 minute bits set (bits 0..59). A field the emitter left fully open. */
|
|
20
|
-
const ALL_MINUTES = (1n << 60n) - 1n;
|
|
21
|
-
/** All 24 hour bits set (bits 0..23). */
|
|
22
|
-
const ALL_HOURS = (1 << 24) - 1;
|
|
23
|
-
/** dom bits 1..31 all set (bit 0 unused). `1 << 32` overflows in JS, so spell it. */
|
|
24
|
-
const ALL_DOM = 0xfffffffe;
|
|
25
|
-
/** month bits 1..12 all set (bit 0 unused). */
|
|
26
|
-
const ALL_MONTH = ((1 << 13) - 1) & ~1;
|
|
27
|
-
/** dow bits 0..6 all set. */
|
|
28
|
-
const ALL_DOW = (1 << 7) - 1;
|
|
29
|
-
|
|
30
|
-
function minuteBit(masks: CronMasks, minute: number): boolean {
|
|
31
|
-
return (masks.minute & (1n << BigInt(minute))) !== 0n;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** True when every cron field's bit is set for `date`'s local-time components. */
|
|
35
|
-
export function cronMatches(masks: CronMasks, date: Date): boolean {
|
|
36
|
-
if (!minuteBit(masks, date.getMinutes())) return false;
|
|
37
|
-
if ((masks.hour & (1 << date.getHours())) === 0) return false;
|
|
38
|
-
if ((masks.month & (1 << date.getMonth() + 1)) === 0) return false;
|
|
39
|
-
|
|
40
|
-
// dom/dow union rule (POSIX cron): if both are restricted, either may match.
|
|
41
|
-
const domRestricted = (masks.dom & ALL_DOM) !== ALL_DOM;
|
|
42
|
-
const dowRestricted = (masks.dow & ALL_DOW) !== ALL_DOW;
|
|
43
|
-
const domHit = (masks.dom & (1 << date.getDate())) !== 0;
|
|
44
|
-
const dowHit = (masks.dow & (1 << date.getDay())) !== 0;
|
|
45
|
-
if (domRestricted && dowRestricted) {
|
|
46
|
-
if (!domHit && !dowHit) return false;
|
|
47
|
-
} else if (domRestricted) {
|
|
48
|
-
if (!domHit) return false;
|
|
49
|
-
} else if (dowRestricted) {
|
|
50
|
-
if (!dowHit) return false;
|
|
51
|
-
}
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** True when a mask can never fire (all-zero) -> the schedule is rejected. */
|
|
56
|
-
export function cronNeverFires(masks: CronMasks): boolean {
|
|
57
|
-
return (
|
|
58
|
-
(masks.minute & ALL_MINUTES) === 0n ||
|
|
59
|
-
(masks.hour & ALL_HOURS) === 0 ||
|
|
60
|
-
(masks.month & ALL_MONTH) === 0 ||
|
|
61
|
-
// dom OR dow must be able to fire (union); both empty => never.
|
|
62
|
-
((masks.dom & ALL_DOM) === 0 && (masks.dow & ALL_DOW) === 0)
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The epoch-ms of the next minute (strictly after `fromMs`) whose components all
|
|
68
|
-
* pass the masks, walking forward minute-by-minute. Returns `null` when no match
|
|
69
|
-
* is found within `horizonMinutes` (a safety bound; an all-zero mask is caught by
|
|
70
|
-
* {@link cronNeverFires} before this is called). Fires land on the :00 second of
|
|
71
|
-
* the matching minute.
|
|
72
|
-
*/
|
|
73
|
-
export function nextCronFireMs(
|
|
74
|
-
masks: CronMasks,
|
|
75
|
-
fromMs: number,
|
|
76
|
-
horizonMinutes = 366 * 24 * 60,
|
|
77
|
-
): number | null {
|
|
78
|
-
// Start at the next whole minute boundary strictly after `fromMs`.
|
|
79
|
-
const d = new Date(fromMs);
|
|
80
|
-
d.setSeconds(0, 0);
|
|
81
|
-
d.setMinutes(d.getMinutes() + 1);
|
|
82
|
-
for (let i = 0; i < horizonMinutes; i++) {
|
|
83
|
-
if (cronMatches(masks, d)) return d.getTime();
|
|
84
|
-
d.setMinutes(d.getMinutes() + 1);
|
|
85
|
-
}
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host-import surface for the dev daemon (cold) box, mirroring the production
|
|
3
|
-
* edge's `daemon.*` imports. DAEMON path only (streams are Phase 4).
|
|
4
|
-
*
|
|
5
|
-
* Per RECONCILIATION:
|
|
6
|
-
* - Part 4 `daemon.*`: is_leader / current_epoch / yield / sleep_ms / task_count
|
|
7
|
-
* / next_fire_ms / http_call. In a single dev process the leader
|
|
8
|
-
* stub is always true and the lease never expires (section 5.2). Fenced DB
|
|
9
|
-
* writes are TRANSPARENT (no `daemon.db_write_fenced` import).
|
|
10
|
-
* - Part 3 error bridge: a u16 subsystem code `c` is returned as `-(0x10000 + c)`;
|
|
11
|
-
* the buffer sentinels `-1` (TOO_SMALL) / `-2` (ABSENT) are unchanged.
|
|
12
|
-
*
|
|
13
|
-
* These live in their OWN wasm module namespace, `daemon`, with BARE names, exactly
|
|
14
|
-
* as the edge registers them (toil-backend `src/wasm/cold/imports.rs`, `NS_DAEMON`).
|
|
15
|
-
* They are NOT dotted names under `env` like `data.*` / `crypto.*` are: a cold box
|
|
16
|
-
* that declares them that way resolves here and then trap-stubs at the edge, which
|
|
17
|
-
* is the one shape of drift a dev emulator must never hide.
|
|
18
|
-
*
|
|
19
|
-
* The cold box also imports the request-surface `env.*` MINUS the response/stream
|
|
20
|
-
* functions it must not have (no `set_status`/`set_header`/`respond_file`/
|
|
21
|
-
* `client_ip`/`ratelimit_check`); it keeps `@data`/crypto/env/`Date.now`/email so a
|
|
22
|
-
* daemon can read+write the DB and send mail. The two allow-lists live in
|
|
23
|
-
* `runtime/module.ts`.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { buildDatabaseImports, type DbDevState, freshDbState } from '../db/index.js';
|
|
27
|
-
import { buildCryptoImports, type CryptoState, freshCryptoState } from '../runtime/crypto.js';
|
|
28
|
-
import { buildEnvImports, type MemoryRef } from '../runtime/host.js';
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resolved daemon (L4) config the dev scheduler reads. Structurally identical to
|
|
32
|
-
* the compiler's `ResolvedDaemonConfig`; declared here so the devserver package has
|
|
33
|
-
* no source dependency on the compiler package (its tsconfig is isolated to
|
|
34
|
-
* `src/devserver`). The compiler's resolved config is passed in verbatim.
|
|
35
|
-
*/
|
|
36
|
-
export interface ResolvedDaemonConfig {
|
|
37
|
-
readonly region: string | null;
|
|
38
|
-
readonly standbyRegion: string | null;
|
|
39
|
-
readonly defaultIntervalMs: number;
|
|
40
|
-
readonly tickBudgetMs: number;
|
|
41
|
-
readonly gasTick: number;
|
|
42
|
-
readonly maxTasks: number;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** RECONCILIATION Part 3 u16 error registry (the subset the daemon imports use). */
|
|
46
|
-
export const enum AbiError {
|
|
47
|
-
DaemonNotLeader = 0x0401,
|
|
48
|
-
DaemonLeaseLost = 0x0402,
|
|
49
|
-
DaemonCallFailed = 0x0405,
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** The edge's `EPOCH_NONE` / "no such task" sentinel: a plain -1, not a bridged error. */
|
|
53
|
-
const DAEMON_NONE = -1n;
|
|
54
|
-
|
|
55
|
-
/** Encode a u16 subsystem error per the Part 3 negative-return bridge:
|
|
56
|
-
* `code = (-v) - 0x10000`, so `v = -(0x10000 + code)`. */
|
|
57
|
-
export function encodeAbiError(code: number): number {
|
|
58
|
-
return -(0x10000 + code);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The minimal scheduler/leader view the `daemon.*` imports read. Implemented by
|
|
62
|
-
* the resident `DaemonHost`. */
|
|
63
|
-
export interface DaemonRuntime {
|
|
64
|
-
isLeader(): boolean;
|
|
65
|
-
/** Monotonic fencing token; bumps on each (re)start. */
|
|
66
|
-
epoch(): bigint;
|
|
67
|
-
/** Number of registered `@scheduled` tasks. */
|
|
68
|
-
taskCount(): number;
|
|
69
|
-
/** Next computed fire time (epoch ms) for `taskId`, or `null` if unknown. */
|
|
70
|
-
nextFireMs(taskId: number): number | null;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** A host-import map: each value is a function over i32 args (i64 params are typed
|
|
74
|
-
* `number | bigint` individually, matching the existing db/crypto import maps). */
|
|
75
|
-
type HostFnMap = Record<string, (...args: number[]) => number | bigint>;
|
|
76
|
-
|
|
77
|
-
/** Build the `daemon` namespace, closing over the resident `DaemonRuntime`. The names
|
|
78
|
-
* are BARE (`is_leader`, not `daemon.is_leader`): they are keyed under the `daemon`
|
|
79
|
-
* wasm module, matching the edge. These imports do not read guest memory (they answer
|
|
80
|
-
* from the resident scheduler state), so they take no `MemoryRef`. */
|
|
81
|
-
export function buildDaemonNamespace(rt: DaemonRuntime): HostFnMap {
|
|
82
|
-
return {
|
|
83
|
-
is_leader: (): number => (rt.isLeader() ? 1 : 0),
|
|
84
|
-
// The edge answers -1 (EPOCH_NONE) when this node does not hold the lease.
|
|
85
|
-
current_epoch: (): bigint => (rt.isLeader() ? rt.epoch() : DAEMON_NONE),
|
|
86
|
-
// The dev lease never expires, so yield/sleep never report LEASE_LOST.
|
|
87
|
-
yield: (): number => 0,
|
|
88
|
-
sleep_ms: (_ms: number | bigint): number => 0,
|
|
89
|
-
task_count: (): number => rt.taskCount(),
|
|
90
|
-
// An unknown / never-firing task is -1 at the edge, NOT a bridged error.
|
|
91
|
-
next_fire_ms: (taskId: number): bigint => {
|
|
92
|
-
const at = rt.nextFireMs(taskId);
|
|
93
|
-
return at === null ? DAEMON_NONE : BigInt(at);
|
|
94
|
-
},
|
|
95
|
-
// Outbound HTTP call stub: dev returns a "call failed" sentinel rather than
|
|
96
|
-
// performing real network I/O from a synchronous wasm import (section 5.4).
|
|
97
|
-
http_call: (
|
|
98
|
-
_reqPtr: number,
|
|
99
|
-
_reqLen: number,
|
|
100
|
-
_outPtr: number,
|
|
101
|
-
_outCap: number,
|
|
102
|
-
): bigint => BigInt(encodeAbiError(AbiError.DaemonCallFailed)),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Per-cold-box host state (DB + crypto scratch), analogous to `DispatchState`. */
|
|
107
|
-
export interface DaemonState {
|
|
108
|
-
crypto: CryptoState;
|
|
109
|
-
db: DbDevState;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function freshDaemonState(): DaemonState {
|
|
113
|
-
return { crypto: freshCryptoState(), db: freshDbState() };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* The import object for the cold daemon box: the request-surface `env` MINUS the
|
|
118
|
-
* response/stream functions (built by `buildEnvImports`), plus the separate
|
|
119
|
-
* `daemon` module. The cold box has no `handle` entry and no response surface.
|
|
120
|
-
*/
|
|
121
|
-
export function buildDaemonImports(
|
|
122
|
-
ref: MemoryRef,
|
|
123
|
-
state: DaemonState,
|
|
124
|
-
rt: DaemonRuntime,
|
|
125
|
-
): WebAssembly.Imports {
|
|
126
|
-
return {
|
|
127
|
-
env: {
|
|
128
|
-
...buildEnvImports(ref, state),
|
|
129
|
-
...buildCryptoImports(ref, state.crypto),
|
|
130
|
-
...buildDatabaseImports(ref, state.db),
|
|
131
|
-
},
|
|
132
|
-
daemon: buildDaemonNamespace(rt),
|
|
133
|
-
};
|
|
134
|
-
}
|