@ultimat3/core 28.0.0 → 29.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/core",
3
- "version": "28.0.0",
3
+ "version": "29.0.0",
4
4
  "description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,13 +30,13 @@
30
30
  "LICENSE"
31
31
  ],
32
32
  "engines": {
33
- "bun": ">=1.4.3"
33
+ "bun": ">=1.4.4"
34
34
  },
35
35
  "scripts": {
36
36
  "typecheck": "tsc --noEmit -p tsconfig.json",
37
37
  "test": "bun test"
38
38
  },
39
39
  "dependencies": {
40
- "@ultimat3/schema": "28.0.0"
40
+ "@ultimat3/schema": "29.0.0"
41
41
  }
42
42
  }
@@ -2,6 +2,7 @@
2
2
  // at any depth and in any layer, is refused by path with the key it most likely meant. A key that
3
3
  // is merged and read by nothing is the switch with no wire, and a typo (`drian`) is the same defect.
4
4
 
5
+ import type { JobsConfig } from './config';
5
6
  import { BASE_FIX } from './config-fixes';
6
7
  import type { MailRetainMimeConfig } from './config-mail';
7
8
  import type { NavigationConfig, SpeculationConfig } from './config-navigation';
@@ -14,6 +15,7 @@ import type {
14
15
  PwaShortcut,
15
16
  PwaVapidConfig,
16
17
  } from './config-pwa';
18
+ import type { SiteConfig } from './config-site';
17
19
  import { ConfigInvalidError } from './errors';
18
20
  import { isJsonObject } from './json-object';
19
21
  import { nearestName } from './nearest-name';
@@ -60,6 +62,18 @@ export const SECTION_KEYS: Readonly<Record<string, readonly string[]>> = Object.
60
62
  'pwa.shortcuts[].icons[]': keysOf<PwaImage>()(['src', 'sizes', 'type', 'purpose']),
61
63
  'pwa.screenshots[]': keysOf<PwaScreenshot>()(['src', 'sizes', 'type', 'formFactor', 'label']),
62
64
  'mail.retainMime': keysOf<MailRetainMimeConfig>()(['maxBytes']),
65
+ jobs: keysOf<JobsConfig>()([
66
+ 'queues',
67
+ 'concurrency',
68
+ 'maxInFlight',
69
+ 'maxAttempts',
70
+ 'backoff',
71
+ 'visibilityTimeoutMs',
72
+ 'doneRetentionMs',
73
+ 'deadRetentionMs',
74
+ 'outboxRetentionMs',
75
+ ]),
76
+ site: keysOf<SiteConfig>()(['origin', 'favicon']),
63
77
  navigation: keysOf<NavigationConfig>()(['client', 'speculation']),
64
78
  'navigation.speculation': keysOf<SpeculationConfig>()(['prefetch', 'exclude']),
65
79
  });
@@ -11,6 +11,8 @@ import {
11
11
  } from './config-sitemap';
12
12
  import { describeValue } from './error-render';
13
13
 
14
+ export type SiteFavicon = `assets/${string}.svg`;
15
+
14
16
  export interface SiteConfig {
15
17
  /**
16
18
  * `https://www.example.com` — canonical, `og:url`, hreflang and the sitemap are absolute against
@@ -19,6 +21,11 @@ export interface SiteConfig {
19
21
  * host or a preview domain.
20
22
  */
21
23
  readonly origin: string | null;
24
+ /**
25
+ * The app-level SVG favicon, named as a site asset path (`assets/brand/icon.svg`). The renderer
26
+ * resolves it once at boot into the content-hashed URL every document carries.
27
+ */
28
+ readonly favicon?: SiteFavicon | undefined;
22
29
  }
23
30
 
24
31
  /**
@@ -105,7 +112,7 @@ export interface SiteSectionsInput {
105
112
  export function mergeSite(layers: readonly SiteSectionsInput[]): SiteSections {
106
113
  return {
107
114
  site: layered<SiteConfig>(
108
- { origin: null },
115
+ { origin: null, favicon: undefined },
109
116
  layers.map((layer) => layer.site),
110
117
  ),
111
118
  seo: {
@@ -141,6 +148,21 @@ function originIssue(origin: string): string | undefined {
141
148
  return undefined;
142
149
  }
143
150
 
151
+ /** A favicon names one SVG under the app's site assets, never an arbitrary URL or path. */
152
+ function faviconIssue(favicon: string): string | undefined {
153
+ if (!favicon.startsWith('assets/')) {
154
+ return `site.favicon "${favicon}" must be a site asset path starting with assets/`;
155
+ }
156
+ if (!favicon.endsWith('.svg')) {
157
+ return `site.favicon "${favicon}" must name an SVG asset`;
158
+ }
159
+ const segments = favicon.split('/');
160
+ if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
161
+ return `site.favicon "${favicon}" has an empty, "." or ".." segment`;
162
+ }
163
+ return undefined;
164
+ }
165
+
144
166
  /** Appends every refusal the two sections earn to `issues`, `config.ts`' one list. */
145
167
  export function siteIssues(config: SiteSections, issues: string[]): void {
146
168
  const origin = config.site.origin;
@@ -148,6 +170,11 @@ export function siteIssues(config: SiteSections, issues: string[]): void {
148
170
  const issue = originIssue(origin);
149
171
  if (issue !== undefined) issues.push(issue);
150
172
  }
173
+ const favicon = config.site.favicon;
174
+ if (favicon !== undefined) {
175
+ const issue = faviconIssue(favicon);
176
+ if (issue !== undefined) issues.push(issue);
177
+ }
151
178
  issues.push(...robotsPathIssues('seo.robots.disallow', config.seo.robots.disallow));
152
179
  issues.push(...robotsGroupIssues(config.seo.robots.groups ?? []));
153
180
  for (const path of config.seo.sitemap.extra as readonly unknown[]) {
package/src/config.ts CHANGED
@@ -160,6 +160,11 @@ export interface JobsConfig {
160
160
  * (`{ banks: 4, 'banks-long': 2 }`, a queue it does not name at `JOBS_CONCURRENCY_DEFAULT`).
161
161
  */
162
162
  readonly concurrency: JobsConcurrency;
163
+ /**
164
+ * Runs one worker process holds at once across every queue it serves (`jobWorker`'s
165
+ * `maxInFlight`), the per-queue `concurrency` still bounding each under it. Unset: no process cap.
166
+ */
167
+ readonly maxInFlight?: number | undefined;
163
168
  readonly maxAttempts: number;
164
169
  readonly backoff: (typeof JOB_BACKOFFS)[number];
165
170
  readonly visibilityTimeoutMs: number;
@@ -283,6 +288,9 @@ function validate(config: AppConfig): void {
283
288
  const perKey: readonly (string | undefined)[] = [
284
289
  ...config.roles.map((role) => oneOfIssue('roles', role, ROLES)),
285
290
  ...jobsConcurrencyIssues(config.jobs.concurrency),
291
+ config.jobs.maxInFlight === undefined
292
+ ? undefined
293
+ : countIssue('jobs.maxInFlight', config.jobs.maxInFlight, 1),
286
294
  countIssue('jobs.maxAttempts', config.jobs.maxAttempts, 1),
287
295
  countIssue('jobs.visibilityTimeoutMs', config.jobs.visibilityTimeoutMs, 1),
288
296
  ...(['doneRetentionMs', 'deadRetentionMs', 'outboxRetentionMs'] as const).map((key) =>
package/src/index.ts CHANGED
@@ -197,6 +197,7 @@ export type {
197
197
  SeoRobotsGroup,
198
198
  SeoSitemapConfig,
199
199
  SiteConfig,
200
+ SiteFavicon,
200
201
  SitemapLastmod,
201
202
  SiteSections,
202
203
  SiteSectionsInput,