@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
|
@@ -1,18 +1,8 @@
|
|
|
1
|
-
// `notify.inboxReadRetentionMs` and `notify.inboxUnreadRetentionMs`,
|
|
2
|
-
// `app
|
|
3
|
-
//
|
|
4
|
-
// a config that resolved through an older core simply has no `notify` section.
|
|
5
|
-
//
|
|
6
|
-
// WHY A LOADER AND NOT A BOOT ARGUMENT: `startServices` has no `AppConfig` — the app's modules
|
|
7
|
-
// import after it, which is the same reason `configureAuthLimiters` takes a factory. A key read
|
|
8
|
-
// here is a key read from the file the operator edited, per boot.
|
|
1
|
+
// `notify.inboxReadRetentionMs` and `notify.inboxUnreadRetentionMs`, out of the config
|
|
2
|
+
// `startServices` loads once (`app-config-load.ts`) — before the app's modules import, so the window
|
|
3
|
+
// is read from the file the operator edited, per boot.
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
// `pathToFileURL` and `fileURLToPath`, and nothing that joins a path.
|
|
12
|
-
import { join } from 'node:path';
|
|
13
|
-
import { INBOX_RETENTION_KEYS } from '@ultimat3/core';
|
|
14
|
-
import { APP_CONFIG_EXPORT } from './app-auth';
|
|
15
|
-
import { APP_CONFIG_FILE } from './app-root';
|
|
5
|
+
import type { AppConfig } from '@ultimat3/core';
|
|
16
6
|
|
|
17
7
|
/**
|
|
18
8
|
* The two windows in milliseconds, each `undefined` where the app named none.
|
|
@@ -32,38 +22,12 @@ export const NO_INBOX_RETENTION: InboxRetention = Object.freeze({
|
|
|
32
22
|
unreadMs: undefined,
|
|
33
23
|
});
|
|
34
24
|
|
|
35
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
36
|
-
typeof value === 'object' && value !== null;
|
|
37
|
-
|
|
38
25
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* assembled by hand, or one that resolved through a core too old to validate the section, reaches
|
|
42
|
-
* this line unchecked. A window that is not a positive finite number reads as absent: refusing
|
|
43
|
-
* would take the sweep over the other four framework tables down with it, and the whole point of
|
|
44
|
-
* the key is that not sweeping is a legal state.
|
|
26
|
+
* Core's validator (through the loader) refuses a window that is not a positive finite number, so
|
|
27
|
+
* a `NaN` cutoff — every row older than it, the inbox emptied on the first pass — cannot reach here.
|
|
45
28
|
*/
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
return
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export async function loadInboxRetention(root: string): Promise<InboxRetention> {
|
|
53
|
-
const configPath = join(root, APP_CONFIG_FILE);
|
|
54
|
-
// `Bun.file(p).exists()` rather than `existsSync`: this function is already async, so the
|
|
55
|
-
// `node:fs` import buys nothing here — and an import with nothing to say for itself is what
|
|
56
|
-
// `bun run node-imports` refuses.
|
|
57
|
-
if (!(await Bun.file(configPath).exists())) return NO_INBOX_RETENTION;
|
|
58
|
-
const module = (await import(configPath)) as Record<string, unknown>;
|
|
59
|
-
const config = module[APP_CONFIG_EXPORT];
|
|
60
|
-
if (!isRecord(config)) return NO_INBOX_RETENTION;
|
|
61
|
-
const notify = config['notify'];
|
|
62
|
-
if (!isRecord(notify)) return NO_INBOX_RETENTION;
|
|
63
|
-
// Keyed off core's own list rather than two string literals here, so the two names exist in one
|
|
64
|
-
// place. What actually stops a third window being declared-and-never-read is
|
|
65
|
-
// `bun run scripts/config-readers.ts` — every leaf key of `AppConfig` needs a reader or a pinned
|
|
66
|
-
// reason, and it is the guard whose own header calls that "the framework's most repeated defect".
|
|
67
|
-
const [readKey, unreadKey] = INBOX_RETENTION_KEYS;
|
|
68
|
-
return { readMs: windowOf(notify, readKey), unreadMs: windowOf(notify, unreadKey) };
|
|
29
|
+
export function inboxRetentionOf(config: AppConfig | undefined): InboxRetention {
|
|
30
|
+
const notify = config?.notify;
|
|
31
|
+
if (notify === undefined) return NO_INBOX_RETENTION;
|
|
32
|
+
return { readMs: notify.inboxReadRetentionMs, unreadMs: notify.inboxUnreadRetentionMs };
|
|
69
33
|
}
|
package/src/runtime-overrides.ts
CHANGED
|
@@ -35,7 +35,7 @@ export interface RuntimeOverrides {
|
|
|
35
35
|
readonly mail?: MailDriver;
|
|
36
36
|
/**
|
|
37
37
|
* Replaces the `NATS_URL` selection. Already connected when it arrives, and NOT closed by
|
|
38
|
-
* `stop()`: whoever built it owns its socket, exactly as `
|
|
38
|
+
* `stop()`: whoever built it owns its socket, exactly as `httpServer` does not close a
|
|
39
39
|
* `rateLimitStore` it was handed.
|
|
40
40
|
*/
|
|
41
41
|
readonly transport?: Transport;
|
|
@@ -53,12 +53,12 @@ export interface RuntimeOverrides {
|
|
|
53
53
|
*/
|
|
54
54
|
readonly rateLimitStore?: RateLimitStore;
|
|
55
55
|
/**
|
|
56
|
-
* Where regenerated ISR pages live. Omitted, `
|
|
56
|
+
* Where regenerated ISR pages live. Omitted, `isrController` keeps a per-process memory
|
|
57
57
|
* store — twelve replicas then hold twelve of them, and a purge tag reaches one twelfth of the
|
|
58
58
|
* fleet.
|
|
59
59
|
*/
|
|
60
60
|
readonly isrStore?: IsrStore;
|
|
61
|
-
/** Prepended to the pipeline by `
|
|
61
|
+
/** Prepended to the pipeline by `httpServer`, which `startRoles` never passed one. */
|
|
62
62
|
readonly middleware?: readonly Middleware[];
|
|
63
63
|
/**
|
|
64
64
|
* Plain HTTP routes at paths no primitive projects to — `/.well-known/oauth-authorization-server`,
|
|
@@ -69,7 +69,7 @@ export interface RuntimeOverrides {
|
|
|
69
69
|
* answers a document, and a protocol that fixes its own error shape is neither.
|
|
70
70
|
*/
|
|
71
71
|
readonly routes?: readonly Route[];
|
|
72
|
-
/** The `/media/*` transform. Omitted, `builtinImageDriver` — core's PNG/JPEG pipeline. */
|
|
72
|
+
/** The `/media/*` transform. Omitted, `builtinImageDriver` — core's PNG/JPEG/WebP pipeline. */
|
|
73
73
|
readonly images?: ImageTransformDriver;
|
|
74
74
|
/**
|
|
75
75
|
* Who is dialling the `sync` node. Omitted, the app's own `configureAuthenticator()` is adapted
|
package/src/runtime-purge.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// The retention sweep this boot owns: the framework tables that grow with traffic, the `purge()`
|
|
2
2
|
// job that empties them and the `task` that fires it hourly.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// `x_auth`
|
|
6
|
-
// prose but here: this header said "three" for two releases
|
|
7
|
-
// boot's DDL, which is how `x_notify_inbox` became the one
|
|
4
|
+
// SEVEN TARGETS over seven tables, `As of 2026-10-02` — `x_idempotency`, `x_rate_limit`, the
|
|
5
|
+
// `x_auth` limiter's table, `x_notify_deliveries`, `x_notify_inbox`, `x_notify_digests` and
|
|
6
|
+
// `x_job_events`. Counted nowhere in prose but here: this header said "three" for two releases
|
|
7
|
+
// after the notify tables joined the boot's DDL, which is how `x_notify_inbox` became the one
|
|
8
|
+
// framework table nothing swept.
|
|
8
9
|
//
|
|
9
10
|
// WHY here and not in the packages that own the tables: `postgresIdempotencyStore` (tier 3),
|
|
10
11
|
// `postgresRateLimitStore` (tier 2) and `postgresAuthLimiter` (tier 2) cannot see each other and
|
|
@@ -20,9 +21,16 @@
|
|
|
20
21
|
import type { PostgresIdempotencyStore } from '@ultimat3/action';
|
|
21
22
|
import { purgeAuthLimits } from '@ultimat3/auth';
|
|
22
23
|
import type { PostgresRateLimitStore } from '@ultimat3/http';
|
|
23
|
-
import type { JobHandle, PurgeInput, PurgeTarget } from '@ultimat3/jobs';
|
|
24
|
-
import {
|
|
25
|
-
|
|
24
|
+
import type { EventBus, JobHandle, PurgeInput, PurgeTarget } from '@ultimat3/jobs';
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_PURGE_CRON,
|
|
27
|
+
eventsPurgeTarget,
|
|
28
|
+
getJob,
|
|
29
|
+
getTask,
|
|
30
|
+
purge,
|
|
31
|
+
task,
|
|
32
|
+
} from '@ultimat3/jobs';
|
|
33
|
+
import { purgeNotifyDeliveries, purgeNotifyDigests, purgeNotifyInbox } from '@ultimat3/notify';
|
|
26
34
|
import type { InboxRetention } from './runtime-notify-retention';
|
|
27
35
|
import { NO_INBOX_RETENTION } from './runtime-notify-retention';
|
|
28
36
|
|
|
@@ -32,7 +40,7 @@ export const PURGE_JOB_NAME = 'x.purge';
|
|
|
32
40
|
export const PURGE_TASK_NAME = 'x.purge.hourly';
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
* The
|
|
43
|
+
* The stores this boot BUILT, handed over rather than rebuilt here. A second
|
|
36
44
|
* `postgresIdempotencyStore({ executor })` would sweep on the default window even where the boot
|
|
37
45
|
* had configured another — two answers to "how long is a record kept", and the shorter one
|
|
38
46
|
* deletes reservations a retry is still entitled to.
|
|
@@ -45,15 +53,22 @@ export interface RetentionStores {
|
|
|
45
53
|
readonly idempotency: PostgresIdempotencyStore;
|
|
46
54
|
readonly rateLimit: PostgresRateLimitStore;
|
|
47
55
|
/**
|
|
48
|
-
* The
|
|
49
|
-
* `
|
|
50
|
-
*
|
|
56
|
+
* The bus this boot installed for `step.waitForEvent`. REQUIRED: `x_job_events` takes a row per
|
|
57
|
+
* publish and `find` only filters an expired one, so a boot that left it out is the table
|
|
58
|
+
* growing for the life of the deployment — which it did until this member existed.
|
|
59
|
+
*/
|
|
60
|
+
readonly events: EventBus;
|
|
61
|
+
/**
|
|
62
|
+
* The app's own answer to "how long is an unread message kept", read off the config
|
|
63
|
+
* `startServices` loads (`inboxRetentionOf`). Absent windows mean the inbox is never swept,
|
|
64
|
+
* which is the default and a decision rather than an oversight — see `NotifyConfig` in
|
|
65
|
+
* `@ultimat3/core`.
|
|
51
66
|
*/
|
|
52
67
|
readonly inboxRetention?: InboxRetention | undefined;
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
/**
|
|
56
|
-
* The
|
|
71
|
+
* The three notify tables, and no store is in `RetentionStores` — deliberately, and for the
|
|
57
72
|
* reason `authTarget` is not either. `setNotifyStores` is an APP's boot line and runs when the
|
|
58
73
|
* app's modules import, which is AFTER this install; `framework-schema.ts` says so where it
|
|
59
74
|
* applies the DDL "whether or not this boot calls `setNotifyStores`". So the sweep can only ask,
|
|
@@ -79,6 +94,12 @@ function notifyTargets(retention: InboxRetention): readonly PurgeTarget[] {
|
|
|
79
94
|
retention.unreadMs === undefined ? undefined : new Date(nowMs - retention.unreadMs),
|
|
80
95
|
}),
|
|
81
96
|
},
|
|
97
|
+
{
|
|
98
|
+
name: 'x_notify_digests',
|
|
99
|
+
// The job's clock, and the store's own window (`postgresDigestStore({ retentionMs })`, a week
|
|
100
|
+
// by default) — a closed window only its dead-lettered flush would ever have drained.
|
|
101
|
+
purgeExpired: (nowMs: number): Promise<number> => purgeNotifyDigests(nowMs),
|
|
102
|
+
},
|
|
82
103
|
];
|
|
83
104
|
}
|
|
84
105
|
|
|
@@ -94,14 +115,13 @@ let installed: readonly PurgeTarget[] = [];
|
|
|
94
115
|
let sweep: JobHandle<PurgeInput> | undefined;
|
|
95
116
|
|
|
96
117
|
/**
|
|
97
|
-
* `
|
|
98
|
-
*
|
|
99
|
-
* tables would be a second delete that removes nothing.
|
|
118
|
+
* `x_auth_lockouts` — every credential bucket, one row per key. The target keeps the `x_auth`
|
|
119
|
+
* name it has always reported under: it is what a dashboard and `PurgeReport.swept` already key on.
|
|
100
120
|
*/
|
|
101
121
|
const authTarget: PurgeTarget = {
|
|
102
122
|
name: 'x_auth',
|
|
103
123
|
// No `nowMs`: a limiter built through the seam holds the clock its host handed it, which is the
|
|
104
|
-
// clock every
|
|
124
|
+
// clock every instant in that table was written from.
|
|
105
125
|
purgeExpired: () => purgeAuthLimits(),
|
|
106
126
|
};
|
|
107
127
|
|
|
@@ -121,6 +141,8 @@ function retentionTargets(stores: RetentionStores): readonly PurgeTarget[] {
|
|
|
121
141
|
},
|
|
122
142
|
authTarget,
|
|
123
143
|
...notifyTargets(stores.inboxRetention ?? NO_INBOX_RETENTION),
|
|
144
|
+
// Its own clock, and so no `nowMs`: every instant in that table is the database's.
|
|
145
|
+
eventsPurgeTarget(stores.events),
|
|
124
146
|
];
|
|
125
147
|
}
|
|
126
148
|
|
package/src/runtime-queue.ts
CHANGED
|
@@ -7,32 +7,34 @@
|
|
|
7
7
|
import {
|
|
8
8
|
type PostgresIdempotencyStore,
|
|
9
9
|
postgresIdempotencyStore,
|
|
10
|
+
requestDeadlineMs,
|
|
10
11
|
resetIdempotency,
|
|
11
12
|
setIdempotencyStore,
|
|
12
13
|
} from '@ultimat3/action';
|
|
13
14
|
import type { DbClient, PgliteClient, PostgresClient, SqlFragment } from '@ultimat3/db';
|
|
14
15
|
import {
|
|
15
|
-
|
|
16
|
-
createPostgresClient,
|
|
16
|
+
baseClient,
|
|
17
17
|
currentTx,
|
|
18
|
+
pgliteClient,
|
|
18
19
|
pgliteDataDir,
|
|
19
|
-
|
|
20
|
+
postgresClient,
|
|
20
21
|
setDbClient,
|
|
21
22
|
} from '@ultimat3/db';
|
|
22
23
|
import type { Tx } from '@ultimat3/entity';
|
|
23
24
|
import type { EventBus, JobDriver, OutboxStore, PgExecutor } from '@ultimat3/jobs';
|
|
24
25
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
outboxJobsFacade,
|
|
27
|
+
postgresEventBus,
|
|
28
|
+
postgresJobDriver,
|
|
29
|
+
postgresOutboxStore,
|
|
30
|
+
resetEventBus,
|
|
29
31
|
resetJobDriver,
|
|
30
32
|
resetJobsFacade,
|
|
31
33
|
setEventBus,
|
|
32
34
|
setJobDriver,
|
|
33
35
|
setJobsFacade,
|
|
34
36
|
} from '@ultimat3/jobs';
|
|
35
|
-
import {
|
|
37
|
+
import { applyLockedSchema, verifySchema } from './framework-schema-apply';
|
|
36
38
|
import { appExtensions } from './migration-extensions';
|
|
37
39
|
import type { DevServices } from './runtime-bindings';
|
|
38
40
|
import type { RuntimeOverrides } from './runtime-overrides';
|
|
@@ -97,11 +99,11 @@ export function startDb(services: DevServices, env: ReplicaEnv): StartedDb {
|
|
|
97
99
|
// Linked at boot, from the app's own migrations: PGlite cannot `create extension` one it
|
|
98
100
|
// was not handed, and the schema dump's scratch replay already asks this same reader — an
|
|
99
101
|
// app that replayed in the gate and failed in `x dev` was the two disagreeing.
|
|
100
|
-
|
|
102
|
+
pgliteClient({
|
|
101
103
|
dataDir: pgliteDataDir(binding.url),
|
|
102
104
|
extensions: () => appExtensions(services.root),
|
|
103
105
|
})
|
|
104
|
-
:
|
|
106
|
+
: postgresClient({ url: binding.url });
|
|
105
107
|
const attached = attachReplica(client, replicaUrlFor(binding, env));
|
|
106
108
|
setDbClient(attached.client);
|
|
107
109
|
return { client, replica: attached.replica };
|
|
@@ -134,10 +136,18 @@ export function pgExecutorFor(client: DbClient): PgExecutor {
|
|
|
134
136
|
* `@ultimat3/notify` because a package that holds no database dependency cannot apply its own
|
|
135
137
|
* schema — the same reason `SQL_JOBS_TABLE` is applied by the boot.
|
|
136
138
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
139
|
+
/**
|
|
140
|
+
* `'apply'` — `ROLE=migrate`, `x dev` and every CLI command — runs the DDL under the migration
|
|
141
|
+
* lock with a bounded `lock_timeout` and stamps the build. `'verify'` — a serving role on an
|
|
142
|
+
* external database — runs none: an `alter table` queued behind one open transaction stalled every
|
|
143
|
+
* enqueue in the fleet on each pod start, and racing `create table`s were a first-deploy `23505`.
|
|
144
|
+
*/
|
|
145
|
+
export type SchemaMode = 'apply' | 'verify';
|
|
140
146
|
|
|
147
|
+
async function applySchema(client: DevDbClient, mode: SchemaMode): Promise<void> {
|
|
148
|
+
if (mode === 'verify') await verifySchema(client);
|
|
149
|
+
else await applyLockedSchema(client);
|
|
150
|
+
}
|
|
141
151
|
/**
|
|
142
152
|
* The dev queue is the real Postgres queue on the embedded Postgres — claiming, leases and the
|
|
143
153
|
* one-live-job-per-key index all behave here exactly as in production. A memory queue in dev
|
|
@@ -169,23 +179,31 @@ async function applySchema(client: DevDbClient): Promise<void> {
|
|
|
169
179
|
async function startJobs(
|
|
170
180
|
client: DevDbClient,
|
|
171
181
|
replica: PostgresClient | undefined,
|
|
172
|
-
overrides
|
|
182
|
+
overrides: RuntimeOverrides | undefined,
|
|
183
|
+
schema: SchemaMode,
|
|
173
184
|
): Promise<RunningQueue> {
|
|
174
|
-
await applySchema(client);
|
|
185
|
+
await applySchema(client, schema);
|
|
175
186
|
const executor = pgExecutorFor(client);
|
|
176
|
-
const driver = overrides?.jobs ??
|
|
187
|
+
const driver = overrides?.jobs ?? postgresJobDriver({ executor });
|
|
177
188
|
setJobDriver(driver);
|
|
178
|
-
const outbox =
|
|
189
|
+
const outbox = postgresOutboxStore({
|
|
179
190
|
executor,
|
|
180
191
|
// The open transaction is a client on its own connection; the `Tx` token is not that object.
|
|
181
192
|
txExecutor: () => pgExecutorFor(currentTx() ?? client),
|
|
182
193
|
});
|
|
183
194
|
setJobsFacade(
|
|
184
|
-
|
|
195
|
+
outboxJobsFacade({ store: outbox, driver }, () => currentTx() as unknown as Tx | undefined),
|
|
185
196
|
);
|
|
186
|
-
const events =
|
|
197
|
+
const events = postgresEventBus({ executor });
|
|
187
198
|
setEventBus(events);
|
|
188
|
-
const idempotency = postgresIdempotencyStore({
|
|
199
|
+
const idempotency = postgresIdempotencyStore({
|
|
200
|
+
executor,
|
|
201
|
+
// `baseClient`, read per call and never `client`: a transaction's origin is the AMBIENT client
|
|
202
|
+
// `startDb` installed, which is the replicated pair when a standby is configured.
|
|
203
|
+
origin: baseClient,
|
|
204
|
+
// The app's own `requestTimeoutMs`, read per reservation — it is declared after this boot.
|
|
205
|
+
reclaimAfterMs: requestDeadlineMs,
|
|
206
|
+
});
|
|
189
207
|
setIdempotencyStore(idempotency);
|
|
190
208
|
return {
|
|
191
209
|
db: client,
|
|
@@ -204,7 +222,8 @@ async function startJobs(
|
|
|
204
222
|
* startup entirely, so every query it makes fails on a connection this process already dropped.
|
|
205
223
|
*
|
|
206
224
|
* The facade goes with the driver for the same reason: an enqueue routed through a store bound to
|
|
207
|
-
* a closed client is a staged row nothing will ever publish
|
|
225
|
+
* a closed client is a staged row nothing will ever publish — and the event bus with both, or a
|
|
226
|
+
* `publishEvent` after `stop()` runs over the closed pool.
|
|
208
227
|
*/
|
|
209
228
|
async function releaseQueue(
|
|
210
229
|
db: DevDbClient,
|
|
@@ -215,6 +234,7 @@ async function releaseQueue(
|
|
|
215
234
|
// holds this client, and the next command in this process would reserve keys over a closed one.
|
|
216
235
|
resetIdempotency();
|
|
217
236
|
resetJobsFacade();
|
|
237
|
+
resetEventBus();
|
|
218
238
|
resetJobDriver();
|
|
219
239
|
setDbClient(undefined);
|
|
220
240
|
await jobs?.close?.();
|
|
@@ -239,13 +259,14 @@ export async function startQueue(
|
|
|
239
259
|
* this file reads it: see `startDb` for what two readers of one question cost.
|
|
240
260
|
*/
|
|
241
261
|
env: ReplicaEnv = process.env,
|
|
262
|
+
schema: SchemaMode = 'apply',
|
|
242
263
|
): Promise<RunningQueue> {
|
|
243
264
|
const { client: db, replica } = startDb(services, env);
|
|
244
265
|
try {
|
|
245
266
|
// Pay the Postgres boot here, so the first request is not the slow one and a broken database
|
|
246
267
|
// fails at boot rather than on some later query.
|
|
247
268
|
await db.ping();
|
|
248
|
-
return await startJobs(db, replica, overrides);
|
|
269
|
+
return await startJobs(db, replica, overrides, schema);
|
|
249
270
|
} catch (error) {
|
|
250
271
|
// `db.ping()` or `startJobs` is where a broken database is supposed to fail. Without this,
|
|
251
272
|
// the caller exits holding the PGlite lock and the ambient accessors, and nothing is left to
|
package/src/runtime-realtime.ts
CHANGED
|
@@ -1,55 +1,21 @@
|
|
|
1
|
-
//
|
|
2
|
-
// `app
|
|
3
|
-
//
|
|
4
|
-
// plain object. Until 22.0.0 no boot read any of the three and `NATS_URL` alone decided the bus.
|
|
1
|
+
// The `realtime` section for the boot, out of the config `startServices` loads once
|
|
2
|
+
// (`app-config-load.ts`). Until 22.0.0 no boot read `enabled`, `transport` or `urlEnv` and
|
|
3
|
+
// `NATS_URL` alone decided the bus; the two per-actor caps are read by the `sync` role.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
// `pathToFileURL` and `fileURLToPath`, and nothing that joins a path.
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { ConfigInvalidError, type RealtimeConfig, type RealtimeTransport } from '@ultimat3/core';
|
|
10
|
-
import { APP_CONFIG_EXPORT } from './app-auth';
|
|
11
|
-
import { APP_CONFIG_FILE } from './app-root';
|
|
5
|
+
import type { AppConfig, RealtimeConfig } from '@ultimat3/core';
|
|
12
6
|
|
|
13
|
-
/** Core's `defaults()` for the section
|
|
7
|
+
/** Core's `defaults()` for the section — the answer for a root with no `app.config.ts`. */
|
|
14
8
|
export const REALTIME_DEFAULTS: RealtimeConfig = Object.freeze({
|
|
15
9
|
enabled: true,
|
|
16
10
|
transport: 'memory',
|
|
17
11
|
urlEnv: undefined,
|
|
12
|
+
maxSubscriptionsPerActor: undefined,
|
|
13
|
+
maxSocketsPerActor: undefined,
|
|
18
14
|
});
|
|
19
15
|
|
|
20
|
-
const TRANSPORTS: readonly RealtimeTransport[] = ['memory', 'nats'];
|
|
21
|
-
|
|
22
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
23
|
-
typeof value === 'object' && value !== null;
|
|
24
|
-
|
|
25
|
-
/** A transport this boot can build, or a refusal — never a guess at a bus. */
|
|
26
|
-
function transportOf(section: Record<string, unknown>): RealtimeTransport {
|
|
27
|
-
const value = section['transport'];
|
|
28
|
-
if (value === undefined) return REALTIME_DEFAULTS.transport;
|
|
29
|
-
const known = TRANSPORTS.find((transport) => transport === value);
|
|
30
|
-
if (known !== undefined) return known;
|
|
31
|
-
throw new ConfigInvalidError({
|
|
32
|
-
cause: `realtime.transport in ${APP_CONFIG_FILE} is not one of ${TRANSPORTS.join(', ')}`,
|
|
33
|
-
fix: `set realtime: { transport: 'memory' } in ${APP_CONFIG_FILE}, or { transport: 'nats', urlEnv: 'NATS_URL' } for more than one node`,
|
|
34
|
-
meta: { key: 'realtime.transport' },
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
16
|
/**
|
|
39
|
-
* The section as the boot obeys it
|
|
40
|
-
*
|
|
17
|
+
* The section as the boot obeys it — `defineConfig`'s per-key merge, so the boot and the app's own
|
|
18
|
+
* config object agree, and a transport nothing builds is core's `X_CONFIG_INVALID`.
|
|
41
19
|
*/
|
|
42
|
-
export
|
|
43
|
-
|
|
44
|
-
if (!(await Bun.file(path).exists())) return REALTIME_DEFAULTS;
|
|
45
|
-
const module = (await import(path)) as Record<string, unknown>;
|
|
46
|
-
const config = module[APP_CONFIG_EXPORT];
|
|
47
|
-
const section = isRecord(config) ? config['realtime'] : undefined;
|
|
48
|
-
if (!isRecord(section)) return REALTIME_DEFAULTS;
|
|
49
|
-
const { enabled, urlEnv } = section;
|
|
50
|
-
return {
|
|
51
|
-
enabled: typeof enabled === 'boolean' ? enabled : REALTIME_DEFAULTS.enabled,
|
|
52
|
-
transport: transportOf(section),
|
|
53
|
-
urlEnv: typeof urlEnv === 'string' && urlEnv.trim() !== '' ? urlEnv.trim() : undefined,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
20
|
+
export const realtimeConfigOf = (config: AppConfig | undefined): RealtimeConfig =>
|
|
21
|
+
config?.realtime ?? REALTIME_DEFAULTS;
|