@ultimat3/cli 23.0.0 → 25.0.0
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.md +28 -13
- package/README.md +32 -7
- package/package.json +29 -29
- package/src/admin-catalog-keys.ts +2 -2
- package/src/affected.ts +30 -36
- package/src/api-registration.ts +89 -0
- package/src/app-artifacts.ts +1 -1
- package/src/app-auth.ts +4 -27
- package/src/app-boundaries.ts +17 -4
- package/src/app-config-load.ts +51 -0
- package/src/app-env.ts +26 -25
- package/src/app-load.ts +38 -14
- package/src/app-manifest.ts +4 -10
- package/src/app-mcp.ts +31 -46
- package/src/app-name.ts +52 -0
- package/src/app-permissions-borrowed.ts +123 -0
- package/src/app-permissions-site.ts +48 -0
- package/src/app-permissions.ts +7 -17
- package/src/app-root.ts +9 -5
- package/src/bin.ts +3 -7
- package/src/browser-launcher.ts +4 -2
- package/src/budgets-charged.ts +60 -0
- package/src/budgets-scan.ts +76 -0
- package/src/budgets-unmeasured.ts +91 -0
- package/src/budgets.ts +117 -154
- package/src/cdp-shot-driver.ts +20 -1
- package/src/cdp-shot-watch.ts +35 -31
- package/src/ci-log.ts +0 -0
- package/src/cmd-auth-spec.ts +16 -0
- package/src/cmd-auth.ts +65 -0
- package/src/cmd-build-spec.ts +7 -1
- package/src/cmd-build.ts +151 -22
- package/src/cmd-db-backfill.ts +26 -13
- package/src/cmd-db-branch.ts +2 -2
- package/src/cmd-db.ts +10 -6
- package/src/cmd-deploy-compose.ts +94 -0
- package/src/cmd-deploy-drain.ts +48 -0
- package/src/cmd-deploy-helm.ts +7 -6
- package/src/cmd-deploy-start-first.ts +178 -0
- package/src/cmd-deploy.ts +158 -36
- package/src/cmd-dev.ts +18 -356
- package/src/cmd-doctor-spec.ts +2 -9
- package/src/cmd-doctor.ts +57 -80
- package/src/cmd-env.ts +55 -16
- package/src/cmd-generate.ts +89 -32
- package/src/cmd-help.ts +19 -6
- package/src/cmd-i18n.ts +21 -17
- package/src/cmd-jobs-spec.ts +7 -22
- package/src/cmd-jobs.ts +33 -140
- package/src/cmd-manifest-spec.ts +1 -1
- package/src/cmd-manifest.ts +6 -25
- package/src/cmd-mcp.ts +71 -22
- package/src/cmd-new.ts +36 -8
- package/src/cmd-planned.ts +24 -8
- package/src/cmd-routes.ts +15 -5
- package/src/cmd-secrets.ts +47 -6
- package/src/cmd-shot-matrix.ts +22 -7
- package/src/cmd-shot-spec.ts +10 -2
- package/src/cmd-shot.ts +46 -41
- package/src/cmd-tasks.ts +2 -2
- package/src/cmd-test-spec.ts +1 -7
- package/src/cmd-test.ts +14 -30
- package/src/cmd-verify.ts +4 -4
- package/src/coverage-lcov.ts +4 -2
- package/src/db-backfill.ts +2 -2
- package/src/db-replay-engine.ts +4 -10
- package/src/db-subscribes.ts +27 -16
- package/src/dev-boot.ts +290 -0
- package/src/dev-child-watch.ts +63 -12
- package/src/dev-dashboard-guard.ts +76 -0
- package/src/dev-dashboard.ts +56 -30
- package/src/dev-lock.ts +100 -31
- package/src/dev-rebuild.ts +80 -0
- package/src/dev-restart-retire.ts +46 -0
- package/src/dev-route-table.ts +30 -8
- package/src/dev-supervisor.ts +114 -22
- package/src/dev-traces.ts +53 -20
- package/src/dispatch.ts +17 -13
- package/src/doctor-app-url.ts +39 -0
- package/src/doctor-auth.ts +95 -0
- package/src/doctor-offline.ts +6 -5
- package/src/document-options.ts +7 -0
- package/src/drift-replica-identity.ts +42 -0
- package/src/drift.ts +29 -6
- package/src/error-catalog.ts +6 -9
- package/src/error-codes.ts +29 -3
- package/src/error-page-csp.ts +14 -15
- package/src/error-unthrown.ts +29 -11
- package/src/errors.ts +54 -83
- package/src/exec.ts +8 -0
- package/src/favicon.ts +2 -2
- package/src/fix-command.ts +9 -1
- package/src/framework-env.ts +148 -0
- package/src/framework-schema-apply.ts +78 -0
- package/src/framework-schema-stamp.ts +96 -0
- package/src/framework-schema.ts +25 -3
- package/src/generate-catalog-locales.ts +79 -0
- package/src/generate-files.ts +4 -3
- package/src/generate-kinds.ts +18 -0
- package/src/generate-plural.ts +50 -0
- package/src/generate-shadow.ts +95 -0
- package/src/generate-write.ts +47 -13
- package/src/gh-target.ts +12 -7
- package/src/hold.ts +56 -1
- package/src/i18n-audit.ts +25 -9
- package/src/i18n-index.ts +5 -6
- package/src/i18n-registration.ts +7 -9
- package/src/import-scan.ts +28 -3
- package/src/index.ts +2 -6
- package/src/island-bundle.ts +57 -79
- package/src/island-capture.ts +1 -1
- package/src/island-duplicate-refusal.ts +57 -0
- package/src/island-duplicates.ts +221 -0
- package/src/island-entry-missing.ts +64 -0
- package/src/island-harness-route.ts +20 -4
- package/src/island-harness-script.ts +23 -2
- package/src/island-identity.ts +17 -14
- package/src/island-package-dedupe.ts +150 -0
- package/src/island-realtime.ts +36 -16
- package/src/island-routes.ts +2 -2
- package/src/island-runtime.ts +241 -0
- package/src/island-sources.ts +105 -0
- package/src/island-states-load.ts +6 -0
- package/src/island-store.ts +90 -6
- package/src/jobs-json.ts +0 -26
- package/src/jobs-report.ts +36 -20
- package/src/local-cli-handoff.ts +90 -0
- package/src/loopback-host.ts +13 -0
- package/src/manifest-staleness.ts +62 -0
- package/src/mcp-errors.ts +20 -4
- package/src/mcp-host.ts +22 -14
- package/src/mcp-test-run.ts +38 -0
- package/src/mcp-ui-inspect.ts +3 -2
- package/src/mcp-ui-interact.ts +3 -2
- package/src/mcp-ui.ts +8 -7
- package/src/measure-scope.ts +15 -10
- package/src/messages.ts +29 -16
- package/src/metrics-endpoint.ts +28 -3
- package/src/migration-extensions.ts +1 -1
- package/src/otlp-export.ts +2 -2
- package/src/output.ts +24 -0
- package/src/page-navigation.ts +7 -23
- package/src/page-speculation.ts +19 -31
- package/src/parse.ts +0 -8
- package/src/permission-grants.ts +3 -3
- package/src/posix-path.ts +30 -0
- package/src/prerender-locales.ts +6 -4
- package/src/prerender-out.ts +34 -7
- package/src/prerender-report.ts +58 -0
- package/src/prerender-served.ts +52 -0
- package/src/prerender.ts +71 -103
- package/src/pwa-artifacts.ts +33 -94
- package/src/pwa-manifest-locales.ts +9 -9
- package/src/registry.ts +2 -0
- package/src/retry-memo.ts +1 -1
- package/src/revalidated-response.ts +4 -3
- package/src/role-realtime.ts +7 -1
- package/src/role-replicator.ts +124 -29
- package/src/role-start.ts +69 -85
- package/src/role-sync.ts +119 -25
- package/src/root-env.ts +157 -28
- package/src/route-islands.ts +69 -0
- package/src/runtime-admin.ts +4 -1
- package/src/runtime-assets.ts +26 -9
- package/src/runtime-bindings.ts +27 -7
- package/src/runtime-cache.ts +46 -55
- package/src/runtime-hooks.ts +2 -1
- package/src/runtime-idempotency-scope.ts +23 -0
- package/src/runtime-isr-outcome.ts +100 -0
- package/src/runtime-isr.ts +36 -0
- package/src/runtime-jobs.ts +76 -41
- package/src/runtime-live-feed.ts +1 -1
- package/src/runtime-mail.ts +26 -0
- package/src/runtime-mfa-warning.ts +29 -0
- package/src/runtime-notify-retention.ts +10 -46
- package/src/runtime-overrides.ts +4 -4
- package/src/runtime-purge.ts +38 -16
- package/src/runtime-queue.ts +43 -22
- package/src/runtime-realtime.ts +11 -45
- package/src/runtime-render.ts +136 -114
- package/src/runtime-replica.ts +3 -8
- package/src/runtime-services.ts +49 -26
- package/src/runtime-storage-upload.ts +2 -2
- package/src/runtime-storage.ts +13 -8
- package/src/schema-diff.ts +16 -0
- package/src/schema-drift.ts +45 -15
- package/src/secrets-rotation.ts +8 -9
- package/src/seo-routes.ts +80 -9
- package/src/serve-boot.ts +51 -12
- package/src/serve-drain.ts +11 -33
- package/src/serve-env.ts +9 -6
- package/src/serve-retire.ts +95 -0
- package/src/serve-web.ts +31 -15
- package/src/serve.ts +27 -10
- package/src/server-barrels.ts +6 -3
- package/src/shot-browser.ts +1 -1
- package/src/shot-locale.ts +6 -33
- package/src/shot-settle.ts +3 -3
- package/src/shot-theme.ts +1 -1
- package/src/shot-viewport.ts +152 -0
- package/src/signal-shred.ts +26 -12
- package/src/site-asset-routes.ts +2 -2
- package/src/site-assets.ts +3 -5
- package/src/site-config.ts +9 -48
- package/src/site-seo.ts +7 -5
- package/src/sitemap-lastmod.ts +5 -0
- package/src/solid-loader.ts +12 -3
- package/src/source-files.ts +19 -3
- package/src/static-report.ts +5 -0
- package/src/stored-object-headers.ts +43 -0
- package/src/style-routes.ts +2 -2
- package/src/sw-artifacts.ts +8 -8
- package/src/sw-precache-plan.ts +34 -4
- package/src/sync-authenticator.ts +3 -3
- package/src/sync-url.ts +3 -3
- package/src/tasks-facts.ts +3 -29
- package/src/templates/action.ts +7 -4
- package/src/templates/backfill.ts +4 -4
- package/src/templates/entity.ts +2 -2
- package/src/templates/github/ci.yml.ts +28 -18
- package/src/templates/github/image.yml.ts +92 -0
- package/src/templates/github/runner.ts +18 -0
- package/src/templates/guard-bare-error.ts +16 -2
- package/src/templates/guard-unzoned-date.ts +56 -6
- package/src/templates/index.ts +1 -1
- package/src/templates/job-body-test.ts +2 -2
- package/src/templates/job.ts +6 -6
- package/src/templates/naming.ts +28 -0
- package/src/templates/query.ts +9 -8
- package/src/templates/resource-page.ts +9 -7
- package/src/templates/resource-service.ts +2 -2
- package/src/templates/route.ts +26 -14
- package/src/templates/scaffold-api.ts +2 -3
- package/src/templates/scaffold-app.ts +8 -3
- package/src/templates/scaffold-auth.ts +205 -70
- package/src/templates/scaffold-bin.ts +122 -0
- package/src/templates/scaffold-claude-commands.ts +11 -11
- package/src/templates/scaffold-container-compose.ts +10 -1
- package/src/templates/scaffold-container.ts +16 -8
- package/src/templates/scaffold-dashboard-bare.ts +8 -4
- package/src/templates/scaffold-dashboard-example-tests.ts +208 -0
- package/src/templates/scaffold-dashboard-example.ts +112 -172
- package/src/templates/scaffold-dashboard-shared.ts +62 -1
- package/src/templates/scaffold-dashboard.ts +9 -5
- package/src/templates/scaffold-db-client.ts +4 -2
- package/src/templates/scaffold-db-package.ts +2 -2
- package/src/templates/scaffold-docs.ts +20 -77
- package/src/templates/scaffold-domain-package.ts +0 -4
- package/src/templates/scaffold-entries.ts +4 -3
- package/src/templates/scaffold-env.ts +6 -3
- package/src/templates/scaffold-gitattributes.ts +59 -0
- package/src/templates/scaffold-helm-fences.ts +76 -0
- package/src/templates/scaffold-helm-templates.ts +100 -19
- package/src/templates/scaffold-helm.ts +62 -5
- package/src/templates/scaffold-http.ts +1 -1
- package/src/templates/scaffold-i18n.ts +15 -2
- package/src/templates/scaffold-icon.ts +2 -2
- package/src/templates/scaffold-repo.ts +27 -16
- package/src/templates/scaffold-shell.ts +55 -37
- package/src/test-counts.ts +2 -6
- package/src/test-ignores.ts +25 -0
- package/src/test-only-shipped.ts +193 -0
- package/src/test-passes.ts +0 -15
- package/src/test-select.ts +7 -15
- package/src/test-shards.ts +40 -95
- package/src/test-workers.ts +26 -4
- package/src/theme-boot.ts +8 -33
- package/src/theme-brand.ts +66 -0
- package/src/trusted-hops-error.ts +17 -0
- package/src/trusted-hops.ts +49 -0
- package/src/unscanned-admin.ts +2 -2
- package/src/unthrown-codes.ts +26 -0
- package/src/verify-checks.ts +29 -48
- package/src/verify-coverage-run.ts +3 -2
- package/src/verify-deadline.ts +207 -33
- package/src/verify-merge.ts +108 -43
- package/src/verify-part-step.ts +98 -0
- package/src/verify-role-load-probe.ts +12 -2
- package/src/verify-role-load.ts +9 -4
- package/src/verify-run.ts +32 -8
- package/src/verify-shard.ts +13 -2
- package/src/verify-stalled.ts +152 -0
- package/src/verify-step.ts +2 -0
- package/src/verify-test-run.ts +3 -2
- package/src/verify-tests.ts +5 -3
- package/src/worker-bundle.ts +2 -2
- package/src/workspace-checks.ts +53 -7
- package/src/workspace-graph.ts +3 -2
- package/src/write-line.ts +82 -7
- package/src/browser-launcher-fake-html.ts +0 -80
- package/src/browser-launcher-fake.ts +0 -168
- package/src/build-errors.ts +0 -54
- package/src/jobs-drain.ts +0 -163
- package/src/scaffold-typecheck.ts +0 -227
- package/src/thrown-by.ts +0 -24
- package/src/verify-errors.ts +0 -58
package/src/runtime-render.ts
CHANGED
|
@@ -1,46 +1,31 @@
|
|
|
1
|
-
// Projecting the route table onto HTTP routes
|
|
1
|
+
// Projecting the route table onto HTTP routes both boots serve. Every mode goes through
|
|
2
2
|
// `@ultimat3/render`'s own function for that mode — the CLI picks the mode and supplies the
|
|
3
|
-
// document
|
|
4
|
-
//
|
|
5
|
-
// The document is head + the route's own component, rendered by `@ultimat3/render`'s server JSX
|
|
6
|
-
// writer, with the surface's compiled CSS LINKED — one content-hashed file per surface
|
|
7
|
-
// (`style-bundle.ts`), served `immutable`.
|
|
8
|
-
//
|
|
9
|
-
// It was inlined until 2026-09-06, on the argument that a `site/` page is a 0kb-JS artifact a CDN
|
|
10
|
-
// serves as one file and a link would add a round trip. The round trip is real and it is paid
|
|
11
|
-
// once: measured against ai-maxxing, every `app/` document carried the SAME 156,738-byte `<style>`
|
|
12
|
-
// block — 92% of the dashboard document — inside a response the pipeline sends
|
|
13
|
-
// `Cache-Control: private, no-store`, so the trip that argument saved was re-paid in full on every
|
|
14
|
-
// navigation, with a re-parse on top. The static export writes the file (`writeStyles`), so the
|
|
15
|
-
// "second file" cost is one `Bun.write`.
|
|
3
|
+
// document (head + the route's component, the surface's CSS LINKED, `style-bundle.ts`), it never
|
|
4
|
+
// decides what a mode means or what headers it earns.
|
|
16
5
|
|
|
17
|
-
// why: Bun ships no path API; an island's file is its route file's directory joined to its `src`.
|
|
18
|
-
import { posix } from 'node:path';
|
|
19
6
|
import { actionPathStyle } from '@ultimat3/action';
|
|
20
7
|
import { clientScopeOf } from '@ultimat3/auth';
|
|
21
|
-
import type { Ctx } from '@ultimat3/core';
|
|
22
|
-
import { CLIENT_SCOPE_HEADER } from '@ultimat3/core';
|
|
8
|
+
import type { Actor, Ctx } from '@ultimat3/core';
|
|
9
|
+
import { CLIENT_SCOPE_HEADER, singleFlight } from '@ultimat3/core';
|
|
23
10
|
import type {
|
|
24
11
|
RouteMeta as HttpRouteMeta,
|
|
25
12
|
RedirectIntent,
|
|
13
|
+
RequestContext,
|
|
26
14
|
Route,
|
|
27
15
|
RouteParams,
|
|
16
|
+
UltimateRequest,
|
|
28
17
|
} from '@ultimat3/http';
|
|
29
18
|
import { asCtx, html, NO_STORE, redirect, stream, takeRedirect } from '@ultimat3/http';
|
|
30
|
-
import { currentLocale
|
|
19
|
+
import { currentLocale } from '@ultimat3/i18n';
|
|
31
20
|
import type { IslandCollector, RenderResult, RouteData, RouteEntry } from '@ultimat3/render';
|
|
32
21
|
import {
|
|
33
|
-
clientBootTags,
|
|
34
22
|
clientPathStyleTags,
|
|
35
23
|
clientPersistTags,
|
|
36
24
|
clientScopeTag,
|
|
37
25
|
clientSyncTags,
|
|
38
|
-
createIslandCollector,
|
|
39
26
|
documentCarriesScope,
|
|
40
27
|
headFromMeta,
|
|
41
28
|
hydrateRuntime,
|
|
42
|
-
islandModuleId,
|
|
43
|
-
islandModuleIds,
|
|
44
29
|
metaContextFor,
|
|
45
30
|
renderHead,
|
|
46
31
|
routeDataFor,
|
|
@@ -52,8 +37,6 @@ import {
|
|
|
52
37
|
import type { IsrController } from '@ultimat3/render/server';
|
|
53
38
|
import {
|
|
54
39
|
contentHash,
|
|
55
|
-
createIsrController,
|
|
56
|
-
isrKey,
|
|
57
40
|
ROOT_ELEMENT_ID,
|
|
58
41
|
renderComponent,
|
|
59
42
|
renderSsr,
|
|
@@ -62,13 +45,15 @@ import {
|
|
|
62
45
|
streamResult,
|
|
63
46
|
} from '@ultimat3/render/server';
|
|
64
47
|
import type { DocumentOptions } from './document-options';
|
|
65
|
-
import { realtimeIslandFiles } from './island-realtime';
|
|
66
48
|
import {
|
|
67
49
|
type NavigationDocumentHead,
|
|
68
50
|
navigationMetaOf,
|
|
69
51
|
navigationTagsOf,
|
|
70
52
|
principalRelocation,
|
|
71
53
|
} from './page-navigation';
|
|
54
|
+
import { bootScript, collectorFor } from './route-islands';
|
|
55
|
+
import { attachedIsr } from './runtime-isr';
|
|
56
|
+
import { isrOutcome } from './runtime-isr-outcome';
|
|
72
57
|
import type { StaticResult } from './static-document';
|
|
73
58
|
import { createStaticMemo, staticMemoKey, staticResponse } from './static-document';
|
|
74
59
|
import { styleBundle } from './style-bundle';
|
|
@@ -77,7 +62,11 @@ export type { DocumentOptions, IslandResolver } from './document-options';
|
|
|
77
62
|
|
|
78
63
|
export interface DevRenderOptions extends DocumentOptions {
|
|
79
64
|
readonly buildId: string;
|
|
80
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* The boot's own controller, attached and released by it (`runtime-isr.ts`). Omitted, one is
|
|
67
|
+
* built AND attached here, never released — a caller with no stop list (a contract test) still
|
|
68
|
+
* gets pages a tag bust reaches; the revalidator slot is the latest attach's.
|
|
69
|
+
*/
|
|
81
70
|
readonly isr?: IsrController;
|
|
82
71
|
/**
|
|
83
72
|
* Keep each `static` document after its first render (`static-document.ts`). The container sets
|
|
@@ -124,9 +113,11 @@ const headFor = async (
|
|
|
124
113
|
seoRenderers({
|
|
125
114
|
path: url.pathname,
|
|
126
115
|
baseUrl: options.origin ?? url.origin,
|
|
116
|
+
// The default is the cluster's own head (`alternates` is default-first by contract), so
|
|
117
|
+
// `x-default` comes from the list the hreflang tags do — never a second locale read.
|
|
127
118
|
localization: {
|
|
128
119
|
locale: meta.locale,
|
|
129
|
-
defaultLocale:
|
|
120
|
+
defaultLocale: meta.alternates[0]?.locale ?? meta.locale,
|
|
130
121
|
alternates: meta.alternates,
|
|
131
122
|
},
|
|
132
123
|
}),
|
|
@@ -156,7 +147,8 @@ const headFor = async (
|
|
|
156
147
|
|
|
157
148
|
/**
|
|
158
149
|
* `<link rel="stylesheet">` for the surface's own stylesheets, or nothing at all when the surface
|
|
159
|
-
* imports none.
|
|
150
|
+
* imports none. Inlined until 2026-09-06: every `app/` document of ai-maxxing then carried the same
|
|
151
|
+
* 156,738-byte `<style>` (92% of it) under `no-store`, re-paid on every navigation.
|
|
160
152
|
*
|
|
161
153
|
* In `<head>`, which is what keeps this a swap and not a regression: a `<link rel="stylesheet">`
|
|
162
154
|
* there is render-blocking in every browser, exactly as the inline block was, so there is no
|
|
@@ -174,6 +166,10 @@ const styleTag = (entry: RouteEntry): string => {
|
|
|
174
166
|
return href === undefined ? '' : `<link rel="stylesheet" href="${href}">`;
|
|
175
167
|
};
|
|
176
168
|
|
|
169
|
+
/** The surface stylesheet, then the app's brand — the order that lets the brand win the cascade. */
|
|
170
|
+
const stylesFor = (entry: RouteEntry, options: DocumentOptions): string =>
|
|
171
|
+
styleTag(entry) + (options.brandHead ?? '');
|
|
172
|
+
|
|
177
173
|
/**
|
|
178
174
|
* The route's rendered body, inside the hydration root. Every mode goes through here — an empty
|
|
179
175
|
* root is what a module exporting no component renders, and nothing else: `spa` was the one mode
|
|
@@ -205,46 +201,6 @@ export async function routeBody(
|
|
|
205
201
|
return `<div id="${ROOT_ELEMENT_ID}">${html}</div>`;
|
|
206
202
|
}
|
|
207
203
|
|
|
208
|
-
/**
|
|
209
|
-
* One collector per RENDER, never module-global: two requests render different params, and a
|
|
210
|
-
* shared collector would bill one page for the other's islands. `hydrate` comes off the route, so
|
|
211
|
-
* an island never declares its own timing, and `resolve` is the build's — identity when nothing
|
|
212
|
-
* built any, which fails at the first island by name rather than emitting an unusable entry.
|
|
213
|
-
*/
|
|
214
|
-
/**
|
|
215
|
-
* Realtime's page boot, as one deferred script — or nothing. Two conditions, both exact: the
|
|
216
|
-
* document carries a principal scope (restoring persisted records and replaying queued writes are
|
|
217
|
-
* per principal; a shareable document has neither), AND one of the islands this render emitted
|
|
218
|
-
* reaches `@ultimat3/realtime` (a page whose islands never touch a record has nothing to restore
|
|
219
|
-
* into and no write to replay). After the body, because which islands rendered is a fact the walk
|
|
220
|
-
* just recorded; still before the hydration runtime, so it runs first among the deferred scripts.
|
|
221
|
-
*/
|
|
222
|
-
function bootScript(
|
|
223
|
-
entry: RouteEntry,
|
|
224
|
-
islands: IslandCollector,
|
|
225
|
-
options: DocumentOptions,
|
|
226
|
-
scope: string | undefined,
|
|
227
|
-
): string {
|
|
228
|
-
if (scope === undefined || options.sync === undefined) return '';
|
|
229
|
-
const rendered = new Set(islandModuleIds(islands.directives));
|
|
230
|
-
if (rendered.size === 0) return '';
|
|
231
|
-
// An island's module id is derived from its `src`, written relative to the page that renders it:
|
|
232
|
-
// each realtime island file, spelled from THIS page, is the id its directive would carry.
|
|
233
|
-
const pageDir = posix.dirname(entry.file);
|
|
234
|
-
const reaches = [...realtimeIslandFiles()].some((file) => {
|
|
235
|
-
const src = posix.relative(pageDir, file);
|
|
236
|
-
return rendered.has(islandModuleId(src.startsWith('.') ? src : `./${src}`));
|
|
237
|
-
});
|
|
238
|
-
return reaches ? renderHead(clientBootTags(options.sync)) : '';
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const collectorFor = (entry: RouteEntry, options: DocumentOptions): IslandCollector =>
|
|
242
|
-
createIslandCollector({
|
|
243
|
-
file: entry.file,
|
|
244
|
-
hydrate: entry.config.hydrate,
|
|
245
|
-
...(options.resolveIsland === undefined ? {} : { resolve: options.resolveIsland(entry.file) }),
|
|
246
|
-
});
|
|
247
|
-
|
|
248
204
|
/**
|
|
249
205
|
* Head + body for one route render. Exported because the build's prerenderer must emit the same
|
|
250
206
|
* document `x dev` serves — two document builders is how a page that works in dev ships broken.
|
|
@@ -257,6 +213,42 @@ export async function routeDocument(
|
|
|
257
213
|
return documentFrom(entry, ctx, await routeDataFor(entry.config, ctx), options);
|
|
258
214
|
}
|
|
259
215
|
|
|
216
|
+
/**
|
|
217
|
+
* The document a request by `actor` is SERVED: `routeDocument` plus the scope the response would
|
|
218
|
+
* carry, which is what earns the page boot. The build weighs an app/ page through this, because a
|
|
219
|
+
* budget is a promise about what that page's browser downloads — and an unscoped render of a
|
|
220
|
+
* scoped page weighed a runtime chunk it never fetches and no boot it always does (#505).
|
|
221
|
+
*/
|
|
222
|
+
export async function servedDocument(
|
|
223
|
+
entry: RouteEntry,
|
|
224
|
+
ctx: DevRouteData,
|
|
225
|
+
actor: Actor,
|
|
226
|
+
options: DocumentOptions & { readonly buildId: string },
|
|
227
|
+
): Promise<string> {
|
|
228
|
+
const data = await routeDataFor(entry.config, ctx);
|
|
229
|
+
return documentFrom(entry, ctx, data, options, documentScope(entry, options.buildId, actor));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Whether, and as whom, a mode's response is scoped — ONE decision for the request path and for
|
|
234
|
+
* `servedDocument`. A stream is always `private, no-store`; an ssr page carries the scope exactly
|
|
235
|
+
* when the headers `renderSsr` sends are private (an ungated page is `public, s-maxage`, and a CDN
|
|
236
|
+
* may hand it to anyone); a static or isr document is shareable by construction.
|
|
237
|
+
*/
|
|
238
|
+
function documentScope(entry: RouteEntry, buildId: string, actor: Actor): string | undefined {
|
|
239
|
+
switch (entry.config.render) {
|
|
240
|
+
case 'static':
|
|
241
|
+
case 'isr':
|
|
242
|
+
return undefined;
|
|
243
|
+
case 'stream':
|
|
244
|
+
return clientScopeOf(actor);
|
|
245
|
+
default:
|
|
246
|
+
return documentCarriesScope(ssrHeaders(entry, { buildId }))
|
|
247
|
+
? clientScopeOf(actor)
|
|
248
|
+
: undefined;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
260
252
|
/**
|
|
261
253
|
* The document from data ALREADY resolved. Split from `routeDocument` so one request resolves
|
|
262
254
|
* `load` exactly once: `stream` renders head and body separately, and resolving in each would let
|
|
@@ -273,23 +265,23 @@ async function documentFrom(
|
|
|
273
265
|
options: DocumentOptions,
|
|
274
266
|
scope?: string,
|
|
275
267
|
): Promise<string> {
|
|
276
|
-
const islands = collectorFor(entry, options);
|
|
268
|
+
const islands = collectorFor(entry, options, scope);
|
|
277
269
|
const [head, body] = await Promise.all([
|
|
278
270
|
headFor(entry, ctx, data, options, scope),
|
|
279
271
|
routeBody(entry, ctx, data, islands),
|
|
280
272
|
]);
|
|
281
273
|
return (
|
|
282
|
-
`<!doctype html><html lang="${lang()}"><head>${head}${
|
|
274
|
+
`<!doctype html><html lang="${lang()}"><head>${head}${stylesFor(entry, options)}</head>` +
|
|
283
275
|
`<body>${body}${bootScript(entry, islands, options, scope)}${hydrateRuntime(islands.directives)}</body></html>`
|
|
284
276
|
);
|
|
285
277
|
}
|
|
286
278
|
|
|
279
|
+
/** Every mode but `isr`, which `renderIsr` answers: its `load` runs only when the store misses. */
|
|
287
280
|
async function resultFor(
|
|
288
281
|
entry: RouteEntry,
|
|
289
282
|
request: DevRouteData,
|
|
290
283
|
data: RouteData,
|
|
291
284
|
options: DevRenderOptions,
|
|
292
|
-
isr: IsrController,
|
|
293
285
|
ctx: Ctx,
|
|
294
286
|
): Promise<RenderResult> {
|
|
295
287
|
const url = new URL(request.url);
|
|
@@ -303,57 +295,40 @@ async function resultFor(
|
|
|
303
295
|
const body = await documentFrom(entry, request, data, options);
|
|
304
296
|
return { status, headers: staticHeaders(contentHash(body), options.buildId), body };
|
|
305
297
|
}
|
|
306
|
-
case 'isr': {
|
|
307
|
-
// `isrKey(url, locale)`, never `url.pathname`: the query is part of what was rendered — this
|
|
308
|
-
// route's own `meta` reads `data.url` — so two URLs differing only in their query are two
|
|
309
|
-
// documents. Keyed on the pathname alone, the first render answered every later query
|
|
310
|
-
// string (#171). Render owns the derivation so no second caller can invent another.
|
|
311
|
-
// The locale is the second dimension and it is `ctx.locale`, the answer the `locale` stage
|
|
312
|
-
// already negotiated for THIS request — never `currentLocale()`, which would read the same
|
|
313
|
-
// value through an ambient store the key does not need.
|
|
314
|
-
// `{ html, status }`, never the bare string: the entry stores the status beside the HTML
|
|
315
|
-
// and serves it on every hit, so a 404 under `isr` is a 404 for its whole TTL.
|
|
316
|
-
const served = await isr.serve(isrKey(url, ctx.locale), async () => ({
|
|
317
|
-
html: await documentFrom(entry, request, data, options),
|
|
318
|
-
status,
|
|
319
|
-
}));
|
|
320
|
-
return served.result;
|
|
321
|
-
}
|
|
322
298
|
case 'stream': {
|
|
323
299
|
// The shell IS the component: nothing can yet mark a subtree as a hole. Solid's `Suspense`
|
|
324
300
|
// is not the missing piece and never will be here — it calls `getContextId()`, which throws
|
|
325
301
|
// outside a Solid renderer, and this package's JSX factory is inert by design. A hole marker
|
|
326
302
|
// has to be the framework's own. Until it exists the first flush carries the whole body —
|
|
327
303
|
// correct output, no streaming benefit.
|
|
328
|
-
|
|
304
|
+
// A stream is always `private, no-store` (`streamResult`), so it always carries the scope.
|
|
305
|
+
const scope = documentScope(entry, options.buildId, ctx.actor);
|
|
306
|
+
const islands = collectorFor(entry, options, scope);
|
|
329
307
|
const [head, shell] = await Promise.all([
|
|
330
|
-
|
|
331
|
-
headFor(entry, request, data, options, clientScopeOf(ctx.actor)),
|
|
308
|
+
headFor(entry, request, data, options, scope),
|
|
332
309
|
routeBody(entry, request, data, islands),
|
|
333
310
|
]);
|
|
334
311
|
return withScope(
|
|
335
312
|
streamResult(
|
|
336
313
|
{
|
|
337
|
-
head: `<!doctype html><html lang="${lang()}"><head>${head}${
|
|
314
|
+
head: `<!doctype html><html lang="${lang()}"><head>${head}${stylesFor(entry, options)}</head><body>`,
|
|
338
315
|
// The runtime rides the first flush, with the shell it boots. A later chunk would leave
|
|
339
316
|
// the window between flush one and the close with inert islands and no listeners on
|
|
340
317
|
// them — which is exactly the first-click-lost failure `interaction` replay exists for.
|
|
341
|
-
shell: `${shell}${bootScript(entry, islands, options,
|
|
318
|
+
shell: `${shell}${bootScript(entry, islands, options, scope)}${hydrateRuntime(islands.directives)}`,
|
|
342
319
|
holes: [],
|
|
343
320
|
},
|
|
344
321
|
{ buildId: options.buildId },
|
|
345
322
|
status,
|
|
346
323
|
),
|
|
347
|
-
|
|
324
|
+
scope,
|
|
348
325
|
);
|
|
349
326
|
}
|
|
350
327
|
default: {
|
|
351
328
|
// Asked of the headers `renderSsr` is about to send: a gated page is private and carries the
|
|
352
329
|
// scope; an ungated one is `public, s-maxage` and a CDN may hand it to anyone, so it carries
|
|
353
330
|
// none — absent, which core reads as "not rendered for anyone", never as anonymous.
|
|
354
|
-
const scope =
|
|
355
|
-
? clientScopeOf(ctx.actor)
|
|
356
|
-
: undefined;
|
|
331
|
+
const scope = documentScope(entry, options.buildId, ctx.actor);
|
|
357
332
|
return withScope(
|
|
358
333
|
await renderSsr(
|
|
359
334
|
{ entry, params: request.params, url, ctx },
|
|
@@ -429,8 +404,11 @@ const metaOf = (entry: RouteEntry, head?: NavigationDocumentHead): HttpRouteMeta
|
|
|
429
404
|
* only a test does — and then guard and page fall back together.
|
|
430
405
|
*/
|
|
431
406
|
export function appRoutes(options: DevRenderOptions): readonly Route[] {
|
|
432
|
-
const isr = options.isr ??
|
|
407
|
+
const isr = options.isr ?? attachedIsr({ buildId: options.buildId }).isr;
|
|
433
408
|
const memo = createStaticMemo();
|
|
409
|
+
// A cold static key is ONE render however many requests arrive during it: the memo alone was
|
|
410
|
+
// check-then-act, so a burst at boot ran `load` and the render once per request.
|
|
411
|
+
const firstRender = singleFlight();
|
|
434
412
|
return routeEntries().map((registered) => ({
|
|
435
413
|
method: 'GET' as const,
|
|
436
414
|
path: registered.path,
|
|
@@ -450,23 +428,67 @@ export function appRoutes(options: DevRenderOptions): readonly Route[] {
|
|
|
450
428
|
: undefined;
|
|
451
429
|
const kept = key === undefined ? undefined : memo.get(key);
|
|
452
430
|
if (kept !== undefined) return staticResponse(request, kept);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
return responseOf(result);
|
|
465
|
-
}
|
|
466
|
-
const document: StaticResult = { ...result, body: result.body };
|
|
467
|
-
if (key !== undefined) memo.set(key, document);
|
|
431
|
+
let led = false;
|
|
432
|
+
const render = async (): Promise<Rendered> => {
|
|
433
|
+
led = true;
|
|
434
|
+
const rendered = await renderEntry(entry, request, ctx, options, isr);
|
|
435
|
+
if (key !== undefined && rendered.document !== undefined) memo.set(key, rendered.document);
|
|
436
|
+
return rendered;
|
|
437
|
+
};
|
|
438
|
+
const shared = key === undefined ? await render() : await firstRender.run(key, render);
|
|
439
|
+
// Only a DOCUMENT is shared. A redirect `load` decided was its request's own, so a request
|
|
440
|
+
// that joined one renders for itself rather than answer with another request's `Location`.
|
|
441
|
+
const rendered = shared.document !== undefined || led ? shared : await render();
|
|
468
442
|
// The ETag was only ever a header: a matching `If-None-Match` is a 304, not the page again.
|
|
469
|
-
return
|
|
443
|
+
return rendered.document === undefined
|
|
444
|
+
? rendered.respond()
|
|
445
|
+
: staticResponse(request, rendered.document);
|
|
470
446
|
},
|
|
471
447
|
}));
|
|
472
448
|
}
|
|
449
|
+
|
|
450
|
+
/** A static document a joined request can share, or how to answer when there is none. */
|
|
451
|
+
interface Rendered {
|
|
452
|
+
readonly document?: StaticResult;
|
|
453
|
+
readonly respond: () => Response;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function renderEntry(
|
|
457
|
+
entry: RouteEntry,
|
|
458
|
+
request: UltimateRequest,
|
|
459
|
+
ctx: RequestContext,
|
|
460
|
+
options: DevRenderOptions,
|
|
461
|
+
isr: IsrController,
|
|
462
|
+
): Promise<Rendered> {
|
|
463
|
+
const data: DevRouteData = { url: request.url.href, params: ctx.params };
|
|
464
|
+
if (entry.config.render === 'isr') return renderIsr(entry, data, ctx, options, isr);
|
|
465
|
+
// ONCE per request, before the mode is chosen: every branch of `resultFor` reads this same
|
|
466
|
+
// object, so a route's `load` runs exactly once however its mode splits head from body.
|
|
467
|
+
const loaded = await routeDataFor(entry.config, data);
|
|
468
|
+
// `setRedirect()` inside `load` — the same slot an action's handler fills — answers here,
|
|
469
|
+
// before a document is rendered for a page the visitor is leaving. `withStatus` refuses a
|
|
470
|
+
// 3xx because a rendered document has no `Location`; this is the path that has one.
|
|
471
|
+
const to = takeRedirect(ctx);
|
|
472
|
+
if (to !== undefined) return { respond: () => loadRedirect(entry, to) };
|
|
473
|
+
const result = await resultFor(entry, data, loaded, options, asCtx(ctx));
|
|
474
|
+
if (entry.config.render !== 'static' || typeof result.body !== 'string') {
|
|
475
|
+
return { respond: () => responseOf(result) };
|
|
476
|
+
}
|
|
477
|
+
const document: StaticResult = { ...result, body: result.body };
|
|
478
|
+
return { document, respond: () => responseOf(document) };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** `isr`'s outcome (`runtime-isr-outcome.ts`) as this file's answer: a page, or the redirect. */
|
|
482
|
+
async function renderIsr(
|
|
483
|
+
entry: RouteEntry,
|
|
484
|
+
data: DevRouteData,
|
|
485
|
+
ctx: RequestContext,
|
|
486
|
+
options: DevRenderOptions,
|
|
487
|
+
isr: IsrController,
|
|
488
|
+
): Promise<Rendered> {
|
|
489
|
+
const document = (loaded: RouteData): Promise<string> =>
|
|
490
|
+
documentFrom(entry, data, loaded, options);
|
|
491
|
+
const outcome = await isrOutcome({ entry, data, ctx, isr, document });
|
|
492
|
+
if (outcome.kind === 'redirect') return { respond: () => loadRedirect(entry, outcome.to) };
|
|
493
|
+
return { respond: () => responseOf(outcome.result) };
|
|
494
|
+
}
|
package/src/runtime-replica.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// 1. `defaultClient()` is the one place db composes `replicatedClient(primary, replica)` from
|
|
8
8
|
// `DATABASE_REPLICA_URL`, and it runs only from `baseClient()` — "the client an app installed
|
|
9
9
|
// none for". Every process the framework boots installs one: `runtime-queue.ts` calls
|
|
10
|
-
// `setDbClient(
|
|
10
|
+
// `setDbClient(pgliteClient(…) | postgresClient({ url }))`, so `defaultClient()` was
|
|
11
11
|
// unreachable from `x dev`, from `apps/web/server.ts` and from every container role.
|
|
12
12
|
// 2. Routing needs an OPEN scope as well as a configured replica, and nothing opened one.
|
|
13
13
|
//
|
|
@@ -17,12 +17,7 @@
|
|
|
17
17
|
// what a database is.
|
|
18
18
|
|
|
19
19
|
import type { DbClient, PostgresClient } from '@ultimat3/db';
|
|
20
|
-
import {
|
|
21
|
-
createPostgresClient,
|
|
22
|
-
REPLICA_URL_ENV,
|
|
23
|
-
replicatedClient,
|
|
24
|
-
withReplicaReads,
|
|
25
|
-
} from '@ultimat3/db';
|
|
20
|
+
import { postgresClient, REPLICA_URL_ENV, replicatedClient, withReplicaReads } from '@ultimat3/db';
|
|
26
21
|
import type { Middleware } from '@ultimat3/http';
|
|
27
22
|
import type { ServiceBinding } from './runtime-bindings';
|
|
28
23
|
import type { RuntimeOverrides } from './runtime-overrides';
|
|
@@ -61,7 +56,7 @@ export function attachReplica(
|
|
|
61
56
|
replicaUrl: string | undefined,
|
|
62
57
|
): ReplicaAttachment {
|
|
63
58
|
if (replicaUrl === undefined) return { client: primary, replica: undefined };
|
|
64
|
-
const replica =
|
|
59
|
+
const replica = postgresClient({ url: replicaUrl, applicationName: 'ultimate-replica' });
|
|
65
60
|
return { client: replicatedClient(primary, replica), replica };
|
|
66
61
|
}
|
|
67
62
|
|
package/src/runtime-services.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// boot installs, only backed by embedded drivers.
|
|
5
5
|
|
|
6
6
|
import { mkdirSync } from 'node:fs';
|
|
7
|
-
import { dirname } from 'node:path';
|
|
8
7
|
import { configureAuthLimiters, postgresAuthLimiter, resetAuthLimiters } from '@ultimat3/auth';
|
|
9
8
|
import type { PurgeDriver } from '@ultimat3/cache';
|
|
10
9
|
import { isNoopPurgeDriver, selectPurgeDriver } from '@ultimat3/cache';
|
|
@@ -17,31 +16,32 @@ import {
|
|
|
17
16
|
systemClock,
|
|
18
17
|
} from '@ultimat3/core';
|
|
19
18
|
import type { RateLimitStore } from '@ultimat3/http';
|
|
20
|
-
import { postgresRateLimitStore } from '@ultimat3/http';
|
|
19
|
+
import { adoptRateLimitStore, postgresRateLimitStore } from '@ultimat3/http';
|
|
21
20
|
import type { EventBus, JobDriver, OutboxStore } from '@ultimat3/jobs';
|
|
22
21
|
import type { MailDriver } from '@ultimat3/mail';
|
|
23
22
|
import {
|
|
24
23
|
isMemoryDriver,
|
|
25
24
|
isUnconfiguredDriver,
|
|
26
25
|
resetMailDriver,
|
|
27
|
-
selectMailDriver,
|
|
28
26
|
setMailDriver,
|
|
29
27
|
} from '@ultimat3/mail';
|
|
30
28
|
import type { Transport, TransportSelection } from '@ultimat3/realtime/server';
|
|
31
29
|
import { selectTransport } from '@ultimat3/realtime/server';
|
|
32
30
|
import type { Storage } from '@ultimat3/storage';
|
|
33
31
|
import { defineStorage, localDriver, s3Driver, usesDevStorageSecret } from '@ultimat3/storage';
|
|
32
|
+
import { loadAppConfig } from './app-config-load';
|
|
34
33
|
import { LocalDiskUnsafeError, StorageUnwritableError } from './errors';
|
|
35
34
|
import { msg } from './messages';
|
|
36
35
|
import type { DevServices, Env } from './runtime-bindings';
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
36
|
+
import { cacheTiersOf, startCacheTiers } from './runtime-cache';
|
|
37
|
+
import { type WorkerConfig, workerConfigOf } from './runtime-jobs';
|
|
38
|
+
import { selectAppMailDriver } from './runtime-mail';
|
|
39
|
+
import { inboxRetentionOf } from './runtime-notify-retention';
|
|
40
40
|
import type { RuntimeOverrides } from './runtime-overrides';
|
|
41
41
|
import { installRetentionSweep } from './runtime-purge';
|
|
42
|
-
import type { DevDbClient } from './runtime-queue';
|
|
42
|
+
import type { DevDbClient, SchemaMode } from './runtime-queue';
|
|
43
43
|
import { pgExecutorFor, startQueue } from './runtime-queue';
|
|
44
|
-
import {
|
|
44
|
+
import { realtimeConfigOf } from './runtime-realtime';
|
|
45
45
|
|
|
46
46
|
export interface RunningServices {
|
|
47
47
|
readonly services: DevServices;
|
|
@@ -57,7 +57,8 @@ export interface RunningServices {
|
|
|
57
57
|
*/
|
|
58
58
|
readonly realtime: RealtimeConfig;
|
|
59
59
|
/**
|
|
60
|
-
* `app.config.ts`'s `jobs.queues` / `concurrency` / `visibilityTimeoutMs`, for the `worker` role
|
|
60
|
+
* `app.config.ts`'s `jobs.queues` / `concurrency` / `visibilityTimeoutMs`, for the `worker` role —
|
|
61
|
+
* the queues replaced by the boot env's `WORKER_QUEUES` when it is set (`runtime-jobs.ts`).
|
|
61
62
|
* Optional because a `RunningServices` can be hand-built, and a test runtime with none keeps the
|
|
62
63
|
* worker's own defaults — the answer it always got. `startServices` always sets it.
|
|
63
64
|
*/
|
|
@@ -87,7 +88,7 @@ export interface RunningServices {
|
|
|
87
88
|
* `roles.web.replicas: 3` and `x new` scaffolds two.
|
|
88
89
|
*
|
|
89
90
|
* OPTIONAL because a `RunningServices` can be hand-built: a test's fixture runtime has a stub
|
|
90
|
-
* client and no shared store, which is `
|
|
91
|
+
* client and no shared store, which is `httpServer`'s memory store and `scope: 'process'` —
|
|
91
92
|
* the honest answer for it, and the one `startWeb` derives.
|
|
92
93
|
*/
|
|
93
94
|
readonly rateLimitStore?: RateLimitStore;
|
|
@@ -301,15 +302,35 @@ async function release(steps: readonly (() => void | Promise<void>)[]): Promise<
|
|
|
301
302
|
return failures;
|
|
302
303
|
}
|
|
303
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Every STOP path's form of `release`: every step runs, newest first, and the FIRST failure is
|
|
307
|
+
* rethrown — it is the cause, the rest its consequences. The one helper for this process's stops
|
|
308
|
+
* (`startServices`, `startRoles`, `bootServing`, `x dev`): a chain of `await`s with no `try`
|
|
309
|
+
* skipped every step after one rejection, and the replicator rejects a refused close on purpose.
|
|
310
|
+
* Steps are listed in BOOT order, as an unwind list is.
|
|
311
|
+
*/
|
|
312
|
+
export async function releaseOrThrow(
|
|
313
|
+
steps: readonly (() => void | Promise<void>)[],
|
|
314
|
+
): Promise<void> {
|
|
315
|
+
const failures = await release(steps);
|
|
316
|
+
if (failures.length > 0) throw failures[0];
|
|
317
|
+
}
|
|
318
|
+
|
|
304
319
|
export async function startServices(
|
|
305
320
|
services: DevServices,
|
|
306
321
|
env: Env,
|
|
307
322
|
overrides?: RuntimeOverrides,
|
|
323
|
+
/** `'verify'` for a serving role on an external database — `runtime-queue.ts`'s `SchemaMode`. */
|
|
324
|
+
schema: SchemaMode = 'apply',
|
|
308
325
|
): Promise<RunningServices> {
|
|
326
|
+
// The app's config is loaded ONCE, here, first, and every section below is read out of that one
|
|
327
|
+
// validated object — mail's `retainMime` is the first reader, so it is loaded before mail.
|
|
328
|
+
const config = await loadAppConfig(services.root);
|
|
309
329
|
// Before the queue: selection is pure — it parses `SMTP_URL` and builds a transport, it does
|
|
310
330
|
// not dial. A typo'd credential must fail on the spot rather than after PGlite has started and
|
|
311
331
|
// been unwound again, and it must fail at boot rather than on the first mail nobody receives.
|
|
312
|
-
|
|
332
|
+
// `mail.retainMime` rides along (`runtime-mail.ts`), refused here the same way.
|
|
333
|
+
const selection = selectAppMailDriver(env, config);
|
|
313
334
|
// Same reason, same place: building a purge driver reads env and dials nothing, so a half-set
|
|
314
335
|
// `FASTLY_API_TOKEN` without its service id fails here rather than on the first stale page.
|
|
315
336
|
const cdn = selectPurgeDriver(env);
|
|
@@ -318,14 +339,14 @@ export async function startServices(
|
|
|
318
339
|
// service starts, rather than quietly keeping the in-process bus. Which transport, which KV
|
|
319
340
|
// bucket and which presence TTL is `@ultimat3/realtime`'s decision, and it is the same call a
|
|
320
341
|
// `ROLE=sync` container makes, so this process cannot resolve the bus differently from the
|
|
321
|
-
// container it stands in for.
|
|
322
|
-
const realtime =
|
|
323
|
-
const workerConfig =
|
|
342
|
+
// container it stands in for.
|
|
343
|
+
const realtime = realtimeConfigOf(config);
|
|
344
|
+
const workerConfig = workerConfigOf(config, env);
|
|
324
345
|
const bus: TransportSelection = selectTransport(env, realtime);
|
|
325
346
|
// `env`, not the ambient one: this function is HANDED the boot's environment and every other
|
|
326
347
|
// reader here already uses it, so a queue that asked `process.env` would decide the standby from
|
|
327
348
|
// a different answer than the middleware that routes to it.
|
|
328
|
-
const queue = await startQueue(services, overrides, env);
|
|
349
|
+
const queue = await startQueue(services, overrides, env, schema);
|
|
329
350
|
const { db, jobs, outbox, events } = queue;
|
|
330
351
|
// The same executor the jobs driver, the outbox, the event bus and the idempotency store run
|
|
331
352
|
// on — one pool, one `Bun.sql` that does NOT satisfy `PgExecutor` (`Bun.sql.query` is
|
|
@@ -354,18 +375,23 @@ export async function startServices(
|
|
|
354
375
|
postgresAuthLimiter({ executor, clock: systemClock, policy }),
|
|
355
376
|
);
|
|
356
377
|
started.push(() => resetAuthLimiters());
|
|
378
|
+
// Where an action's or a query's declared `rateLimit:` is counted — on every surface, so a
|
|
379
|
+
// task or a one-off command spends from the fleet's table too, not one process' memory. Before
|
|
380
|
+
// `loadApp` for the auth limiters' reason; `startRoles` stacks a deployment's override over it.
|
|
381
|
+
started.push(adoptRateLimitStore(rateLimitStore));
|
|
357
382
|
// The hourly sweep over the framework tables this boot is responsible for. Every one of them
|
|
358
383
|
// ships a `purgeExpired()` that nothing called, so every row written was a row kept —
|
|
359
384
|
// `x_rate_limit` takes one upsert per request the web role serves, assets included.
|
|
360
385
|
//
|
|
361
386
|
// `inboxRetention` is READ HERE and not defaulted in `runtime-purge.ts`: the two windows are the
|
|
362
|
-
// app's,
|
|
363
|
-
//
|
|
387
|
+
// app's, and a loader that silently answered "never sweep" from inside the sweep would be
|
|
388
|
+
// indistinguishable from an app that chose it.
|
|
364
389
|
started.push(
|
|
365
390
|
installRetentionSweep({
|
|
366
391
|
idempotency: queue.idempotency,
|
|
367
392
|
rateLimit: rateLimitStore,
|
|
368
|
-
|
|
393
|
+
events,
|
|
394
|
+
inboxRetention: inboxRetentionOf(config),
|
|
369
395
|
}),
|
|
370
396
|
);
|
|
371
397
|
// One readiness check per resource this boot OWNS, released with it. Nothing in the tree
|
|
@@ -402,17 +428,15 @@ export async function startServices(
|
|
|
402
428
|
started.push(() => resetMailDriver());
|
|
403
429
|
const purge = overrides?.purge ?? cdn.driver;
|
|
404
430
|
// Every tier this process reads through, plus the cross-instance invalidation hop, in one
|
|
405
|
-
// call. The CDN tier used to be the only one registered here — so `
|
|
406
|
-
// `
|
|
431
|
+
// call. The CDN tier used to be the only one registered here — so `redisTier`,
|
|
432
|
+
// `lruTier` and `memoTier` shipped with no caller at all and every replica
|
|
407
433
|
// recomputed every cached read. Released with `resetTiers()`, which drops the whole registry:
|
|
408
434
|
// this boot is the only thing that registers one, and a tier left behind would purge for a
|
|
409
435
|
// process that has stopped.
|
|
410
436
|
// The ladder is the app's declaration, never the environment's. `cache.tiers` was declared,
|
|
411
437
|
// validated and documented while NOTHING read it — so an app asking for one rung got four,
|
|
412
|
-
// and one asking for a shared tier got whatever `REDIS_URL` happened to say.
|
|
413
|
-
|
|
414
|
-
// and does not already carry.
|
|
415
|
-
const tiers = await loadCacheTiers(dirname(services.stateDir));
|
|
438
|
+
// and one asking for a shared tier got whatever `REDIS_URL` happened to say.
|
|
439
|
+
const tiers = cacheTiersOf(config);
|
|
416
440
|
started.push(startCacheTiers({ env, purge, transport, tiers }));
|
|
417
441
|
|
|
418
442
|
return {
|
|
@@ -443,8 +467,7 @@ export async function startServices(
|
|
|
443
467
|
// the FIRST failure is rethrown because it is the cause and the rest are its consequences,
|
|
444
468
|
// and every step still runs, so a refused shutdown never leaks into the next boot.
|
|
445
469
|
async stop() {
|
|
446
|
-
|
|
447
|
-
if (failures.length > 0) throw failures[0];
|
|
470
|
+
await releaseOrThrow(started);
|
|
448
471
|
},
|
|
449
472
|
};
|
|
450
473
|
} catch (error) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { finiteCount, readWithinLimit } from '@ultimat3/core';
|
|
7
7
|
import type { RequestContext, Route, UltimateRequest } from '@ultimat3/http';
|
|
8
|
-
import {
|
|
8
|
+
import { jsonResponse, NO_STORE } from '@ultimat3/http';
|
|
9
9
|
import type { Storage } from '@ultimat3/storage';
|
|
10
10
|
import {
|
|
11
11
|
acceptSignedUpload,
|
|
@@ -74,7 +74,7 @@ async function acceptUpload(
|
|
|
74
74
|
: { allowedContentTypes: [constraints.contentType] }),
|
|
75
75
|
}),
|
|
76
76
|
});
|
|
77
|
-
return
|
|
77
|
+
return jsonResponse({ key: stored.key }, { status: 201 });
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/**
|