@jimhoyd/urlcode 0.4.1 → 0.4.6
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/.claude/skills/urlcode-authoring/SKILL.md +60 -7
- package/.claude/skills/urlcode-operations/SKILL.md +4 -0
- package/README.md +19 -15
- package/SECURITY.md +5 -3
- package/dist/BUILD-MANIFEST.json +31 -28
- package/dist/agent-context.js +82 -0
- package/dist/agents-guide.js +42 -42
- package/dist/authoring.js +12 -2
- package/dist/body-schema.js +159 -0
- package/dist/build-cloudflare.js +2 -0
- package/dist/capabilities.js +1 -1
- package/dist/cli.js +29 -12
- package/dist/config.js +74 -6
- package/dist/context.js +4 -6
- package/dist/errors.js +3 -1
- package/dist/examples.js +1 -1
- package/dist/extensions.js +138 -3
- package/dist/http-policy.js +19 -4
- package/dist/http-response.js +2 -2
- package/dist/init-with.js +71 -9
- package/dist/mcp.js +15 -2
- package/dist/pattern-guard.js +32 -0
- package/dist/policies/security.js +0 -0
- package/dist/policy.js +16 -0
- package/dist/project-tests.js +35 -11
- package/dist/readiness.js +188 -32
- package/dist/recipes.js +1 -1
- package/dist/router.js +17 -0
- package/dist/runtime.js +26 -3
- package/dist/scaffold.js +0 -0
- package/dist/server.js +26 -3
- package/dist/site.js +0 -0
- package/dist/tooling.js +3 -1
- package/dist/types/agent-context.d.ts +44 -0
- package/dist/types/authoring.d.ts +3 -1
- package/dist/types/body-schema.d.ts +58 -0
- package/dist/types/config.d.ts +10 -2
- package/dist/types/context.d.ts +1 -1
- package/dist/types/errors.d.ts +9 -1
- package/dist/types/examples.d.ts +1 -1
- package/dist/types/extensions.d.ts +105 -2
- package/dist/types/http-policy.d.ts +3 -0
- package/dist/types/init-with.d.ts +10 -1
- package/dist/types/pattern-guard.d.ts +10 -0
- package/dist/types/project-tests.d.ts +9 -0
- package/dist/types/readiness.d.ts +71 -0
- package/dist/types/recipes.d.ts +1 -1
- package/dist/types/runtime.d.ts +4 -0
- package/dist/types/server.d.ts +9 -1
- package/dist/types/tooling.d.ts +2 -0
- package/dist/types/types.d.ts +13 -0
- package/dist/types.js +10 -2
- package/dist/typescript-authoring.js +5 -3
- package/dist/verify-deployment.js +16 -10
- package/examples/body-validation/README.md +16 -0
- package/examples/body-validation/example.yaml +17 -0
- package/examples/body-validation/tests/requests.json +15 -0
- package/examples/body-validation/urlcode.yaml +34 -0
- package/examples/coverage-waiver/README.md +8 -0
- package/examples/coverage-waiver/example.yaml +16 -0
- package/examples/coverage-waiver/functions/notes.mjs +2 -0
- package/examples/coverage-waiver/tests/requests.json +3 -0
- package/examples/coverage-waiver/urlcode.yaml +10 -0
- package/examples/data-dir/README.md +39 -0
- package/examples/data-dir/data/welcome.txt +1 -0
- package/examples/data-dir/example.yaml +22 -0
- package/examples/data-dir/functions/note.mjs +18 -0
- package/examples/data-dir/tests/requests.json +6 -0
- package/examples/data-dir/urlcode.yaml +6 -0
- package/examples/lifecycle/README.md +15 -0
- package/examples/lifecycle/example.yaml +19 -0
- package/examples/lifecycle/functions/notes.mjs +29 -0
- package/examples/lifecycle/tests/requests.json +12 -0
- package/examples/lifecycle/urlcode.yaml +29 -0
- package/examples/not-found/README.md +10 -0
- package/examples/not-found/example.yaml +17 -0
- package/examples/not-found/public/404.html +3 -0
- package/examples/not-found/public/index.html +3 -0
- package/examples/not-found/tests/requests.json +7 -0
- package/examples/not-found/urlcode.yaml +7 -0
- package/examples/shared-blocks/README.md +11 -0
- package/examples/shared-blocks/example.yaml +17 -0
- package/examples/shared-blocks/tests/requests.json +7 -0
- package/examples/shared-blocks/urlcode.yaml +37 -0
- package/llms-full.txt +436 -149
- package/llms.txt +44 -6
- package/package.json +15 -22
- package/recipes/static-page/README.md +9 -0
- package/recipes/static-page/public/index.html +11 -0
- package/recipes/static-page/recipe.yaml +21 -0
- package/recipes/static-page/tests/requests.json +22 -0
- package/recipes/static-page/urlcode.yaml +7 -0
- package/recipes/static-plus-api/README.md +6 -0
- package/recipes/static-plus-api/urlcode.yaml +4 -0
- package/recipes/store-crud/README.md +53 -0
- package/recipes/store-crud/recipe.yaml +31 -0
- package/recipes/store-crud/tests/requests.json +18 -0
- package/recipes/store-crud/urlcode.yaml +18 -0
- package/schemas/urlcode.schema.json +125 -60
- package/skills/urlcode/SKILL.md +53 -26
- package/starters/default/AGENTS.md +43 -43
- package/starters/page/README.md +14 -0
- package/starters/page/public/index.html +12 -0
- package/starters/page/tests/requests.json +17 -0
- package/starters/page/urlcode.yaml +6 -0
- package/.claude-plugin/marketplace.json +0 -18
- package/CONTRIBUTING.md +0 -112
- package/ROADMAP.md +0 -66
- package/docs/AI-AUTHORING.md +0 -338
- package/docs/ASSETS.md +0 -107
- package/docs/AUTH-BACKUP.md +0 -32
- package/docs/AWS.md +0 -86
- package/docs/BEST-PRACTICES.md +0 -276
- package/docs/BULK.md +0 -79
- package/docs/CAPABILITIES.md +0 -192
- package/docs/CAPACITY.md +0 -305
- package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
- package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
- package/docs/CI.md +0 -147
- package/docs/CLOUDFLARE.md +0 -109
- package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
- package/docs/COMPLIANCE.md +0 -239
- package/docs/COMPOSING-A-SITE.md +0 -278
- package/docs/CONDITIONS.md +0 -74
- package/docs/DEPLOYMENT-CHECKS.md +0 -108
- package/docs/DEVELOPMENT-PIPELINE.md +0 -270
- package/docs/EGRESS.md +0 -125
- package/docs/EXTENSIONS.md +0 -438
- package/docs/FRAMEWORK.md +0 -217
- package/docs/FUNCTION-SECURITY.md +0 -254
- package/docs/HTTP.md +0 -129
- package/docs/INSTALL.md +0 -128
- package/docs/INTERCHANGE.md +0 -134
- package/docs/LOAD-TESTING.md +0 -91
- package/docs/LOCAL-DEVELOPMENT.md +0 -102
- package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
- package/docs/MIDDLEWARE.md +0 -102
- package/docs/MONITORING.md +0 -115
- package/docs/OBSERVABILITY.md +0 -222
- package/docs/OPEN-DECISIONS.md +0 -224
- package/docs/OPERATIONAL-PROOF.md +0 -41
- package/docs/OPERATIONS.md +0 -201
- package/docs/ORGANIZATION.md +0 -135
- package/docs/PERFORMANCE.md +0 -72
- package/docs/PLUGINS.md +0 -271
- package/docs/POLICIES.md +0 -211
- package/docs/PRERENDER.md +0 -245
- package/docs/PROJECT-DIRECTION.md +0 -118
- package/docs/PROVIDER-VERIFICATION.md +0 -84
- package/docs/READINESS.md +0 -150
- package/docs/README.md +0 -87
- package/docs/RECIPES.md +0 -99
- package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
- package/docs/RELEASE-0.4.1.md +0 -73
- package/docs/RELEASE-READINESS.md +0 -117
- package/docs/RELEASE-SECURITY.md +0 -96
- package/docs/RESILIENCE.md +0 -161
- package/docs/ROUTING.md +0 -92
- package/docs/SANDBOX-REVIEW.md +0 -72
- package/docs/SCAFFOLDING.md +0 -70
- package/docs/SECURITY-AUDIT.md +0 -164
- package/docs/SITE.md +0 -150
- package/docs/SPECIFICATION.md +0 -359
- package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
- package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
- package/docs/SPIKE-CORE-LAYERING.md +0 -368
- package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -211
- package/docs/STANDARDS.md +0 -311
- package/docs/STARTERS.md +0 -83
- package/docs/STATIC.md +0 -105
- package/docs/TOOLING.md +0 -298
- package/docs/TUNNELS.md +0 -72
- package/docs/TYPESCRIPT-AUTHORING.md +0 -87
- package/docs/TYPESCRIPT.md +0 -123
- package/docs/VERCEL.md +0 -114
- package/docs/VERSION-ALIGNMENT.md +0 -80
- package/docs/YAML-GUIDE.md +0 -57
- package/docs/YAML-REFERENCE.md +0 -449
- package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
- package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
- package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
- package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
- package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
- package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
- package/docs/archive/2026-09-19/ROADMAP.md +0 -387
- package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
- package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
- package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
- package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
- package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
- package/docs/archive/README.md +0 -28
- package/docs/policies/agents.md +0 -182
- package/docs/policies/cache.md +0 -152
- package/docs/policies/compression.md +0 -169
- package/docs/policies/contract.md +0 -52
- package/docs/policies/hardened.md +0 -56
- package/docs/policies/interoperability.md +0 -169
- package/docs/policies/operations.md +0 -45
- package/docs/policies/security.md +0 -161
- package/docs/policies/throttle.md +0 -103
- package/docs/yaml/assets.md +0 -36
- package/docs/yaml/conditions.md +0 -20
- package/docs/yaml/functions.md +0 -168
- package/docs/yaml/middleware.md +0 -31
- package/docs/yaml/organization.md +0 -74
- package/docs/yaml/policies.md +0 -37
- package/docs/yaml/redirects.md +0 -64
- package/docs/yaml/responses.md +0 -57
- package/docs/yaml/site.md +0 -24
- package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
- package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -120
- package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
package/docs/CAPACITY.md
DELETED
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
# Capacity, concurrency and system limits
|
|
2
|
-
|
|
3
|
-
These are 0.3.0 implementation limits and planning models, not a throughput
|
|
4
|
-
SLA. Route count, connections, in-flight requests and sandbox concurrency are
|
|
5
|
-
four different quantities. Always measure the actual application on deployment
|
|
6
|
-
hardware with the intended proxy, TLS, logging and limits enabled.
|
|
7
|
-
|
|
8
|
-
## What happens for each request
|
|
9
|
-
|
|
10
|
-
A single Node process accepts HTTP, parses/validates inputs, matches a compiled
|
|
11
|
-
route and builds the response. Exact routes use a Map lookup (expected O(1)
|
|
12
|
-
lookup after path parsing). Parameter candidates are grouped by segment count
|
|
13
|
-
and scanned in specificity order; matching is O(P × L) in the worst case for P
|
|
14
|
-
candidates and L segments. Static mount prefixes are scanned longest first.
|
|
15
|
-
|
|
16
|
-
Plain redirects, declared responses and assets do not enter
|
|
17
|
-
the sandbox or the trusted executor.
|
|
18
|
-
|
|
19
|
-
`function`/`middleware` routes have **two distinct capacity models**, chosen
|
|
20
|
-
per route by `sandbox` (docs/SPIKE-DEFAULT-TRUST-MODEL.md):
|
|
21
|
-
|
|
22
|
-
- **`sandbox: true` (the isolated worker pool, unchanged from every earlier
|
|
23
|
-
release):** a function or any attached middleware occupies one shared
|
|
24
|
-
worker slot for its whole chain. Workers are shared by all `sandbox: true`
|
|
25
|
-
routes in that snapshot; there is no per-route fairness or reserved
|
|
26
|
-
capacity. Awaiting guest timers still occupies the slot. A fresh guest and
|
|
27
|
-
module initialization are part of each call. See "Enforced limits and
|
|
28
|
-
defaults" below for the numbers (2 workers, 5 s deadline, 32 MiB heap).
|
|
29
|
-
- **`sandbox` false/absent (the trusted default):** the call runs in-process,
|
|
30
|
-
on the same event loop as everything else the server does — ordinary Node
|
|
31
|
-
concurrency, not a fixed worker-slot ceiling. There is no separate pool to
|
|
32
|
-
exhaust and no per-invocation heap/module reset: it is bounded by the same
|
|
33
|
-
`--max-in-flight` HTTP admission cap (default 64) that bounds every other
|
|
34
|
-
request, not by a `workers` count. A trusted call's declared `timeoutMs`
|
|
35
|
-
races the call's own promise rather than forcibly terminating a worker
|
|
36
|
-
thread — see "Trusted-path deadlines" below for what that does and does not
|
|
37
|
-
protect against.
|
|
38
|
-
|
|
39
|
-
This was architectural reasoning, not a measurement, until the run below:
|
|
40
|
-
see "Measured: sandboxed vs trusted dispatch" for the actual comparison.
|
|
41
|
-
|
|
42
|
-
Node's main event loop remains a shared bottleneck for HTTP parsing, logging and
|
|
43
|
-
native responses. The sandbox contains a `sandbox: true` route's application
|
|
44
|
-
code authority and bounds its individual execution; the trusted default does
|
|
45
|
-
not attempt to, by design. Neither mode makes all host resources immune to
|
|
46
|
-
exhaustion.
|
|
47
|
-
|
|
48
|
-
### Trusted-path deadlines
|
|
49
|
-
|
|
50
|
-
A sandboxed worker's deadline is enforced by an interrupt handler the WASM
|
|
51
|
-
engine checks between guest operations, backed by an independent outer
|
|
52
|
-
termination that kills the worker thread if the guest never yields — the
|
|
53
|
-
worker (and its slot) can be forcibly reclaimed even from a stuck call. A
|
|
54
|
-
trusted, in-process call has no such mechanism available: `timeoutMs` starts
|
|
55
|
-
a race between the call's promise and a timer, so a call that never resolves
|
|
56
|
-
(an unresolved promise, an awaited operation that never completes) is
|
|
57
|
-
answered with a 504 on schedule, but a call that blocks the event loop
|
|
58
|
-
*synchronously* (an infinite `while` loop, a huge synchronous computation)
|
|
59
|
-
is not preempted — it keeps running, delays that timer's own firing, and
|
|
60
|
-
holds up every other request on the same process until it returns control to
|
|
61
|
-
the event loop or the process is restarted. This is a real, documented
|
|
62
|
-
difference from the sandboxed path's guarantee, not an oversight: Node has no
|
|
63
|
-
supported way to interrupt another turn of the same thread's event loop from
|
|
64
|
-
inside it. A route whose trusted code cannot be trusted to yield promptly is
|
|
65
|
-
exactly the kind of route `sandbox: true` exists for.
|
|
66
|
-
|
|
67
|
-
## Enforced limits and defaults
|
|
68
|
-
|
|
69
|
-
| Resource | Current behavior | Scope / configuration |
|
|
70
|
-
|---|---|---|
|
|
71
|
-
| Routes | 100,000 combined | Per project snapshot; schema/loader cap |
|
|
72
|
-
| Parameter routes | 1,000 | Per snapshot; not 1,000 concurrent requests |
|
|
73
|
-
| Included files / YAML size | 256 / 32 MiB per file / 64 MiB aggregate | Parser worker: 256 MiB old heap, 10 s deadline, two concurrent loads per isolate |
|
|
74
|
-
| Route path | 2,048 characters, 32 segments | Configured path; no regex or greedy parameters |
|
|
75
|
-
| Request target / headers | 8,192 characters / 16 KiB headers | Target is checked as a JS string; HTTP header limit is bytes |
|
|
76
|
-
| HTTP connections | 1,024 | Per server; includes keep-alive sockets, not worker slots or users |
|
|
77
|
-
| In-flight application requests | 64 default, no queue; excess gets 503 | From body receipt through response finish/disconnect; health probes exempt |
|
|
78
|
-
| Socket inactivity | 15 s | Destroys inactive sockets, including stalled response writers; not an absolute response deadline |
|
|
79
|
-
| Requests per socket | 1,000 | Connection recycling; not a requests-per-second limit |
|
|
80
|
-
| Header / request receipt / keep-alive timeouts | 10 s / 15 s / 5 s | These are not an overall end-to-end response deadline |
|
|
81
|
-
| Request body | 1 MiB default | Buffered; route maxBytes can tighten to 0–1 MiB |
|
|
82
|
-
| Sandbox concurrency (`sandbox: true` only) | 2 workers, no queue | Shared per `sandbox: true` snapshot; full pool returns 503 |
|
|
83
|
-
| Sandbox execution deadline (`sandbox: true` only) | 5 s default | Entire middleware + handler invocation; forcibly terminates the worker; timeout returns 504 |
|
|
84
|
-
| Trusted concurrency (`sandbox` false/absent, the default) | Ordinary Node concurrency | Bounded by `--max-in-flight` (default 64), not a worker count; no separate pool to exhaust |
|
|
85
|
-
| Trusted execution deadline (`sandbox` false/absent) | 5 s default (same `timeoutMs` knob) | Races the call's promise; cannot preempt synchronous event-loop-blocking code (see "Trusted-path deadlines" above); timeout returns 504 |
|
|
86
|
-
| Guest heap / stack (`sandbox: true` only) | 32 MiB / 512 KiB | Fresh per invocation; not a bound on total process RSS |
|
|
87
|
-
| Outer worker old-generation V8 budget (`sandbox: true` only) | 128 MiB | Separate from WASM/host/native allocations |
|
|
88
|
-
| Function response | 1 MiB default, 16 KiB / 256 header pairs | Buffered text/JSON; YAML headers also bounded; applies to both execution modes |
|
|
89
|
-
| Middleware | 16 entries per route | One shared slot/deadline (`sandbox: true`) or one in-process call (trusted), not 16 independent workers either way |
|
|
90
|
-
| Function sources (`sandbox: true` only) | 128 modules, 1 MiB/module, 4 MiB total | Sandboxed snapshot, including middleware dependencies; a trusted route's own source is hashed for grant pinning but not bundled or budget-limited this way (see docs/FUNCTION-SECURITY.md) |
|
|
91
|
-
| Worker startup (`sandbox: true` only) | 5 s deadline | Failure rejects activation; no untrusted host fallback |
|
|
92
|
-
| Worker replacement (`sandbox: true` only) | Up to 3 exits/minute per slot trigger replacement | Further churn leaves the slot unavailable until reload/restart |
|
|
93
|
-
| Assets | 16 MiB/file, 64 MiB unique contents | Buffered immutable snapshots; 10,000 static entries, depth 20 |
|
|
94
|
-
| Logger buffering | Drop at 1 MiB stdout buffering | Reports logs_dropped when output recovers |
|
|
95
|
-
|
|
96
|
-
The 1,024-connection cap is not a global memory bound, fairness policy or DDoS
|
|
97
|
-
protection. At the default admission/body limits, accepted uploads can buffer up to
|
|
98
|
-
64 MiB of payload before copies and other allocations. Slow readers can hold
|
|
99
|
-
sockets/response memory until completion/disconnect or the 15-second inactivity
|
|
100
|
-
timeout. A peer that continues making progress can stay connected longer. Use
|
|
101
|
-
proxy admission limits, timeouts and OS/container limits.
|
|
102
|
-
|
|
103
|
-
The CLI and the embedding JS API accept `--workers`/`workers` (1–32),
|
|
104
|
-
`--function-timeout-ms`/`timeoutMs` (10–60,000), `--max-response-bytes`/`maxBytes`
|
|
105
|
-
(response limit, 1–16 MiB), `--max-body-bytes`/`maxBodyBytes` (request limit, 1–16 MiB),
|
|
106
|
-
`--max-in-flight`/`maxInFlightRequests` (1–1,024; default 64) and
|
|
107
|
-
`--max-in-flight-health`/`maxInFlightHealthRequests` (1–1,024; default 16). Measure the effect with
|
|
108
|
-
[load testing](LOAD-TESTING.md) rather than guessing; `shedResponses` names the
|
|
109
|
-
limit that bound. These are
|
|
110
|
-
operator choices on `startServer`, not supported YAML fields or CLI flags.
|
|
111
|
-
Route body policy still cannot exceed 1 MiB. More workers consume memory and CPU;
|
|
112
|
-
increasing a timeout also increases how long an attacker can occupy capacity.
|
|
113
|
-
The CLI uses defaults. Keep settings identical across replicas unless testing a
|
|
114
|
-
controlled rollout. See [operations](OPERATIONS.md).
|
|
115
|
-
|
|
116
|
-
## Measured: sandboxed vs trusted dispatch
|
|
117
|
-
|
|
118
|
-
The claim above — that the trusted path has "no fixed worker-slot ceiling"
|
|
119
|
-
and scales as ordinary Node concurrency instead — was written as
|
|
120
|
-
architectural reasoning when the trusted-by-default execution model shipped
|
|
121
|
-
(docs/SPIKE-DEFAULT-TRUST-MODEL.md), not backed by a benchmark run. This is
|
|
122
|
-
that run: `benchmarks/sandbox-vs-trusted.ts`
|
|
123
|
-
(`npm run benchmark:sandbox-vs-trusted`), same 20 ms handler source on both a
|
|
124
|
-
`sandbox: true` route and a trusted route in the same server, default
|
|
125
|
-
settings (`workers: 2`, `maxInFlightRequests: 64`, `timeoutMs: 5000`), 2,000
|
|
126
|
-
requests per concurrency level after a 20-request warmup, keep-alive
|
|
127
|
-
connections. Raw output:
|
|
128
|
-
[benchmarks/results/2026-09-19-sandbox-vs-trusted.json](../benchmarks/results/2026-09-19-sandbox-vs-trusted.json).
|
|
129
|
-
|
|
130
|
-
One development machine: Intel Xeon @ 2.10 GHz, 4 vCPUs, 16 GiB RAM, Linux
|
|
131
|
-
x64, Node v22.22.2, 2026-09-19. This is one machine's numbers, not a
|
|
132
|
-
universal claim — re-run on deployment hardware before sizing anything.
|
|
133
|
-
|
|
134
|
-
| Concurrency | Sandboxed req/s (successful) | Sandboxed shed | Sandboxed p95 | Trusted req/s (successful) | Trusted shed | Trusted p95 |
|
|
135
|
-
|---:|---:|---:|---:|---:|---:|---:|
|
|
136
|
-
| 1 | 39 | 0 / 2000 | 26.9 ms | 47 | 0 / 2000 | 21.5 ms |
|
|
137
|
-
| 2 | 80 | 0 / 2000 | 26.5 ms | 95 | 0 / 2000 | 21.7 ms |
|
|
138
|
-
| 8 | 80 | 1,912 / 2000 | 30.2 ms | 379 | 0 / 2000 | 22.1 ms |
|
|
139
|
-
| 32 | 80 | 1,936 / 2000 | 43.5 ms | 1,465 | 0 / 2000 | 23.5 ms |
|
|
140
|
-
| 128 | 80 | 1,950 / 2000 | 70.0 ms | 2,615 | 521 / 2000 | 80.6 ms |
|
|
141
|
-
|
|
142
|
-
"Sandboxed req/s" and "shed" are separate columns for a reason: throughput
|
|
143
|
-
computed over successful responses only is flat at ~80 req/s from
|
|
144
|
-
concurrency 2 upward because the 2-worker ceiling admits at most two
|
|
145
|
-
in-flight calls at a time and returns 503 immediately for the rest, exactly
|
|
146
|
-
as "Enforced limits and defaults" describes (no queue). The ceiling is
|
|
147
|
-
visible starting at concurrency 8 in this run (already >95% shed) and only
|
|
148
|
-
gets worse as concurrency rises — 2 concurrent in-flight sandboxed calls is
|
|
149
|
-
the practical limit regardless of how much load is offered.
|
|
150
|
-
|
|
151
|
-
The trusted path has no such wall: it scales with offered concurrency —
|
|
152
|
-
379 req/s at 8, 1,465 req/s at 32, both with zero shedding — until offered
|
|
153
|
-
concurrency (128) exceeds the `--max-in-flight` admission cap (64), at which
|
|
154
|
-
point it starts shedding too (521/2000, p95 rises to 80.6 ms) but keeps
|
|
155
|
-
substantially higher successful throughput (2,615 req/s) than the sandboxed
|
|
156
|
-
path ever reaches at any concurrency tested. This confirms the architectural
|
|
157
|
-
prediction rather than contradicting it: the trusted path's ceiling is the
|
|
158
|
-
admission cap, not a fixed worker count, and it sits far above the sandboxed
|
|
159
|
-
path's 2-worker ceiling in this run. It is still a real ceiling — raising
|
|
160
|
-
`--max-in-flight` moves it, it does not remove it — and it is shared with
|
|
161
|
-
every other route on the process, per "Native routes and mixed traffic"
|
|
162
|
-
below.
|
|
163
|
-
|
|
164
|
-
Caveats specific to this run: one handler shape (a 20 ms guest/host timer,
|
|
165
|
-
chosen to make both ceilings visible rather than to represent any particular
|
|
166
|
-
application), one machine, one process with both route types present
|
|
167
|
-
(so CPU contention between them is part of the trusted numbers, not
|
|
168
|
-
excluded), and no TLS, proxy or production logging in the path. Re-run with
|
|
169
|
-
your own handler's actual latency profile before sizing a deployment; see
|
|
170
|
-
"Establish a deployment budget" below.
|
|
171
|
-
|
|
172
|
-
## A useful theoretical model
|
|
173
|
-
|
|
174
|
-
This worker-slot model describes the `sandbox: true` path only. A trusted
|
|
175
|
-
route has no fixed worker count to plug in as W; its ceiling is ordinary Node
|
|
176
|
-
request concurrency bounded by `--max-in-flight`, not this model — see
|
|
177
|
-
"Measured: sandboxed vs trusted dispatch" above for what that ceiling looks
|
|
178
|
-
like in practice.
|
|
179
|
-
|
|
180
|
-
Let W be worker slots, S the measured mean slot occupancy in seconds (including
|
|
181
|
-
sandbox startup and cleanup effects), and lambda the offered programmable
|
|
182
|
-
requests per second. An idealized worker ceiling is:
|
|
183
|
-
|
|
184
|
-
```text
|
|
185
|
-
worker-limited throughput <= W / S
|
|
186
|
-
mean offered worker load A = lambda * S
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
This ignores CPU contention, event-loop work, garbage collection, worker failures
|
|
190
|
-
and network overhead. It is an upper bound under simplified assumptions, not a
|
|
191
|
-
recommended arrival rate. With the default W=2:
|
|
192
|
-
|
|
193
|
-
| Mean slot time S | Idealized ceiling W/S |
|
|
194
|
-
|---|---:|
|
|
195
|
-
| 5 ms | 400 requests/s |
|
|
196
|
-
| 50 ms | 40 requests/s |
|
|
197
|
-
| 500 ms | 4 requests/s |
|
|
198
|
-
| 5 s | 0.4 requests/s (at the timeout boundary; not useful successful capacity) |
|
|
199
|
-
|
|
200
|
-
No queue means requests are rejected when both slots are occupied, even if the
|
|
201
|
-
average arrival rate is below the ceiling. Under a simplified independent
|
|
202
|
-
Poisson-arrival loss model, Erlang B gives blocking probability:
|
|
203
|
-
|
|
204
|
-
```text
|
|
205
|
-
B(W,A) = (A^W / W!) / sum(k=0..W, A^k / k!)
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
For W=2 and S=50 ms, an offered 20 requests/s gives A=1 and B=20%. That is a
|
|
209
|
-
model illustration, not a measured URLCode result. Bursts, correlated traffic and
|
|
210
|
-
CPU-dependent service times can differ substantially. Measure rejection rate as
|
|
211
|
-
well as latency; fast 503 responses must not count as successful throughput.
|
|
212
|
-
An upstream bounded queue may smooth bursts but adds latency and memory; it is
|
|
213
|
-
not included in this runtime. Unbounded queues just move the failure.
|
|
214
|
-
|
|
215
|
-
Little's law, L=lambda*R, describes average in-flight work for a stable system
|
|
216
|
-
using admitted/completed throughput and mean residence time. It does not turn
|
|
217
|
-
1,024 sockets into 1,024 execution slots or predict tail latency. CPU and bandwidth
|
|
218
|
-
put separate ceilings on throughput. For CPU-bound work, adding workers beyond
|
|
219
|
-
available cores cannot produce linear scaling. Bandwidth must also carry asset
|
|
220
|
-
bytes, response headers, TLS and protocol overhead.
|
|
221
|
-
|
|
222
|
-
## Native routes and mixed traffic
|
|
223
|
-
|
|
224
|
-
For native-only traffic, the Node event loop, network, buffers and logging dominate;
|
|
225
|
-
the W/S sandbox model does not apply. Function saturation does not itself consume
|
|
226
|
-
native route worker slots. However all routes share the server/event loop and
|
|
227
|
-
host resources, so a flood can still degrade ordinary redirects and health checks.
|
|
228
|
-
A single expensive function can starve other functions. For stronger isolation,
|
|
229
|
-
use separate processes/containers and proxy routing; there is no per-route pool
|
|
230
|
-
configuration in YAML today.
|
|
231
|
-
|
|
232
|
-
Horizontal replicas can add capacity if balanced well and supplied identical
|
|
233
|
-
runtime/application revisions and bindings. Scaling is not perfectly linear,
|
|
234
|
-
and capacity falls during failures/rollouts. Rate limits must account for all
|
|
235
|
-
replicas. In-memory counters in middleware reset per request and cannot implement
|
|
236
|
-
a shared rate limiter or durable application state.
|
|
237
|
-
|
|
238
|
-
Optional [policies](POLICIES.md) keep their state per runtime instance, and
|
|
239
|
-
their memory bounds are per instance too: the `throttle` counter table is one
|
|
240
|
-
LRU table per runtime capped by the largest declared `maxKeys` (default
|
|
241
|
-
100,000 keys), and the `cache` policy's origin cache is bounded by its
|
|
242
|
-
`maxEntries` and `maxBytes` per route and by 64 MiB of bodies across the
|
|
243
|
-
whole runtime; the `compression` policy holds up to 64 MiB of precompressed
|
|
244
|
-
asset variants per runtime, the same figure as the asset snapshot itself, so
|
|
245
|
-
a fully policied instance can hold three such budgets. Neither is shared between replicas or
|
|
246
|
-
serverless instances, so a client budget across N replicas is up to N times
|
|
247
|
-
the declared quota and a cached response is computed once per replica. Both
|
|
248
|
-
tables are dropped on a snapshot reload. Sharing state across instances is a
|
|
249
|
-
[plugin](PLUGINS.md) concern.
|
|
250
|
-
|
|
251
|
-
## Memory, startup and reload
|
|
252
|
-
|
|
253
|
-
A practical memory budget includes the Node baseline, parsed YAML/compiled route
|
|
254
|
-
objects, asset snapshots, source copies in workers, WASM heaps, active request
|
|
255
|
-
and response buffers, sockets, logs and transient garbage collection allocations.
|
|
256
|
-
These are not all covered by worker heap limits. Production needs measured peak
|
|
257
|
-
RSS with an OS/container ceiling, plus headroom.
|
|
258
|
-
|
|
259
|
-
Reload constructs a complete new snapshot while the old one serves/drains. Old
|
|
260
|
-
and new assets and worker pools can overlap; repeated reloads with in-flight calls
|
|
261
|
-
can retain multiple generations. Host route compilation and snapshot transfer can delay the shared event
|
|
262
|
-
loop even though the HTTP listener is not restarted. Do not equate atomic swap
|
|
263
|
-
with zero latency impact or incremental route updates. Prefer candidate replicas
|
|
264
|
-
and traffic switching for production. `serve` does not watch configuration.
|
|
265
|
-
|
|
266
|
-
A trusted route's own entry file is re-imported fresh on every reload (see
|
|
267
|
-
[docs/FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)), matching the sandboxed
|
|
268
|
-
pool rebuilding its whole snapshot; a file that entry only imports is not,
|
|
269
|
-
since ordinary Node module resolution — not a per-reload snapshot — governs
|
|
270
|
-
it. Restart the process rather than reload after editing a trusted route's
|
|
271
|
-
dependency, not just its declared `source`.
|
|
272
|
-
|
|
273
|
-
Before parser-worker limits were introduced, recorded 100k-route startup RSS was about 621 MiB on one development machine,
|
|
274
|
-
above the illustrative 512 MiB container example. Route limits are acceptance
|
|
275
|
-
caps, not a promise that the maximum fits your deployment. See [measurements](PERFORMANCE.md).
|
|
276
|
-
That short benchmark uses 5,000 measured requests and does not exercise all routes
|
|
277
|
-
in the larger datasets; client/server share a process, logs are off, and no TLS
|
|
278
|
-
or production proxy is involved. No NGINX performance ratio has been measured.
|
|
279
|
-
|
|
280
|
-
## Establish a deployment budget
|
|
281
|
-
|
|
282
|
-
1. Pin runtime, app, dependency locks and image; record CPU, RAM, Node, proxy/TLS
|
|
283
|
-
settings, workers, timeouts and logging. Select representative input/body sizes.
|
|
284
|
-
2. Measure native redirects, parameter hits/misses, functions, middleware and
|
|
285
|
-
assets separately, then use the expected mixed workload and hot-route skew.
|
|
286
|
-
3. Use a separate load generator for deployment tests. Increase offered rate and
|
|
287
|
-
concurrency gradually; record successful throughput, all status counts,
|
|
288
|
-
p50/p95/p99, CPU, peak RSS, sockets, restarts and network bytes.
|
|
289
|
-
4. Include bursts, slow clients, saturation, invalid inputs and one failed replica.
|
|
290
|
-
Sustain tests long enough to observe memory/GC behavior and stable plateaus.
|
|
291
|
-
5. Choose admission limits below the measured failure knee with explicit spare
|
|
292
|
-
capacity for a replica loss. Verify the service recovers after load stops.
|
|
293
|
-
6. Record the accepted load, error and latency budgets and repeat after changes.
|
|
294
|
-
|
|
295
|
-
The built-in local benchmark is a quick correctness-aware signal, not the above
|
|
296
|
-
production exercise. The readiness endpoint can stay 200 while all worker slots
|
|
297
|
-
are busy. Use error/latency signals too. No universal safe RPS can be derived
|
|
298
|
-
from the route count or these defaults alone. See [resilience](RESILIENCE.md).
|
|
299
|
-
|
|
300
|
-
Configuration parsing/schema validation now run in a terminated-on-deadline worker;
|
|
301
|
-
route compilation still runs cooperatively on the host (10 seconds, yields every
|
|
302
|
-
64 routes). Source, AST, structured-clone output, compiled routes, assets, module
|
|
303
|
-
snapshots and overlapping runtimes all consume memory. Worker V8 limits do not cap
|
|
304
|
-
external buffers or aggregate process RSS. Enforce container/process limits and
|
|
305
|
-
operator-controlled activation; see [review scope](SANDBOX-REVIEW.md).
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# CI and release follow-up, September 19, 2026
|
|
2
|
-
|
|
3
|
-
This is the evidence and disposition for [#185](https://github.com/jimhoyd-com/urlcode/issues/185),
|
|
4
|
-
after the Windows fixes (#190) and matrix change (#192). It does not authorize
|
|
5
|
-
publication or change required checks.
|
|
6
|
-
|
|
7
|
-
## Reproducible measurements
|
|
8
|
-
|
|
9
|
-
```sh
|
|
10
|
-
npm run ci:history -- 100 2026-09-19 > /tmp/ci-history.json
|
|
11
|
-
npm run ci:report -- 35482828280
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
The history helper reads GitHub through `gh`, with four concurrent requests at
|
|
15
|
-
most. It does not dispatch new runs. Increase the limit (maximum 1,000) as more
|
|
16
|
-
history accumulates. Use a date cutoff after the rollout when comparing the
|
|
17
|
-
same workflow generation. It groups by event, inferred lane and exact matrix,
|
|
18
|
-
reports counts and nearest-rank p50/p95, and flags groups with fewer than 20
|
|
19
|
-
successful first attempts. Failures/cancellations remain visible but are excluded
|
|
20
|
-
from successful-run percentiles. Reruns are excluded because run creation time
|
|
21
|
-
includes the human delay before retrying. Earlier attempts are not a complete
|
|
22
|
-
usage ledger. Raw job/step durations and run URLs remain in the output.
|
|
23
|
-
|
|
24
|
-
Workflow elapsed time ends at the last active job's completion. Runner-minutes
|
|
25
|
-
sum overlapping job execution durations: they are neither wall time nor billed
|
|
26
|
-
minutes. Job-creation-to-start is an observed scheduling interval, not a guarantee
|
|
27
|
-
of pure runner queue time. Workflow-creation-to-start also includes dependencies.
|
|
28
|
-
Missing timestamps remain missing; they are never treated as zero.
|
|
29
|
-
|
|
30
|
-
The initial 100-run sample spans September 19 04:59 UTC through September 20
|
|
31
|
-
02:00 UTC. Successful first-attempt results:
|
|
32
|
-
|
|
33
|
-
| Configuration | Samples | Wall p50 / p95 | Runner-minute p50 / p95 |
|
|
34
|
-
| --- | ---: | --- | --- |
|
|
35
|
-
| Historical combined main, 9 jobs | 20 | 8m27s / 9m04s | 49.82 / 53.62 |
|
|
36
|
-
| Historical combined PR, 3 jobs | 40 | 6m12s / 8m52s | 17.28 / 24.57 |
|
|
37
|
-
| Split full main, 18 jobs | 1 | 7m36s / 7m36s | 74.30 / 74.30 |
|
|
38
|
-
| Split compact main, 10 jobs | 1 | 6m48s / 6m48s | 43.20 / 43.20 |
|
|
39
|
-
| Split compact PR, 10 jobs | 2 | 6m38s / 7m15s | 40.23 / 43.58 |
|
|
40
|
-
|
|
41
|
-
Historical groups contain changing code and workflow revisions; they are context,
|
|
42
|
-
not a controlled experiment. The sample includes no classified docs-only runs.
|
|
43
|
-
It cannot satisfy the 20-run acceptance criterion for the new lanes. Do not
|
|
44
|
-
manufacture 20 redundant workflow runs to fill the sample.
|
|
45
|
-
|
|
46
|
-
The [compact main run](https://github.com/jimhoyd-com/urlcode/actions/runs/35482828280)
|
|
47
|
-
used 31.1 fewer runner-minutes than the preceding
|
|
48
|
-
[full matrix main run](https://github.com/jimhoyd-com/urlcode/actions/runs/35482515877),
|
|
49
|
-
an observed 41.9% reduction between two runs, not an established long-term rate.
|
|
50
|
-
Its Windows core job took 377 seconds: 258 in `npm test`, 58 in package smoke,
|
|
51
|
-
13 in installation. Linux Node 22 spent 300 of its 355 seconds in `npm test`.
|
|
52
|
-
The next useful optimization target is the runtime suite, not removing static,
|
|
53
|
-
container, audit or provenance checks. CLI subcommands are separate steps in the
|
|
54
|
-
raw report. File-level profiling is still needed before choosing balanced shards;
|
|
55
|
-
extra shards would add setup and runner pressure.
|
|
56
|
-
|
|
57
|
-
## Changes delivered in this follow-up
|
|
58
|
-
|
|
59
|
-
- A Windows validation failure exposed rejected auth initialization returning
|
|
60
|
-
before its worker terminated. The caller now awaits termination before receiving
|
|
61
|
-
the original error; a deterministic regression test proves the ordering.
|
|
62
|
-
- `ci:history` makes the remaining baseline measurable without new CI jobs.
|
|
63
|
-
- Auth/admin release instructions now use scoped monorepo tags and the correct
|
|
64
|
-
trusted-publisher workflow filenames. Package agent guides file issues here
|
|
65
|
-
and point documentation at root `docs/`.
|
|
66
|
-
- Optional evals checks its credential before checkout/setup/install, and retains
|
|
67
|
-
artifacts for 14 days. This avoids unused setup; it does not remove the weekly
|
|
68
|
-
quality check or claim measured savings from a workflow with no prior runs.
|
|
69
|
-
- The manual signed candidate builds all four tarballs and installs them together
|
|
70
|
-
in a temporary consumer outside the workspace. It verifies the peer dependency
|
|
71
|
-
tree, installed versions, public imports, and `init --with ui,auth,admin`.
|
|
72
|
-
`train.json` records package SHA-512 integrity and the source commit; the
|
|
73
|
-
candidate manifest/checksums and provenance include the extension archives.
|
|
74
|
-
Failure stops the candidate before attestation/upload. Nothing is published.
|
|
75
|
-
|
|
76
|
-
The candidate validates the proposed package set, not live-provider behavior or
|
|
77
|
-
registry OIDC. Per-package release workflows still prepare their own archives
|
|
78
|
-
and retain their original bytes for retries; candidate archives are not silently
|
|
79
|
-
substituted for published release bytes. Before a release, dispatch the candidate
|
|
80
|
-
at the selected main commit, then the full verification workflow, and inspect
|
|
81
|
-
both results before using the authorized coordinator.
|
|
82
|
-
|
|
83
|
-
## Remaining decisions and external validation
|
|
84
|
-
|
|
85
|
-
| Item | Disposition |
|
|
86
|
-
| --- | --- |
|
|
87
|
-
| Windows packaging/process coverage | Delivered in #190/#192; Node 24 on relevant PRs/main, all supported versions nightly/manually. |
|
|
88
|
-
| 20-run baseline per new lane | Wait for organic runs, then rerun the helper with a rollout cutoff. |
|
|
89
|
-
| Dependency-aware package selection | Keep conservative coverage for now. UI feeds auth/admin; auth feeds admin; core extension/CLI/scaffold changes affect the full composition. A package map must include integration, generated styles, peers and tooling, not just changed directory names. |
|
|
90
|
-
| Changeset/no-release enforcement | Still open. Define explicit core release intent as well as workspace Changesets; require reviewed reasons for no-release cases before implementing a gate. Blanket source-path rules would misclassify tests/tooling and root core is not versioned by Changesets. |
|
|
91
|
-
| Move core into `packages/core` | Separate migration, not a prerequisite for fast CI. Root-relative build, package files, CLI, Docker and starter paths make this higher risk than keeping the explicit root inventory. |
|
|
92
|
-
| OIDC and retained-artifact retry | Needs the next explicitly authorized release. No synthetic run proves npm's trust configuration or partial publication recovery. |
|
|
93
|
-
| Historical GHCR image labels/digests | Blocked on read access: anonymous registry lookup returned 403; organization package API explicitly requires `read:packages`. No labels/digests were verified and no images or publication settings changed. The repository has no `PUBLISH_CONTAINER` variable; inherited organization variables could not be inspected with this credential. |
|
|
94
|
-
| Dependabot grouping / unchanged nightly reuse | No change yet. Measure update PR fanout before grouping; unchanged source can still acquire new advisory findings, so reusing old verification indiscriminately would hide them. |
|
|
95
|
-
|
|
96
|
-
Keep #185 open for these acceptance items. The remaining items are not evidence
|
|
97
|
-
that a publish or recovery rehearsal has already succeeded.
|