@bgord/bun 1.18.11 → 1.18.13

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.
Files changed (128) hide show
  1. package/dist/index.d.ts +17 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +17 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/job-claimer-sqlite.adapter.d.ts.map +1 -1
  6. package/dist/job-claimer-sqlite.adapter.js +3 -2
  7. package/dist/job-claimer-sqlite.adapter.js.map +1 -1
  8. package/dist/job-completer-sqlite.adapter.d.ts.map +1 -1
  9. package/dist/job-completer-sqlite.adapter.js +2 -1
  10. package/dist/job-completer-sqlite.adapter.js.map +1 -1
  11. package/dist/job-enqueuer-sqlite.adapter.d.ts.map +1 -1
  12. package/dist/job-enqueuer-sqlite.adapter.js +2 -1
  13. package/dist/job-enqueuer-sqlite.adapter.js.map +1 -1
  14. package/dist/job-failer-sqlite.adapter.d.ts.map +1 -1
  15. package/dist/job-failer-sqlite.adapter.js +4 -1
  16. package/dist/job-failer-sqlite.adapter.js.map +1 -1
  17. package/dist/job-pruner-collecting.adapter.d.ts +7 -0
  18. package/dist/job-pruner-collecting.adapter.d.ts.map +1 -0
  19. package/dist/job-pruner-collecting.adapter.js +10 -0
  20. package/dist/job-pruner-collecting.adapter.js.map +1 -0
  21. package/dist/job-pruner-noop.adapter.d.ts +6 -0
  22. package/dist/job-pruner-noop.adapter.d.ts.map +1 -0
  23. package/dist/job-pruner-noop.adapter.js +7 -0
  24. package/dist/job-pruner-noop.adapter.js.map +1 -0
  25. package/dist/job-pruner-sqlite.adapter.d.ts +15 -0
  26. package/dist/job-pruner-sqlite.adapter.d.ts.map +1 -0
  27. package/dist/job-pruner-sqlite.adapter.js +15 -0
  28. package/dist/job-pruner-sqlite.adapter.js.map +1 -0
  29. package/dist/job-pruner.port.d.ts +5 -0
  30. package/dist/job-pruner.port.d.ts.map +1 -0
  31. package/dist/job-pruner.port.js +2 -0
  32. package/dist/job-pruner.port.js.map +1 -0
  33. package/dist/job-pruner.service.d.ts +14 -0
  34. package/dist/job-pruner.service.d.ts.map +1 -0
  35. package/dist/job-pruner.service.js +10 -0
  36. package/dist/job-pruner.service.js.map +1 -0
  37. package/dist/job-queue-sqlite-store.service.d.ts.map +1 -1
  38. package/dist/job-queue-sqlite-store.service.js +2 -1
  39. package/dist/job-queue-sqlite-store.service.js.map +1 -1
  40. package/dist/job-queue-stats-provider-sqlite.adapter.d.ts.map +1 -1
  41. package/dist/job-queue-stats-provider-sqlite.adapter.js +4 -3
  42. package/dist/job-queue-stats-provider-sqlite.adapter.js.map +1 -1
  43. package/dist/job-queue.adapter.d.ts +1 -1
  44. package/dist/job-queue.adapter.d.ts.map +1 -1
  45. package/dist/job-queue.adapter.js +2 -5
  46. package/dist/job-queue.adapter.js.map +1 -1
  47. package/dist/job-queue.port.d.ts +1 -1
  48. package/dist/job-queue.port.d.ts.map +1 -1
  49. package/dist/job-requeuer-sqlite.adapter.d.ts.map +1 -1
  50. package/dist/job-requeuer-sqlite.adapter.js +2 -1
  51. package/dist/job-requeuer-sqlite.adapter.js.map +1 -1
  52. package/dist/job-status.vo.d.ts +7 -0
  53. package/dist/job-status.vo.d.ts.map +1 -0
  54. package/dist/job-status.vo.js +8 -0
  55. package/dist/job-status.vo.js.map +1 -0
  56. package/dist/sitemap-changefreq.vo.d.ts +10 -0
  57. package/dist/sitemap-changefreq.vo.d.ts.map +1 -0
  58. package/dist/sitemap-changefreq.vo.js +11 -0
  59. package/dist/sitemap-changefreq.vo.js.map +1 -0
  60. package/dist/sitemap-entries-provider-dynamic.adapter.d.ts +13 -0
  61. package/dist/sitemap-entries-provider-dynamic.adapter.d.ts.map +1 -0
  62. package/dist/sitemap-entries-provider-dynamic.adapter.js +11 -0
  63. package/dist/sitemap-entries-provider-dynamic.adapter.js.map +1 -0
  64. package/dist/sitemap-entries-provider-static.adapter.d.ts +8 -0
  65. package/dist/sitemap-entries-provider-static.adapter.d.ts.map +1 -0
  66. package/dist/sitemap-entries-provider-static.adapter.js +10 -0
  67. package/dist/sitemap-entries-provider-static.adapter.js.map +1 -0
  68. package/dist/sitemap-entries-provider-with-cache.adapter.d.ts +20 -0
  69. package/dist/sitemap-entries-provider-with-cache.adapter.d.ts.map +1 -0
  70. package/dist/sitemap-entries-provider-with-cache.adapter.js +19 -0
  71. package/dist/sitemap-entries-provider-with-cache.adapter.js.map +1 -0
  72. package/dist/sitemap-entries-provider.port.d.ts +5 -0
  73. package/dist/sitemap-entries-provider.port.d.ts.map +1 -0
  74. package/dist/sitemap-entries-provider.port.js +2 -0
  75. package/dist/sitemap-entries-provider.port.js.map +1 -0
  76. package/dist/sitemap-entry.vo.d.ts +16 -0
  77. package/dist/sitemap-entry.vo.d.ts.map +1 -0
  78. package/dist/sitemap-entry.vo.js +14 -0
  79. package/dist/sitemap-entry.vo.js.map +1 -0
  80. package/dist/sitemap-hono.handler.d.ts +8 -0
  81. package/dist/sitemap-hono.handler.d.ts.map +1 -0
  82. package/dist/sitemap-hono.handler.js +17 -0
  83. package/dist/sitemap-hono.handler.js.map +1 -0
  84. package/dist/sitemap-priority.vo.d.ts +7 -0
  85. package/dist/sitemap-priority.vo.d.ts.map +1 -0
  86. package/dist/sitemap-priority.vo.js +6 -0
  87. package/dist/sitemap-priority.vo.js.map +1 -0
  88. package/dist/sitemap-url.vo.d.ts +7 -0
  89. package/dist/sitemap-url.vo.d.ts.map +1 -0
  90. package/dist/sitemap-url.vo.js +7 -0
  91. package/dist/sitemap-url.vo.js.map +1 -0
  92. package/dist/sitemap.handler.d.ts +10 -0
  93. package/dist/sitemap.handler.d.ts.map +1 -0
  94. package/dist/sitemap.handler.js +10 -0
  95. package/dist/sitemap.handler.js.map +1 -0
  96. package/dist/sitemap.service.d.ts +8 -0
  97. package/dist/sitemap.service.d.ts.map +1 -0
  98. package/dist/sitemap.service.js +17 -0
  99. package/dist/sitemap.service.js.map +1 -0
  100. package/package.json +6 -6
  101. package/readme.md +17 -0
  102. package/src/index.ts +17 -0
  103. package/src/job-claimer-sqlite.adapter.ts +3 -2
  104. package/src/job-completer-sqlite.adapter.ts +5 -1
  105. package/src/job-enqueuer-sqlite.adapter.ts +2 -1
  106. package/src/job-failer-sqlite.adapter.ts +4 -1
  107. package/src/job-pruner-collecting.adapter.ts +14 -0
  108. package/src/job-pruner-noop.adapter.ts +8 -0
  109. package/src/job-pruner-sqlite.adapter.ts +23 -0
  110. package/src/job-pruner.port.ts +5 -0
  111. package/src/job-pruner.service.ts +16 -0
  112. package/src/job-queue-sqlite-store.service.ts +2 -1
  113. package/src/job-queue-stats-provider-sqlite.adapter.ts +4 -3
  114. package/src/job-queue.adapter.ts +5 -5
  115. package/src/job-queue.port.ts +1 -1
  116. package/src/job-requeuer-sqlite.adapter.ts +2 -1
  117. package/src/job-status.vo.ts +6 -0
  118. package/src/sitemap-changefreq.vo.ts +9 -0
  119. package/src/sitemap-entries-provider-dynamic.adapter.ts +14 -0
  120. package/src/sitemap-entries-provider-static.adapter.ts +10 -0
  121. package/src/sitemap-entries-provider-with-cache.adapter.ts +32 -0
  122. package/src/sitemap-entries-provider.port.ts +5 -0
  123. package/src/sitemap-entry.vo.ts +28 -0
  124. package/src/sitemap-hono.handler.ts +22 -0
  125. package/src/sitemap-priority.vo.ts +13 -0
  126. package/src/sitemap-url.vo.ts +12 -0
  127. package/src/sitemap.handler.ts +11 -0
  128. package/src/sitemap.service.ts +20 -0
@@ -1,6 +1,7 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import type { GenericJob } from "./job.types";
3
3
  import type { JobFailerPort } from "./job-failer.port";
4
+ import { JobStatusEnum } from "./job-status.vo";
4
5
 
5
6
  type Dependencies = { db: Database };
6
7
 
@@ -8,6 +9,8 @@ export class JobFailerSqliteAdapter implements JobFailerPort {
8
9
  constructor(private readonly deps: Dependencies) {}
9
10
 
10
11
  async fail(id: GenericJob["id"]): Promise<void> {
11
- this.deps.db.run<[GenericJob["id"]]>("UPDATE jobs SET status = 'failed' WHERE id = ?", [id]);
12
+ this.deps.db.run<[GenericJob["id"]]>(`UPDATE jobs SET status = '${JobStatusEnum.failed}' WHERE id = ?`, [
13
+ id,
14
+ ]);
12
15
  }
13
16
  }
@@ -0,0 +1,14 @@
1
+ import * as tools from "@bgord/tools";
2
+ import type { JobPrunerPort } from "./job-pruner.port";
3
+
4
+ export class JobPrunerCollectingAdapter implements JobPrunerPort {
5
+ readonly pruned: Array<[tools.Duration, tools.IntegerNonNegativeType]> = [];
6
+
7
+ async prune(olderThan: tools.Duration): Promise<tools.IntegerNonNegativeType> {
8
+ const count = tools.Int.nonNegative(0);
9
+
10
+ this.pruned.push([olderThan, count]);
11
+
12
+ return count;
13
+ }
14
+ }
@@ -0,0 +1,8 @@
1
+ import * as tools from "@bgord/tools";
2
+ import type { JobPrunerPort } from "./job-pruner.port";
3
+
4
+ export class JobPrunerNoopAdapter implements JobPrunerPort {
5
+ async prune(_olderThan: tools.Duration): Promise<tools.IntegerNonNegativeType> {
6
+ return tools.Int.nonNegative(1);
7
+ }
8
+ }
@@ -0,0 +1,23 @@
1
+ import type { Database } from "bun:sqlite";
2
+ import * as tools from "@bgord/tools";
3
+ import type { ClockPort } from "./clock.port";
4
+ import type { JobPrunerPort } from "./job-pruner.port";
5
+
6
+ type Dependencies = { db: Database; Clock: ClockPort };
7
+
8
+ export class JobPrunerSqliteAdapter implements JobPrunerPort {
9
+ constructor(private readonly deps: Dependencies) {}
10
+
11
+ async prune(olderThan: tools.Duration): Promise<tools.IntegerNonNegativeType> {
12
+ const threshold = this.deps.Clock.now().subtract(olderThan).ms;
13
+
14
+ const result = this.deps.db.run<[tools.TimestampValueType]>(
15
+ `DELETE FROM jobs
16
+ WHERE status IN ('completed', 'failed')
17
+ AND createdAt <= ?`,
18
+ [threshold],
19
+ );
20
+
21
+ return tools.Int.nonNegative(result.changes);
22
+ }
23
+ }
@@ -0,0 +1,5 @@
1
+ import type * as tools from "@bgord/tools";
2
+
3
+ export interface JobPrunerPort {
4
+ prune(olderThan: tools.Duration): Promise<tools.IntegerNonNegativeType>;
5
+ }
@@ -0,0 +1,16 @@
1
+ import type * as tools from "@bgord/tools";
2
+ import type { CronTask } from "./cron-task.vo";
3
+ import type { JobPrunerPort } from "./job-pruner.port";
4
+
5
+ type Config = { label: CronTask["label"]; cron: CronTask["cron"]; olderThan: tools.Duration };
6
+ type Dependencies = { JobPruner: JobPrunerPort };
7
+
8
+ export function JobPrunerService(config: Config, deps: Dependencies): CronTask {
9
+ return {
10
+ label: config.label,
11
+ cron: config.cron,
12
+ handler: async () => {
13
+ await deps.JobPruner.prune(config.olderThan);
14
+ },
15
+ };
16
+ }
@@ -1,4 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
+ import { JobStatusEnum } from "./job-status.vo";
2
3
 
3
4
  type Config = { database: string };
4
5
 
@@ -17,7 +18,7 @@ export class JobQueueSqliteStore {
17
18
  name TEXT NOT NULL,
18
19
  revision INTEGER NOT NULL DEFAULT 0,
19
20
  payload TEXT NOT NULL,
20
- status TEXT NOT NULL DEFAULT 'pending',
21
+ status TEXT NOT NULL DEFAULT '${JobStatusEnum.pending}',
21
22
  claimableAt INTEGER NOT NULL DEFAULT 0
22
23
  )
23
24
  `);
@@ -1,6 +1,7 @@
1
1
  import type { Database } from "bun:sqlite";
2
2
  import * as tools from "@bgord/tools";
3
3
  import type { JobQueueStatsProviderPort, JobQueueStatsSnapshot } from "./job-queue-stats-provider.port";
4
+ import { JobStatusEnum } from "./job-status.vo";
4
5
 
5
6
  type Dependencies = { db: Database };
6
7
 
@@ -14,9 +15,9 @@ export class JobQueueStatsProviderSqliteAdapter implements JobQueueStatsProvider
14
15
  )
15
16
  .all();
16
17
 
17
- const oldest = this.deps.db
18
+ const oldestPending = this.deps.db
18
19
  .query<{ createdAt: number | null }, []>(
19
- "SELECT MIN(createdAt) as createdAt FROM jobs WHERE status = 'pending'",
20
+ `SELECT MIN(createdAt) as createdAt FROM jobs WHERE status = '${JobStatusEnum.pending}'`,
20
21
  )
21
22
  .get();
22
23
 
@@ -27,7 +28,7 @@ export class JobQueueStatsProviderSqliteAdapter implements JobQueueStatsProvider
27
28
  claimed: tools.Int.nonNegative(counts["claimed"] ?? 0),
28
29
  completed: tools.Int.nonNegative(counts["completed"] ?? 0),
29
30
  failed: tools.Int.nonNegative(counts["failed"] ?? 0),
30
- oldestPending: oldest?.createdAt ? tools.Timestamp.fromNumber(oldest.createdAt).ms : null,
31
+ oldestPending: oldestPending?.createdAt ? tools.Timestamp.fromNumber(oldestPending.createdAt).ms : null,
31
32
  };
32
33
  }
33
34
  }
@@ -23,11 +23,11 @@ type Config<Job extends GenericJob> = {
23
23
  export class JobQueueAdapter<Job extends GenericJob> implements JobQueuePort<Job> {
24
24
  constructor(private readonly config: Config<Job>) {}
25
25
 
26
- async enqueue<EnqueuedJob extends Job>(job: EnqueuedJob): Promise<EnqueuedJob> {
27
- const serialized = await this.config.enqueuer.enqueue({
28
- ...job,
29
- payload: this.config.serializer.serialize(job.payload),
30
- });
26
+ async enqueue<EnqueuedJob extends Job>(job: EnqueuedJob, delay?: tools.Duration): Promise<EnqueuedJob> {
27
+ const serialized = await this.config.enqueuer.enqueue(
28
+ { ...job, payload: this.config.serializer.serialize(job.payload) },
29
+ delay,
30
+ );
31
31
 
32
32
  return { ...serialized, payload: this.config.serializer.deserialize(serialized.payload) } as EnqueuedJob;
33
33
  }
@@ -4,7 +4,7 @@ import type { JobHandler } from "./job-registry.port";
4
4
  import type { JobRetryPolicyStrategy } from "./job-retry-policy.strategy";
5
5
 
6
6
  export interface JobDispatcherPort<Job extends GenericJob> {
7
- enqueue<EnqueuedJob extends Job>(job: EnqueuedJob): Promise<EnqueuedJob>;
7
+ enqueue<EnqueuedJob extends Job>(job: EnqueuedJob, delay?: tools.Duration): Promise<EnqueuedJob>;
8
8
  }
9
9
 
10
10
  export interface JobQueuePort<Job extends GenericJob> extends JobDispatcherPort<Job> {
@@ -3,6 +3,7 @@ import type * as tools from "@bgord/tools";
3
3
  import type { ClockPort } from "./clock.port";
4
4
  import type { GenericJob } from "./job.types";
5
5
  import type { JobRequeuerPort } from "./job-requeuer.port";
6
+ import { JobStatusEnum } from "./job-status.vo";
6
7
 
7
8
  type Dependencies = { db: Database; Clock: ClockPort };
8
9
 
@@ -15,7 +16,7 @@ export class JobRequeuerSqliteAdapter implements JobRequeuerPort {
15
16
  delay: tools.Duration,
16
17
  ): Promise<void> {
17
18
  this.deps.db.run<[GenericJob["revision"], tools.TimestampValueType, GenericJob["id"]]>(
18
- "UPDATE jobs SET status = 'pending', revision = ?, claimableAt = ? WHERE id = ?",
19
+ `UPDATE jobs SET status = '${JobStatusEnum.pending}', revision = ?, claimableAt = ? WHERE id = ?`,
19
20
  [revision, this.deps.Clock.now().add(delay).ms, id],
20
21
  );
21
22
  }
@@ -0,0 +1,6 @@
1
+ export enum JobStatusEnum {
2
+ pending = "pending",
3
+ claimed = "claimed",
4
+ completed = "completed",
5
+ failed = "failed",
6
+ }
@@ -0,0 +1,9 @@
1
+ export enum SitemapChangefreqEnum {
2
+ always = "always",
3
+ hourly = "hourly",
4
+ daily = "daily",
5
+ weekly = "weekly",
6
+ monthly = "monthly",
7
+ yearly = "yearly",
8
+ never = "never",
9
+ }
@@ -0,0 +1,14 @@
1
+ import type { SitemapEntriesProvider } from "./sitemap-entries-provider.port";
2
+ import type { SitemapEntry } from "./sitemap-entry.vo";
3
+
4
+ type Config<T> = { fetcher: () => Promise<ReadonlyArray<T>>; mapper: (item: T) => SitemapEntry };
5
+
6
+ export class SitemapEntriesProviderDynamicAdapter<T> implements SitemapEntriesProvider {
7
+ constructor(private readonly config: Config<T>) {}
8
+
9
+ async produce(): Promise<ReadonlyArray<SitemapEntry>> {
10
+ const items = await this.config.fetcher();
11
+
12
+ return items.map(this.config.mapper);
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { SitemapEntriesProvider } from "./sitemap-entries-provider.port";
2
+ import type { SitemapEntry } from "./sitemap-entry.vo";
3
+
4
+ export class SitemapEntriesProviderStaticAdapter implements SitemapEntriesProvider {
5
+ constructor(private readonly entries: ReadonlyArray<SitemapEntry>) {}
6
+
7
+ async produce(): Promise<ReadonlyArray<SitemapEntry>> {
8
+ return this.entries;
9
+ }
10
+ }
@@ -0,0 +1,32 @@
1
+ import type { CacheResolverStrategy } from "./cache-resolver.strategy";
2
+ import type { HashContentStrategy } from "./hash-content.strategy";
3
+ import type { SitemapEntriesProvider } from "./sitemap-entries-provider.port";
4
+ import type { SitemapEntry } from "./sitemap-entry.vo";
5
+ import { SubjectApplicationResolver } from "./subject-application-resolver.vo";
6
+ import { SubjectSegmentFixedStrategy } from "./subject-segment-fixed.strategy";
7
+
8
+ type Config = { id: string; inner: SitemapEntriesProvider };
9
+ type Dependencies = { CacheResolver: CacheResolverStrategy; HashContent: HashContentStrategy };
10
+
11
+ export class SitemapEntriesProviderWithCacheAdapter implements SitemapEntriesProvider {
12
+ constructor(
13
+ private readonly config: Config,
14
+ private readonly deps: Dependencies,
15
+ ) {}
16
+
17
+ async produce(): Promise<ReadonlyArray<SitemapEntry>> {
18
+ const resolver = new SubjectApplicationResolver(
19
+ [
20
+ new SubjectSegmentFixedStrategy("sitemap_entries_provider"),
21
+ new SubjectSegmentFixedStrategy(this.config.id),
22
+ ],
23
+ this.deps,
24
+ );
25
+
26
+ const subject = await resolver.resolve();
27
+
28
+ return this.deps.CacheResolver.resolve<ReadonlyArray<SitemapEntry>>(subject.hex, () =>
29
+ this.config.inner.produce(),
30
+ );
31
+ }
32
+ }
@@ -0,0 +1,5 @@
1
+ import type { SitemapEntry } from "./sitemap-entry.vo";
2
+
3
+ export interface SitemapEntriesProvider {
4
+ produce(): Promise<ReadonlyArray<SitemapEntry>>;
5
+ }
@@ -0,0 +1,28 @@
1
+ import type * as tools from "@bgord/tools";
2
+ import type { SitemapChangefreqEnum } from "./sitemap-changefreq.vo";
3
+ import type { SitemapPriorityType } from "./sitemap-priority.vo";
4
+ import type { SitemapUrlType } from "./sitemap-url.vo";
5
+
6
+ export type SitemapEntryType = {
7
+ loc: SitemapUrlType;
8
+ lastmod?: tools.DayIsoIdType;
9
+ changefreq?: SitemapChangefreqEnum;
10
+ priority?: SitemapPriorityType;
11
+ };
12
+
13
+ export class SitemapEntry {
14
+ constructor(private readonly entry: SitemapEntryType) {}
15
+
16
+ toXml(): string {
17
+ const lastmod = this.entry.lastmod ? `<lastmod>${this.entry.lastmod}</lastmod>` : "";
18
+
19
+ const changefreq = this.entry.changefreq ? `<changefreq>${this.entry.changefreq}</changefreq>` : "";
20
+
21
+ const priority =
22
+ this.entry.priority !== undefined ? `<priority>${this.entry.priority.toFixed(1)}</priority>` : "";
23
+
24
+ const loc = `<loc>${this.entry.loc}</loc>`;
25
+
26
+ return ["<url>", loc, lastmod, changefreq, priority, "</url>"].join("");
27
+ }
28
+ }
@@ -0,0 +1,22 @@
1
+ import { createFactory } from "hono/factory";
2
+ import type { HandlerHonoPort } from "./handler-hono.port";
3
+ import { SitemapHandler, type SitemapHandlerConfig } from "./sitemap.handler";
4
+
5
+ const factory = createFactory();
6
+
7
+ export class SitemapHonoHandler implements HandlerHonoPort {
8
+ private readonly handler: SitemapHandler;
9
+
10
+ constructor(config: SitemapHandlerConfig) {
11
+ this.handler = new SitemapHandler(config);
12
+ }
13
+
14
+ handle() {
15
+ return factory.createHandlers(async (c) => {
16
+ const xml = await this.handler.generate();
17
+
18
+ c.header("Content-Type", "application/xml");
19
+ return c.body(xml, 200);
20
+ });
21
+ }
22
+ }
@@ -0,0 +1,13 @@
1
+ import * as v from "valibot";
2
+
3
+ export const SitemapPriorityError = { Invalid: "sitemap.priority.invalid" };
4
+
5
+ export const SitemapPriority = v.pipe(
6
+ v.number(SitemapPriorityError.Invalid),
7
+ v.minValue(0, SitemapPriorityError.Invalid),
8
+ v.maxValue(1, SitemapPriorityError.Invalid),
9
+ // Stryker disable next-line StringLiteral
10
+ v.brand("SitemapPriority"),
11
+ );
12
+
13
+ export type SitemapPriorityType = v.InferOutput<typeof SitemapPriority>;
@@ -0,0 +1,12 @@
1
+ import * as tools from "@bgord/tools";
2
+ import * as v from "valibot";
3
+
4
+ export const SitemapUrlError = { Invalid: "sitemap.url.invalid" };
5
+
6
+ export const SitemapUrl = v.pipe(
7
+ v.union([tools.UrlWithSlash, tools.UrlWithoutSlash], SitemapUrlError.Invalid),
8
+ // Stryker disable next-line StringLiteral
9
+ v.brand("SitemapUrl"),
10
+ );
11
+
12
+ export type SitemapUrlType = v.InferOutput<typeof SitemapUrl>;
@@ -0,0 +1,11 @@
1
+ import type { Sitemap } from "./sitemap.service";
2
+
3
+ export type SitemapHandlerConfig = { sitemap: Sitemap };
4
+
5
+ export class SitemapHandler {
6
+ constructor(private readonly config: SitemapHandlerConfig) {}
7
+
8
+ async generate(): Promise<string> {
9
+ return this.config.sitemap.toXml();
10
+ }
11
+ }
@@ -0,0 +1,20 @@
1
+ import type { SitemapEntriesProvider } from "./sitemap-entries-provider.port";
2
+
3
+ export type SitemapXml = string;
4
+
5
+ export class Sitemap {
6
+ constructor(private readonly providers: ReadonlyArray<SitemapEntriesProvider>) {}
7
+
8
+ async toXml(): Promise<SitemapXml> {
9
+ const resolved = await Promise.all(this.providers.map((provider) => provider.produce()));
10
+
11
+ const xmls = resolved.flat().map((entry) => entry.toXml());
12
+
13
+ return [
14
+ '<?xml version="1.0" encoding="UTF-8"?>',
15
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
16
+ ...xmls,
17
+ "</urlset>",
18
+ ].join("");
19
+ }
20
+ }