1tube 0.1.32 → 0.1.34
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/README.md +97 -0
- package/deno.json +2 -0
- package/deno.lock +152 -6
- package/dist/warmup-client.d.ts +94 -0
- package/dist/warmup-client.js +100 -0
- package/drizzle/20260610161429_init_logs/migration.sql +30 -0
- package/drizzle/20260610161429_init_logs/snapshot.json +305 -0
- package/drizzle/20260610161430_logs_fts/migration.sql +25 -0
- package/drizzle/20260610161430_logs_fts/snapshot.json +305 -0
- package/package.json +14 -4
- package/src/backends/deno/hot-reloader.ts +82 -1
- package/src/backends/deno/worker-entry.ts +78 -5
- package/src/backends/deno/worker-host.ts +501 -16
- package/src/backends/workerd/bundler.ts +69 -1
- package/src/gateway/cors.ts +7 -0
- package/src/gateway/logging.ts +81 -0
- package/src/logs/api.ts +141 -0
- package/src/logs/console-marker.ts +54 -0
- package/src/logs/db.ts +83 -0
- package/src/logs/id.ts +33 -0
- package/src/logs/query.ts +372 -0
- package/src/logs/schema.ts +95 -0
- package/src/logs/writer.ts +388 -0
- package/src/registry.ts +7 -0
- package/src/server.ts +489 -22
- package/src/warmup-client.ts +159 -0
package/README.md
CHANGED
|
@@ -73,6 +73,7 @@ dependency install — **1tube’s runtime graph is just Hono + JSR std**.
|
|
|
73
73
|
| `POST /functions/v1/:name` | Invoke an edge function |
|
|
74
74
|
| `GET /health` | Auth-gated health (`Authorization: Bearer $INTERNAL_KEY`); without auth returns the same minimal `{"status":"ok"}`. |
|
|
75
75
|
| `GET /metrics` | Auth-gated Prometheus exposition (same scheme). |
|
|
76
|
+
| `GET /1tube/warmup` | Boot progress (deferred boot): `{ready, total, loaded, loading, queued, failed}`. CORS-enabled so frontends can poll it to drive a warm-up overlay. |
|
|
76
77
|
|
|
77
78
|
## Configuration
|
|
78
79
|
|
|
@@ -87,6 +88,9 @@ All knobs default to safe-but-backwards-compatible values. The TS gateway
|
|
|
87
88
|
| `--timeout` / `FUNCTION_TIMEOUT_MS` | `150000` | Per-request wall-clock cap (also overridable per function) |
|
|
88
89
|
| `--dev` / `1TUBE_DEV` | off | Applies the well-known local Supabase JWT/secrets. **Refuses to start in prod when JWT_SECRET / SUPABASE_SERVICE_ROLE_KEY are missing or are the public dev defaults.** |
|
|
89
90
|
| `--hmr` / `1TUBE_HMR` | off | File-watch + per-function reload (dev only). |
|
|
91
|
+
| `--defer-boot` / `1TUBE_DEFER_BOOT` | on in dev/HMR | Deferred boot (Deno backend only): the gateway accepts requests immediately while function Workers spawn in the background. Requests for cold functions jump the boot queue and get `503` + `X-1tube-Warming: 1` until ready. Disable with `--no-defer-boot` / `1TUBE_DEFER_BOOT=0`. See [Deferred boot & warm-up overlay](#deferred-boot--warm-up-overlay). |
|
|
92
|
+
| `--warmup-grace-ms` / `1TUBE_WARMUP_GRACE_MS` | `250` | How long the dispatcher waits for a warming function before answering with the warming 503. Keeps fast-loading functions from flashing the frontend overlay. |
|
|
93
|
+
| `1TUBE_HMR_FRESH_WAIT_MS` | `2500` | How long a request waits for an in-flight HMR respawn of its function before falling back to the previous (stale) worker. Fresh code wins by default (Vite-style "request stalls until the rebuild lands"). Stale fallbacks carry `X-1tube-Stale: 1`. `0` = never wait. |
|
|
90
94
|
| `1TUBE_BODY_LIMIT_MB` | `30` | Hono `bodyLimit`; matches Supabase. Returns 413 before the handler runs. |
|
|
91
95
|
| `1TUBE_BODY_READ_MS` | `30000` | Slow-loris guard. Max idle gap (ms) between body chunks before the request is aborted with **408**. NOT a total body-read deadline — large but fast uploads pass through. Set `0` to disable. |
|
|
92
96
|
| `1TUBE_CORS_ORIGIN` | `*` (dev only) | Comma-separated allowlist or `*`. In prod, leaving unset disables CORS. |
|
|
@@ -103,6 +107,99 @@ All knobs default to safe-but-backwards-compatible values. The TS gateway
|
|
|
103
107
|
| `1TUBE_DISABLE_RATE_LIMIT` | unset | Set to `1` to bypass rate limiting entirely. **Load-test / dev only** — production deployments must keep the limiter on. The gateway prints a clear warning at boot when this is enabled. |
|
|
104
108
|
|
|
105
109
|
````
|
|
110
|
+
## Deferred boot & warm-up overlay
|
|
111
|
+
|
|
112
|
+
On the Deno backend, eager boot pays the full cost of spawning one Worker per
|
|
113
|
+
function (each transpiling + evaluating its whole import graph) **before** the
|
|
114
|
+
gateway accepts a single request. With dozens of functions importing heavy
|
|
115
|
+
deps, that's tens of seconds of staring at a boot log.
|
|
116
|
+
|
|
117
|
+
Deferred boot (default whenever `--dev` or `--hmr` is on) flips the order:
|
|
118
|
+
|
|
119
|
+
1. Functions are **discovered instantly** and registered as known-but-queued —
|
|
120
|
+
the gateway starts serving right away. Time-to-first-request drops from
|
|
121
|
+
"sum of all worker spawns" to effectively zero.
|
|
122
|
+
2. Workers spawn in the background with the usual bounded concurrency
|
|
123
|
+
(`--deno-worker-concurrency`, default 8).
|
|
124
|
+
3. A request for a function whose Worker isn't ready yet **bumps it to the
|
|
125
|
+
front of the queue** and spawns it immediately, out-of-band. If it becomes
|
|
126
|
+
ready within the grace window (`--warmup-grace-ms`, default 250 ms) the
|
|
127
|
+
request is served normally and the client never notices. Otherwise the
|
|
128
|
+
gateway answers with the **warming contract** below, and the rest of the
|
|
129
|
+
queue keeps loading in the background.
|
|
130
|
+
|
|
131
|
+
### The warming contract
|
|
132
|
+
|
|
133
|
+
```http
|
|
134
|
+
HTTP/1.1 503 Service Unavailable
|
|
135
|
+
X-1tube-Warming: 1
|
|
136
|
+
Retry-After: 1
|
|
137
|
+
|
|
138
|
+
{"error":"Backend is warming up","reason":"function_warming","function":"hello",
|
|
139
|
+
"state":"loading","warmup":{"total":53,"ready":12,"loading":8,"queued":33,"failed":0}}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`X-1tube-Warming: 1` is the discriminator — a plain 503 (circuit breaker,
|
|
143
|
+
overload) never carries it. It's in `Access-Control-Expose-Headers`, so browser
|
|
144
|
+
JS can read it cross-origin. This is what lets a frontend distinguish "the
|
|
145
|
+
backend is still warming up" from real latency or a real outage and show an
|
|
146
|
+
honest overlay instead of a spinner.
|
|
147
|
+
|
|
148
|
+
`GET /1tube/warmup` reports overall progress (also CORS-enabled), e.g. for a
|
|
149
|
+
progress bar inside the overlay.
|
|
150
|
+
|
|
151
|
+
### Frontend helper
|
|
152
|
+
|
|
153
|
+
The npm package ships a tiny client (`1tube/warmup-client`, no dependencies):
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { createWarmupFetch } from "1tube/warmup-client";
|
|
157
|
+
|
|
158
|
+
const warmFetch = createWarmupFetch({
|
|
159
|
+
onWarmingChange: (warming) => setShowWarmupOverlay(warming), // drive the overlay
|
|
160
|
+
retryDelayMs: 400,
|
|
161
|
+
maxWaitMs: 120_000,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Drop-in fetch replacement: retries through warm-up transparently.
|
|
165
|
+
const resp = await warmFetch(`${GATEWAY}/functions/v1/hello`, { method: "POST" });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`isWarmingResponse(resp)` and `fetchWarmupStatus(baseUrl)` are exported for
|
|
169
|
+
hand-rolled integrations.
|
|
170
|
+
|
|
171
|
+
### Smart HMR reloads
|
|
172
|
+
|
|
173
|
+
The same queue machinery powers HMR on the Deno backend, so a save costs as
|
|
174
|
+
little as possible:
|
|
175
|
+
|
|
176
|
+
- **No-op saves are dropped.** Changed files are content-hashed; a re-save
|
|
177
|
+
with identical bytes (format-on-save no-ops, `touch`, agent tooling
|
|
178
|
+
re-writing the same file) never respawns a Worker.
|
|
179
|
+
- **Leading-edge debounce.** The first fs event of a quiet period flushes
|
|
180
|
+
after ~40 ms instead of the full 200 ms trailing window — a single editor
|
|
181
|
+
save starts reloading almost immediately. Stragglers still coalesce.
|
|
182
|
+
- **MRU-first respawns.** When one edit affects more functions than there are
|
|
183
|
+
spawn lanes (shared-module edits), the functions you've hit most recently
|
|
184
|
+
respawn first; idle ones go last.
|
|
185
|
+
- **Request-driven priority + freshness.** A request for a function whose
|
|
186
|
+
respawn is still queued/in-flight bumps it to the front (out-of-band, above
|
|
187
|
+
the concurrency bound) and waits up to `1TUBE_HMR_FRESH_WAIT_MS` (default
|
|
188
|
+
2500 ms) so save → refresh returns the **new** code, like Vite. If the
|
|
189
|
+
respawn takes longer, the previous worker answers and the response is marked
|
|
190
|
+
`X-1tube-Stale: 1` (CORS-exposed) so tooling can tell the difference.
|
|
191
|
+
- **Targeted re-discovery.** Reloading one function stats only that function's
|
|
192
|
+
dir instead of re-scanning the whole functions root.
|
|
193
|
+
|
|
194
|
+
Notes:
|
|
195
|
+
|
|
196
|
+
- Deferred boot is **off by default in prod** (eager boot keeps the "everything
|
|
197
|
+
is ready before traffic" contract); opt in with `--defer-boot`.
|
|
198
|
+
- Functions that fail to boot answer `503 {"reason":"function_boot_failed"}`
|
|
199
|
+
(not a warming response — retrying won't help).
|
|
200
|
+
- Workerd backend is unaffected: it boots all functions as a single bundle
|
|
201
|
+
generation.
|
|
202
|
+
|
|
106
203
|
## Workerd backend
|
|
107
204
|
|
|
108
205
|
1tube can optionally run each function inside a [workerd](https://github.com/cloudflare/workerd) subprocess instead of importing it directly into the gateway. This gives:
|
package/deno.json
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"@std/path": "jsr:@std/path@^1",
|
|
16
16
|
"@std/assert": "jsr:@std/assert@^1",
|
|
17
17
|
"hono": "npm:hono@4",
|
|
18
|
+
"drizzle-orm": "npm:drizzle-orm@1.0.0-rc.3",
|
|
19
|
+
"drizzle-orm/": "npm:/drizzle-orm@1.0.0-rc.3/",
|
|
18
20
|
"esbuild": "npm:esbuild@^0.28",
|
|
19
21
|
"es-module-lexer": "npm:es-module-lexer@^2",
|
|
20
22
|
"@deno/esbuild-plugin": "jsr:@deno/esbuild-plugin@^1.2",
|
package/deno.lock
CHANGED
|
@@ -11,12 +11,19 @@
|
|
|
11
11
|
"jsr:@std/path@^1.1.1": "1.1.4",
|
|
12
12
|
"jsr:@std/path@^1.1.4": "1.1.4",
|
|
13
13
|
"jsr:@zip-js/zip-js@^2.8.0": "2.8.26",
|
|
14
|
+
"npm:@ai-sdk/anthropic@2": "2.0.80_zod@3.25.76",
|
|
15
|
+
"npm:@ai-sdk/google@2": "2.0.74_zod@3.25.76",
|
|
16
|
+
"npm:@ai-sdk/openai@2": "2.0.106_zod@3.25.76",
|
|
17
|
+
"npm:ai@6": "6.0.193_zod@3.25.76",
|
|
18
|
+
"npm:drizzle-kit@1.0.0-rc.3": "1.0.0-rc.3",
|
|
19
|
+
"npm:drizzle-orm@1.0.0-rc.3": "1.0.0-rc.3_zod@3.25.76",
|
|
14
20
|
"npm:es-module-lexer@2": "2.1.0",
|
|
15
21
|
"npm:esbuild@0.28": "0.28.0",
|
|
16
22
|
"npm:esbuild@~0.25.5": "0.25.12",
|
|
17
|
-
"npm:hono@4": "4.12.
|
|
18
|
-
"npm:hono@^4.12.
|
|
19
|
-
"npm:typescript@latest": "6.0.3"
|
|
23
|
+
"npm:hono@4": "4.12.25",
|
|
24
|
+
"npm:hono@^4.12.25": "4.12.25",
|
|
25
|
+
"npm:typescript@latest": "6.0.3",
|
|
26
|
+
"npm:zod@^3.23.0": "3.25.76"
|
|
20
27
|
},
|
|
21
28
|
"jsr": {
|
|
22
29
|
"@deno/esbuild-plugin@1.2.1": {
|
|
@@ -59,6 +66,72 @@
|
|
|
59
66
|
}
|
|
60
67
|
},
|
|
61
68
|
"npm": {
|
|
69
|
+
"@ai-sdk/anthropic@2.0.80_zod@3.25.76": {
|
|
70
|
+
"integrity": "sha512-7HPgFv91YFFYyZapM/Vy3tRbBq/hL44omh0vz/WikiU7fJFxdOjCeg6Nb089lxM7+qBe7blZsoSRJ7gfAkEo1w==",
|
|
71
|
+
"dependencies": [
|
|
72
|
+
"@ai-sdk/provider@2.0.3",
|
|
73
|
+
"@ai-sdk/provider-utils@3.0.25_zod@3.25.76",
|
|
74
|
+
"zod"
|
|
75
|
+
]
|
|
76
|
+
},
|
|
77
|
+
"@ai-sdk/gateway@3.0.121_zod@3.25.76": {
|
|
78
|
+
"integrity": "sha512-uY248djJRxa5W68MHiyqO8WLdOeKQoRClGg7PVX/VPhVW8SJNM7/l5DcrA5WAM3YfQrLyNkgZa2VOu8T0t8LUw==",
|
|
79
|
+
"dependencies": [
|
|
80
|
+
"@ai-sdk/provider@3.0.10",
|
|
81
|
+
"@ai-sdk/provider-utils@4.0.27_zod@3.25.76",
|
|
82
|
+
"@vercel/oidc",
|
|
83
|
+
"zod"
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
"@ai-sdk/google@2.0.74_zod@3.25.76": {
|
|
87
|
+
"integrity": "sha512-Lhw1742RXc+4pRIvqVXa0jdl5+qdpmw8lj0lm6OchUg9rVGHzymlaxe7CDiYX5U2af4jbjKeTY22LDi3bIycgQ==",
|
|
88
|
+
"dependencies": [
|
|
89
|
+
"@ai-sdk/provider@2.0.3",
|
|
90
|
+
"@ai-sdk/provider-utils@3.0.25_zod@3.25.76",
|
|
91
|
+
"zod"
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
"@ai-sdk/openai@2.0.106_zod@3.25.76": {
|
|
95
|
+
"integrity": "sha512-EFC0rpo1wfe4HIz5KZCE72edP2J7fOeR7wPXzjCDljaTRB1wectKDIKRLowpU4F0mbcJ+XScAsoYNPK/Z20aVQ==",
|
|
96
|
+
"dependencies": [
|
|
97
|
+
"@ai-sdk/provider@2.0.3",
|
|
98
|
+
"@ai-sdk/provider-utils@3.0.25_zod@3.25.76",
|
|
99
|
+
"zod"
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
"@ai-sdk/provider-utils@3.0.25_zod@3.25.76": {
|
|
103
|
+
"integrity": "sha512-CvsRu+32Y8a167s+lrIBtsybvgTHp8j9y+6BeTvLeoW3Q+okw/b4CnNUFOLIXsRaKHQKAH+IHNJPYWywfpw0LA==",
|
|
104
|
+
"dependencies": [
|
|
105
|
+
"@ai-sdk/provider@2.0.3",
|
|
106
|
+
"@standard-schema/spec",
|
|
107
|
+
"eventsource-parser",
|
|
108
|
+
"zod"
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
"@ai-sdk/provider-utils@4.0.27_zod@3.25.76": {
|
|
112
|
+
"integrity": "sha512-ubkAJ+xODouwtmN1tYlvTPphH1hPOBfZaEQe8U7skGvFAnIRs9PPpsq57bC2+Ky/MB4yzhd6YOsxTAx9sGpazw==",
|
|
113
|
+
"dependencies": [
|
|
114
|
+
"@ai-sdk/provider@3.0.10",
|
|
115
|
+
"@standard-schema/spec",
|
|
116
|
+
"eventsource-parser",
|
|
117
|
+
"zod"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"@ai-sdk/provider@2.0.3": {
|
|
121
|
+
"integrity": "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww==",
|
|
122
|
+
"dependencies": [
|
|
123
|
+
"json-schema"
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
"@ai-sdk/provider@3.0.10": {
|
|
127
|
+
"integrity": "sha512-Q3BZ27qfpYqnCYGvE3vt+Qi6LGOF9R5Nmzn+9JoM1lCRsD9mYaIhfJLkSunN48nfGXJ6n+XNV0J/XVpqGQl7Dw==",
|
|
128
|
+
"dependencies": [
|
|
129
|
+
"json-schema"
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
"@drizzle-team/brocli@0.11.0": {
|
|
133
|
+
"integrity": "sha512-hD3pekGiPg0WPCCGAZmusBBJsDqGUR66Y452YgQsZOnkdQ7ViEPKuyP4huUGEZQefp8g34RRodXYmJ2TbCH+tg=="
|
|
134
|
+
},
|
|
62
135
|
"@esbuild/aix-ppc64@0.25.12": {
|
|
63
136
|
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
|
64
137
|
"os": ["aix"],
|
|
@@ -319,6 +392,51 @@
|
|
|
319
392
|
"os": ["win32"],
|
|
320
393
|
"cpu": ["x64"]
|
|
321
394
|
},
|
|
395
|
+
"@js-temporal/polyfill@0.5.1": {
|
|
396
|
+
"integrity": "sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==",
|
|
397
|
+
"dependencies": [
|
|
398
|
+
"jsbi"
|
|
399
|
+
]
|
|
400
|
+
},
|
|
401
|
+
"@opentelemetry/api@1.9.1": {
|
|
402
|
+
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="
|
|
403
|
+
},
|
|
404
|
+
"@standard-schema/spec@1.1.0": {
|
|
405
|
+
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="
|
|
406
|
+
},
|
|
407
|
+
"@vercel/oidc@3.2.0": {
|
|
408
|
+
"integrity": "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="
|
|
409
|
+
},
|
|
410
|
+
"ai@6.0.193_zod@3.25.76": {
|
|
411
|
+
"integrity": "sha512-VQOTOse8+X8kMtg61DNSXlYJzwOW4NjMLDJNk/qxClWsFe4oiyFJDHGGG1oezfGcFzuYuQe/8Z7r4kwiZWh2YQ==",
|
|
412
|
+
"dependencies": [
|
|
413
|
+
"@ai-sdk/gateway",
|
|
414
|
+
"@ai-sdk/provider@3.0.10",
|
|
415
|
+
"@ai-sdk/provider-utils@4.0.27_zod@3.25.76",
|
|
416
|
+
"@opentelemetry/api",
|
|
417
|
+
"zod"
|
|
418
|
+
]
|
|
419
|
+
},
|
|
420
|
+
"drizzle-kit@1.0.0-rc.3": {
|
|
421
|
+
"integrity": "sha512-lwGIi6GTlEYzrtac1Szecns+zrAxjaOoNysfBCKRQOXQE7nMJ8IU2I2S6ek3FHEm1N30dQb5YrTdqV7zNH6n7A==",
|
|
422
|
+
"dependencies": [
|
|
423
|
+
"@drizzle-team/brocli",
|
|
424
|
+
"@js-temporal/polyfill",
|
|
425
|
+
"esbuild@0.25.12",
|
|
426
|
+
"get-tsconfig",
|
|
427
|
+
"jiti"
|
|
428
|
+
],
|
|
429
|
+
"bin": true
|
|
430
|
+
},
|
|
431
|
+
"drizzle-orm@1.0.0-rc.3_zod@3.25.76": {
|
|
432
|
+
"integrity": "sha512-akZOa5UxapFbdBG8IDkfBRpSZJpMHaOJtGgp7oi1oHaiU8S3KN92waHo2l5aRuv1D9tMGYpv3BQFOsiGcNjLTQ==",
|
|
433
|
+
"dependencies": [
|
|
434
|
+
"zod"
|
|
435
|
+
],
|
|
436
|
+
"optionalPeers": [
|
|
437
|
+
"zod"
|
|
438
|
+
]
|
|
439
|
+
},
|
|
322
440
|
"es-module-lexer@2.1.0": {
|
|
323
441
|
"integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ=="
|
|
324
442
|
},
|
|
@@ -388,12 +506,37 @@
|
|
|
388
506
|
"scripts": true,
|
|
389
507
|
"bin": true
|
|
390
508
|
},
|
|
391
|
-
"
|
|
392
|
-
"integrity": "sha512-
|
|
509
|
+
"eventsource-parser@3.1.0": {
|
|
510
|
+
"integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg=="
|
|
511
|
+
},
|
|
512
|
+
"get-tsconfig@4.14.0": {
|
|
513
|
+
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
|
|
514
|
+
"dependencies": [
|
|
515
|
+
"resolve-pkg-maps"
|
|
516
|
+
]
|
|
517
|
+
},
|
|
518
|
+
"hono@4.12.25": {
|
|
519
|
+
"integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="
|
|
520
|
+
},
|
|
521
|
+
"jiti@2.6.1": {
|
|
522
|
+
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
|
523
|
+
"bin": true
|
|
524
|
+
},
|
|
525
|
+
"jsbi@4.3.2": {
|
|
526
|
+
"integrity": "sha512-9fqMSQbhJykSeii05nxKl4m6Eqn2P6rOlYiS+C5Dr/HPIU/7yZxu5qzbs40tgaFORiw2Amd0mirjxatXYMkIew=="
|
|
527
|
+
},
|
|
528
|
+
"json-schema@0.4.0": {
|
|
529
|
+
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
|
|
530
|
+
},
|
|
531
|
+
"resolve-pkg-maps@1.0.0": {
|
|
532
|
+
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="
|
|
393
533
|
},
|
|
394
534
|
"typescript@6.0.3": {
|
|
395
535
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
|
396
536
|
"bin": true
|
|
537
|
+
},
|
|
538
|
+
"zod@3.25.76": {
|
|
539
|
+
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
|
|
397
540
|
}
|
|
398
541
|
},
|
|
399
542
|
"workspace": {
|
|
@@ -403,13 +546,16 @@
|
|
|
403
546
|
"jsr:@std/assert@1",
|
|
404
547
|
"jsr:@std/path@1",
|
|
405
548
|
"jsr:@zip-js/zip-js@^2.8.0",
|
|
549
|
+
"npm:drizzle-orm@1.0.0-rc.3",
|
|
406
550
|
"npm:es-module-lexer@2",
|
|
407
551
|
"npm:esbuild@0.28",
|
|
408
552
|
"npm:hono@4"
|
|
409
553
|
],
|
|
410
554
|
"packageJson": {
|
|
411
555
|
"dependencies": [
|
|
412
|
-
"npm:
|
|
556
|
+
"npm:drizzle-kit@1.0.0-rc.3",
|
|
557
|
+
"npm:drizzle-orm@1.0.0-rc.3",
|
|
558
|
+
"npm:hono@^4.12.25"
|
|
413
559
|
]
|
|
414
560
|
}
|
|
415
561
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend helper for 1tube's deferred-boot warming contract.
|
|
3
|
+
*
|
|
4
|
+
* When the gateway boots with `--defer-boot` (default in dev/HMR mode),
|
|
5
|
+
* a request for a function whose Worker isn't ready yet returns:
|
|
6
|
+
*
|
|
7
|
+
* 503 Service Unavailable
|
|
8
|
+
* X-1tube-Warming: 1
|
|
9
|
+
* Retry-After: 1
|
|
10
|
+
* { "error": "Backend is warming up", "reason": "function_warming", ... }
|
|
11
|
+
*
|
|
12
|
+
* The gateway simultaneously bumps that function to the front of its
|
|
13
|
+
* boot queue, so retrying soon usually succeeds. This module wraps
|
|
14
|
+
* `fetch` so the warming state is surfaced to the UI (show a
|
|
15
|
+
* "Backend is warming up" overlay) instead of looking like real latency.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
*
|
|
19
|
+
* import { createWarmupFetch } from "1tube/warmup-client";
|
|
20
|
+
*
|
|
21
|
+
* const warmFetch = createWarmupFetch({
|
|
22
|
+
* onWarmingChange: (warming) => overlay.toggle(warming),
|
|
23
|
+
* });
|
|
24
|
+
* const resp = await warmFetch("/functions/v1/hello", { method: "POST" });
|
|
25
|
+
*/
|
|
26
|
+
/** Response header the gateway sets on warming 503s. */
|
|
27
|
+
export declare const WARMING_HEADER = "x-1tube-warming";
|
|
28
|
+
/** True when `resp` is the gateway's "function is warming up" response. */
|
|
29
|
+
export declare function isWarmingResponse(resp: Response): boolean;
|
|
30
|
+
export interface WarmupInfo {
|
|
31
|
+
/** 1-based count of attempts made so far (including the initial one). */
|
|
32
|
+
attempt: number;
|
|
33
|
+
/** Milliseconds elapsed since the first attempt. */
|
|
34
|
+
elapsedMs: number;
|
|
35
|
+
}
|
|
36
|
+
export interface WarmupFetchOptions {
|
|
37
|
+
/** Underlying fetch implementation. Defaults to the global `fetch`. */
|
|
38
|
+
fetch?: typeof fetch;
|
|
39
|
+
/**
|
|
40
|
+
* Give up after this long and return the last warming response as-is.
|
|
41
|
+
* Defaults to 120 000 ms. Set 0 to disable retrying entirely (the
|
|
42
|
+
* warming response is returned immediately; `onWarmingChange` still
|
|
43
|
+
* fires so the overlay can be driven manually).
|
|
44
|
+
*/
|
|
45
|
+
maxWaitMs?: number;
|
|
46
|
+
/**
|
|
47
|
+
* Delay between retries, in ms. Defaults to 400. The warming 503's
|
|
48
|
+
* `Retry-After` header is deliberately ignored here — it has seconds
|
|
49
|
+
* granularity, far too coarse for warm-up polling (functions usually
|
|
50
|
+
* become ready within a few hundred ms of being prioritized).
|
|
51
|
+
*/
|
|
52
|
+
retryDelayMs?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Fired with `true` when the first warming response is seen and with
|
|
55
|
+
* `false` once a non-warming response arrives (or retrying stops).
|
|
56
|
+
* Wire this to your "Backend is warming up" overlay.
|
|
57
|
+
*/
|
|
58
|
+
onWarmingChange?: (warming: boolean, info: WarmupInfo) => void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wrap `fetch` so warming 503s are retried transparently while the UI is
|
|
62
|
+
* informed via `onWarmingChange`. The returned function is a drop-in
|
|
63
|
+
* `fetch` replacement for calls into the 1tube gateway.
|
|
64
|
+
*
|
|
65
|
+
* Note on request bodies: retrying re-sends `init.body`. Strings, Blobs,
|
|
66
|
+
* ArrayBuffers, FormData and URLSearchParams are safely re-sendable; a
|
|
67
|
+
* `ReadableStream` body is consumed by the first attempt, so streaming
|
|
68
|
+
* requests are returned without retrying (the overlay callback still
|
|
69
|
+
* fires).
|
|
70
|
+
*/
|
|
71
|
+
export declare function createWarmupFetch(options?: WarmupFetchOptions): (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
72
|
+
export interface WarmupStatus {
|
|
73
|
+
backend: string;
|
|
74
|
+
/** True once every discovered function finished booting. */
|
|
75
|
+
ready: boolean;
|
|
76
|
+
total: number;
|
|
77
|
+
loaded: number;
|
|
78
|
+
loading: string[];
|
|
79
|
+
queued: string[];
|
|
80
|
+
failed: Array<{
|
|
81
|
+
name: string;
|
|
82
|
+
error: string;
|
|
83
|
+
}>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fetch the gateway's boot status from `GET /1tube/warmup`. Useful for a
|
|
87
|
+
* progress bar inside the overlay ("12/53 functions ready").
|
|
88
|
+
*
|
|
89
|
+
* @param baseUrl Gateway origin, e.g. `http://localhost:3100`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function fetchWarmupStatus(baseUrl: string, init?: {
|
|
92
|
+
fetch?: typeof fetch;
|
|
93
|
+
signal?: AbortSignal;
|
|
94
|
+
}): Promise<WarmupStatus>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend helper for 1tube's deferred-boot warming contract.
|
|
3
|
+
*
|
|
4
|
+
* When the gateway boots with `--defer-boot` (default in dev/HMR mode),
|
|
5
|
+
* a request for a function whose Worker isn't ready yet returns:
|
|
6
|
+
*
|
|
7
|
+
* 503 Service Unavailable
|
|
8
|
+
* X-1tube-Warming: 1
|
|
9
|
+
* Retry-After: 1
|
|
10
|
+
* { "error": "Backend is warming up", "reason": "function_warming", ... }
|
|
11
|
+
*
|
|
12
|
+
* The gateway simultaneously bumps that function to the front of its
|
|
13
|
+
* boot queue, so retrying soon usually succeeds. This module wraps
|
|
14
|
+
* `fetch` so the warming state is surfaced to the UI (show a
|
|
15
|
+
* "Backend is warming up" overlay) instead of looking like real latency.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
*
|
|
19
|
+
* import { createWarmupFetch } from "1tube/warmup-client";
|
|
20
|
+
*
|
|
21
|
+
* const warmFetch = createWarmupFetch({
|
|
22
|
+
* onWarmingChange: (warming) => overlay.toggle(warming),
|
|
23
|
+
* });
|
|
24
|
+
* const resp = await warmFetch("/functions/v1/hello", { method: "POST" });
|
|
25
|
+
*/
|
|
26
|
+
/** Response header the gateway sets on warming 503s. */
|
|
27
|
+
export const WARMING_HEADER = "x-1tube-warming";
|
|
28
|
+
/** True when `resp` is the gateway's "function is warming up" response. */
|
|
29
|
+
export function isWarmingResponse(resp) {
|
|
30
|
+
return resp.status === 503 && resp.headers.get(WARMING_HEADER) === "1";
|
|
31
|
+
}
|
|
32
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
33
|
+
/**
|
|
34
|
+
* Wrap `fetch` so warming 503s are retried transparently while the UI is
|
|
35
|
+
* informed via `onWarmingChange`. The returned function is a drop-in
|
|
36
|
+
* `fetch` replacement for calls into the 1tube gateway.
|
|
37
|
+
*
|
|
38
|
+
* Note on request bodies: retrying re-sends `init.body`. Strings, Blobs,
|
|
39
|
+
* ArrayBuffers, FormData and URLSearchParams are safely re-sendable; a
|
|
40
|
+
* `ReadableStream` body is consumed by the first attempt, so streaming
|
|
41
|
+
* requests are returned without retrying (the overlay callback still
|
|
42
|
+
* fires).
|
|
43
|
+
*/
|
|
44
|
+
export function createWarmupFetch(options = {}) {
|
|
45
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
46
|
+
const maxWaitMs = options.maxWaitMs ?? 120_000;
|
|
47
|
+
const retryDelayMs = options.retryDelayMs ?? 400;
|
|
48
|
+
return async (input, init) => {
|
|
49
|
+
const startedAt = Date.now();
|
|
50
|
+
let attempt = 0;
|
|
51
|
+
let warming = false;
|
|
52
|
+
const info = () => ({
|
|
53
|
+
attempt,
|
|
54
|
+
elapsedMs: Date.now() - startedAt,
|
|
55
|
+
});
|
|
56
|
+
const setWarming = (next) => {
|
|
57
|
+
if (warming === next)
|
|
58
|
+
return;
|
|
59
|
+
warming = next;
|
|
60
|
+
options.onWarmingChange?.(next, info());
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
while (true) {
|
|
64
|
+
attempt++;
|
|
65
|
+
const resp = await fetchImpl(input, init);
|
|
66
|
+
if (!isWarmingResponse(resp)) {
|
|
67
|
+
setWarming(false);
|
|
68
|
+
return resp;
|
|
69
|
+
}
|
|
70
|
+
setWarming(true);
|
|
71
|
+
const bodyNotReplayable = typeof ReadableStream !== "undefined" &&
|
|
72
|
+
init?.body instanceof ReadableStream;
|
|
73
|
+
const elapsed = Date.now() - startedAt;
|
|
74
|
+
if (bodyNotReplayable || elapsed + retryDelayMs > maxWaitMs) {
|
|
75
|
+
return resp;
|
|
76
|
+
}
|
|
77
|
+
await sleep(retryDelayMs);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
// Covers thrown fetch errors and the give-up paths above: never
|
|
82
|
+
// leave the overlay stuck on.
|
|
83
|
+
setWarming(false);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Fetch the gateway's boot status from `GET /1tube/warmup`. Useful for a
|
|
89
|
+
* progress bar inside the overlay ("12/53 functions ready").
|
|
90
|
+
*
|
|
91
|
+
* @param baseUrl Gateway origin, e.g. `http://localhost:3100`.
|
|
92
|
+
*/
|
|
93
|
+
export async function fetchWarmupStatus(baseUrl, init) {
|
|
94
|
+
const fetchImpl = init?.fetch ?? fetch;
|
|
95
|
+
const resp = await fetchImpl(`${baseUrl.replace(/\/+$/, "")}/1tube/warmup`, init?.signal ? { signal: init.signal } : undefined);
|
|
96
|
+
if (!resp.ok) {
|
|
97
|
+
throw new Error(`warmup status request failed: HTTP ${resp.status}`);
|
|
98
|
+
}
|
|
99
|
+
return await resp.json();
|
|
100
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
CREATE TABLE `invocations` (
|
|
2
|
+
`id` text PRIMARY KEY,
|
|
3
|
+
`ts_ms` integer NOT NULL,
|
|
4
|
+
`function_name` text NOT NULL,
|
|
5
|
+
`method` text NOT NULL,
|
|
6
|
+
`path` text NOT NULL,
|
|
7
|
+
`status` integer NOT NULL,
|
|
8
|
+
`duration_ms` integer NOT NULL,
|
|
9
|
+
`user_id` text,
|
|
10
|
+
`backend` text NOT NULL,
|
|
11
|
+
`error_kind` text,
|
|
12
|
+
`error_message` text,
|
|
13
|
+
`error_stack` text
|
|
14
|
+
);
|
|
15
|
+
--> statement-breakpoint
|
|
16
|
+
CREATE TABLE `logs` (
|
|
17
|
+
`id` integer PRIMARY KEY AUTOINCREMENT,
|
|
18
|
+
`invocation_id` text,
|
|
19
|
+
`ts_ms` integer NOT NULL,
|
|
20
|
+
`level` text NOT NULL,
|
|
21
|
+
`function_name` text,
|
|
22
|
+
`source` text DEFAULT 'function' NOT NULL,
|
|
23
|
+
`message` text NOT NULL
|
|
24
|
+
);
|
|
25
|
+
--> statement-breakpoint
|
|
26
|
+
CREATE INDEX `idx_invocations_ts` ON `invocations` (`ts_ms`);--> statement-breakpoint
|
|
27
|
+
CREATE INDEX `idx_invocations_fn_ts` ON `invocations` (`function_name`,`ts_ms`);--> statement-breakpoint
|
|
28
|
+
CREATE INDEX `idx_invocations_status_ts` ON `invocations` (`status`,`ts_ms`);--> statement-breakpoint
|
|
29
|
+
CREATE INDEX `idx_logs_invocation` ON `logs` (`invocation_id`);--> statement-breakpoint
|
|
30
|
+
CREATE INDEX `idx_logs_ts` ON `logs` (`ts_ms`);
|