@gravito/constellation 3.1.1 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,19 +13,34 @@ Powerful, high-performance SEO and Sitemap orchestration module for **Gravito ap
13
13
  ## 🌟 Key Features
14
14
 
15
15
  ### 🚀 High Performance & Scalability
16
- - **Streaming Generation**: Uses `SitemapStream` for memory-efficient XML building.
17
- - **Stream Writing (v3.1+)**: Reduces memory peaks by 40%+ with async iterable streaming to storage.
18
- - **Gzip Compression (v3.1+)**: Automatically compress sitemaps to reduce file size by 70%+ and save bandwidth.
16
+ - **🪐 Galaxy-Ready Indexing**: Native integration with PlanetCore for universal sitemap management across all Satellites.
17
+ - **✨ Atomic SEO Deployments**: Shadow-processing engine for "Blue-Green" sitemap swaps, ensuring zero downtime for search crawlers.
18
+ - **Streaming Generation**: Uses `SitemapStream` for memory-efficient XML building with 40%+ memory peak reduction.
19
19
  - **Auto-Sharding**: Automatically splits large sitemaps into multiple files (50,000 URLs limit) and generates sitemap indexes.
20
- - **Async Iterators**: Support for streaming data directly from databases via async generators.
21
- - **Distributed Locking**: Prevents "cache stampedes" in distributed environments (e.g., Kubernetes) using Redis locks.
20
+ - **Distributed Locking**: Plasma-backed Redis locks to prevent "cache stampedes" in multi-node clusters.
22
21
 
23
22
  ### 🏢 Enterprise SEO Orchestration
24
23
  - **Incremental Generation**: Only update modified URLs instead of regenerating the entire sitemap.
25
- - **Shadow Processing**: Atomic "blue-green" deployments for sitemaps using temporary staging and swapping.
26
- - **301/302 Redirect Handling**: Intelligent detection and removal/replacement of redirected URLs to ensure search engines only see canonical links.
24
+ - **301/302 Redirect Handling**: Intelligent detection and removal/replacement of redirected URLs.
27
25
  - **Cloud Storage Integration**: Built-in support for AWS S3 and Google Cloud Storage (GCS).
28
26
 
27
+ ## 🌌 Role in Galaxy Architecture
28
+
29
+ In the **Gravito Galaxy Architecture**, Constellation acts as the **Star Chart (Navigation Layer)**.
30
+
31
+ - **Galaxy Map**: Aggregates the public-facing "Coodinates" (URLs) of all isolated Satellites, creating a unified map for search engines to navigate the entire ecosystem.
32
+ - **Discovery Pulse**: Works with the `Photon` Sensing Layer to identify and index new content as soon as it is manifested by a Satellite.
33
+ - **SEO Orchestrator**: Coordinates the deployment of metadata across multiple domains and environments, ensuring consistent brand visibility.
34
+
35
+ ```mermaid
36
+ graph TD
37
+ S1[Satellite: Blog] -- "URLs" --> Const{Constellation Chart}
38
+ S2[Satellite: Shop] -- "URLs" --> Const
39
+ Const -->|Stream| S3[(S3 Storage)]
40
+ Const -->|Index| Google([Search Engines])
41
+ Const -.->|Lock| Plasma[(Plasma Redis)]
42
+ ```
43
+
29
44
  ### 🛠️ Advanced Capabilities
30
45
  - **Rich Extensions**: Support for Images, Videos, News, and i18n alternate links (hreflang).
31
46
  - **Background Jobs**: Non-blocking generation with persistent progress tracking.
@@ -95,6 +110,14 @@ await sitemap.generate()
95
110
 
96
111
  ---
97
112
 
113
+ ## 📚 Documentation
114
+
115
+ Detailed guides and references for the Galaxy Architecture:
116
+
117
+ - [🏗️ **Architecture Overview**](./README.md) — SEO and Sitemap orchestration engine.
118
+ - [🛰️ **Galaxy SEO**](./doc/GALAXY_SEO.md) — **NEW**: Multi-satellite sitemaps and shadow deployment.
119
+ - [💎 **Advanced Usage**](#-advanced-usage) — Compression, AWS S3, and streaming.
120
+
98
121
  ## 🏗️ Architecture & Modules
99
122
 
100
123
  Constellation is composed of several specialized sub-modules:
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DiskSitemapStorage
3
- } from "./chunk-3IZTXYU7.js";
3
+ } from "./chunk-O3SCFF44.js";
4
4
  export {
5
5
  DiskSitemapStorage
6
6
  };
@@ -1,32 +1,26 @@
1
1
  // src/storage/DiskSitemapStorage.ts
2
2
  import { createReadStream, createWriteStream } from "fs";
3
- import fs from "fs/promises";
4
3
  import path from "path";
5
- import { Readable as Readable2 } from "stream";
4
+ import { Readable } from "stream";
6
5
  import { pipeline } from "stream/promises";
6
+ import { getRuntimeAdapter, runtimeMkdir, runtimeReadText } from "@gravito/core";
7
7
 
8
8
  // src/utils/Compression.ts
9
- import { Readable } from "stream";
10
9
  import { createGzip } from "zlib";
10
+ import { getCompressionAdapter } from "@gravito/core";
11
11
  async function compressToBuffer(source, config) {
12
12
  const level = config?.level ?? 6;
13
13
  if (level < 1 || level > 9) {
14
14
  throw new Error(`Invalid compression level: ${level}. Must be between 1 and 9.`);
15
15
  }
16
- const chunks = [];
17
- const gzip = createGzip({ level });
18
- const readable = Readable.from(source, { encoding: "utf-8" });
19
- try {
20
- readable.pipe(gzip);
21
- for await (const chunk of gzip) {
22
- chunks.push(chunk);
23
- }
24
- return Buffer.concat(chunks);
25
- } catch (error) {
26
- readable.destroy();
27
- gzip.destroy();
28
- throw error;
16
+ const parts = [];
17
+ for await (const chunk of source) {
18
+ parts.push(chunk);
29
19
  }
20
+ const input = new TextEncoder().encode(parts.join(""));
21
+ const adapter = getCompressionAdapter();
22
+ const compressed = await adapter.gzip(input, { level });
23
+ return Buffer.from(compressed);
30
24
  }
31
25
  function createCompressionStream(config) {
32
26
  const level = config?.level ?? 6;
@@ -63,6 +57,7 @@ var DiskSitemapStorage = class {
63
57
  this.outDir = outDir;
64
58
  this.baseUrl = baseUrl;
65
59
  }
60
+ runtime = getRuntimeAdapter();
66
61
  /**
67
62
  * Writes sitemap content to a file on the local disk.
68
63
  *
@@ -71,8 +66,8 @@ var DiskSitemapStorage = class {
71
66
  */
72
67
  async write(filename, content) {
73
68
  const safeName = sanitizeFilename(filename);
74
- await fs.mkdir(this.outDir, { recursive: true });
75
- await fs.writeFile(path.join(this.outDir, safeName), content);
69
+ await runtimeMkdir(this.runtime, this.outDir, { recursive: true });
70
+ await this.runtime.writeFile(path.join(this.outDir, safeName), content);
76
71
  }
77
72
  /**
78
73
  * 使用串流方式寫入 sitemap 檔案,可選擇性啟用 gzip 壓縮。
@@ -86,10 +81,10 @@ var DiskSitemapStorage = class {
86
81
  */
87
82
  async writeStream(filename, stream, options) {
88
83
  const safeName = sanitizeFilename(options?.compress ? toGzipFilename(filename) : filename);
89
- await fs.mkdir(this.outDir, { recursive: true });
84
+ await runtimeMkdir(this.runtime, this.outDir, { recursive: true });
90
85
  const filePath = path.join(this.outDir, safeName);
91
86
  const writeStream = createWriteStream(filePath);
92
- const readable = Readable2.from(stream, { encoding: "utf-8" });
87
+ const readable = Readable.from(stream, { encoding: "utf-8" });
93
88
  if (options?.compress) {
94
89
  const gzip = createCompressionStream();
95
90
  await pipeline(readable, gzip, writeStream);
@@ -106,7 +101,7 @@ var DiskSitemapStorage = class {
106
101
  async read(filename) {
107
102
  try {
108
103
  const safeName = sanitizeFilename(filename);
109
- return await fs.readFile(path.join(this.outDir, safeName), "utf-8");
104
+ return await runtimeReadText(this.runtime, path.join(this.outDir, safeName));
110
105
  } catch {
111
106
  return null;
112
107
  }
@@ -121,7 +116,9 @@ var DiskSitemapStorage = class {
121
116
  try {
122
117
  const safeName = sanitizeFilename(filename);
123
118
  const fullPath = path.join(this.outDir, safeName);
124
- await fs.access(fullPath);
119
+ if (!await this.runtime.exists(fullPath)) {
120
+ return null;
121
+ }
125
122
  const stream = createReadStream(fullPath, { encoding: "utf-8" });
126
123
  return stream;
127
124
  } catch {
@@ -135,13 +132,8 @@ var DiskSitemapStorage = class {
135
132
  * @returns A promise resolving to true if the file exists, false otherwise.
136
133
  */
137
134
  async exists(filename) {
138
- try {
139
- const safeName = sanitizeFilename(filename);
140
- await fs.access(path.join(this.outDir, safeName));
141
- return true;
142
- } catch {
143
- return false;
144
- }
135
+ const safeName = sanitizeFilename(filename);
136
+ return this.runtime.exists(path.join(this.outDir, safeName));
145
137
  }
146
138
  /**
147
139
  * Returns the full public URL for a sitemap file.
package/dist/index.cjs CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -36,20 +35,14 @@ async function compressToBuffer(source, config) {
36
35
  if (level < 1 || level > 9) {
37
36
  throw new Error(`Invalid compression level: ${level}. Must be between 1 and 9.`);
38
37
  }
39
- const chunks = [];
40
- const gzip = (0, import_node_zlib.createGzip)({ level });
41
- const readable = import_node_stream.Readable.from(source, { encoding: "utf-8" });
42
- try {
43
- readable.pipe(gzip);
44
- for await (const chunk of gzip) {
45
- chunks.push(chunk);
46
- }
47
- return Buffer.concat(chunks);
48
- } catch (error) {
49
- readable.destroy();
50
- gzip.destroy();
51
- throw error;
38
+ const parts = [];
39
+ for await (const chunk of source) {
40
+ parts.push(chunk);
52
41
  }
42
+ const input = new TextEncoder().encode(parts.join(""));
43
+ const adapter = (0, import_core.getCompressionAdapter)();
44
+ const compressed = await adapter.gzip(input, { level });
45
+ return Buffer.from(compressed);
53
46
  }
54
47
  function createCompressionStream(config) {
55
48
  const level = config?.level ?? 6;
@@ -64,12 +57,11 @@ function toGzipFilename(filename) {
64
57
  function fromGzipFilename(filename) {
65
58
  return filename.endsWith(".gz") ? filename.slice(0, -3) : filename;
66
59
  }
67
- var import_node_stream, import_node_zlib;
60
+ var import_node_zlib, import_core;
68
61
  var init_Compression = __esm({
69
62
  "src/utils/Compression.ts"() {
70
- "use strict";
71
- import_node_stream = require("stream");
72
63
  import_node_zlib = require("zlib");
64
+ import_core = require("@gravito/core");
73
65
  }
74
66
  });
75
67
 
@@ -93,21 +85,21 @@ function sanitizeFilename(filename) {
93
85
  }
94
86
  return filename;
95
87
  }
96
- var import_node_fs, import_promises, import_node_path, import_node_stream2, import_promises2, DiskSitemapStorage;
88
+ var import_node_fs, import_node_path, import_node_stream, import_promises, import_core4, DiskSitemapStorage;
97
89
  var init_DiskSitemapStorage = __esm({
98
90
  "src/storage/DiskSitemapStorage.ts"() {
99
- "use strict";
100
91
  import_node_fs = require("fs");
101
- import_promises = __toESM(require("fs/promises"), 1);
102
92
  import_node_path = __toESM(require("path"), 1);
103
- import_node_stream2 = require("stream");
104
- import_promises2 = require("stream/promises");
93
+ import_node_stream = require("stream");
94
+ import_promises = require("stream/promises");
95
+ import_core4 = require("@gravito/core");
105
96
  init_Compression();
106
97
  DiskSitemapStorage = class {
107
98
  constructor(outDir, baseUrl) {
108
99
  this.outDir = outDir;
109
100
  this.baseUrl = baseUrl;
110
101
  }
102
+ runtime = (0, import_core4.getRuntimeAdapter)();
111
103
  /**
112
104
  * Writes sitemap content to a file on the local disk.
113
105
  *
@@ -116,8 +108,8 @@ var init_DiskSitemapStorage = __esm({
116
108
  */
117
109
  async write(filename, content) {
118
110
  const safeName = sanitizeFilename(filename);
119
- await import_promises.default.mkdir(this.outDir, { recursive: true });
120
- await import_promises.default.writeFile(import_node_path.default.join(this.outDir, safeName), content);
111
+ await (0, import_core4.runtimeMkdir)(this.runtime, this.outDir, { recursive: true });
112
+ await this.runtime.writeFile(import_node_path.default.join(this.outDir, safeName), content);
121
113
  }
122
114
  /**
123
115
  * 使用串流方式寫入 sitemap 檔案,可選擇性啟用 gzip 壓縮。
@@ -131,15 +123,15 @@ var init_DiskSitemapStorage = __esm({
131
123
  */
132
124
  async writeStream(filename, stream, options) {
133
125
  const safeName = sanitizeFilename(options?.compress ? toGzipFilename(filename) : filename);
134
- await import_promises.default.mkdir(this.outDir, { recursive: true });
126
+ await (0, import_core4.runtimeMkdir)(this.runtime, this.outDir, { recursive: true });
135
127
  const filePath = import_node_path.default.join(this.outDir, safeName);
136
128
  const writeStream = (0, import_node_fs.createWriteStream)(filePath);
137
- const readable = import_node_stream2.Readable.from(stream, { encoding: "utf-8" });
129
+ const readable = import_node_stream.Readable.from(stream, { encoding: "utf-8" });
138
130
  if (options?.compress) {
139
131
  const gzip = createCompressionStream();
140
- await (0, import_promises2.pipeline)(readable, gzip, writeStream);
132
+ await (0, import_promises.pipeline)(readable, gzip, writeStream);
141
133
  } else {
142
- await (0, import_promises2.pipeline)(readable, writeStream);
134
+ await (0, import_promises.pipeline)(readable, writeStream);
143
135
  }
144
136
  }
145
137
  /**
@@ -151,7 +143,7 @@ var init_DiskSitemapStorage = __esm({
151
143
  async read(filename) {
152
144
  try {
153
145
  const safeName = sanitizeFilename(filename);
154
- return await import_promises.default.readFile(import_node_path.default.join(this.outDir, safeName), "utf-8");
146
+ return await (0, import_core4.runtimeReadText)(this.runtime, import_node_path.default.join(this.outDir, safeName));
155
147
  } catch {
156
148
  return null;
157
149
  }
@@ -166,7 +158,9 @@ var init_DiskSitemapStorage = __esm({
166
158
  try {
167
159
  const safeName = sanitizeFilename(filename);
168
160
  const fullPath = import_node_path.default.join(this.outDir, safeName);
169
- await import_promises.default.access(fullPath);
161
+ if (!await this.runtime.exists(fullPath)) {
162
+ return null;
163
+ }
170
164
  const stream = (0, import_node_fs.createReadStream)(fullPath, { encoding: "utf-8" });
171
165
  return stream;
172
166
  } catch {
@@ -180,13 +174,8 @@ var init_DiskSitemapStorage = __esm({
180
174
  * @returns A promise resolving to true if the file exists, false otherwise.
181
175
  */
182
176
  async exists(filename) {
183
- try {
184
- const safeName = sanitizeFilename(filename);
185
- await import_promises.default.access(import_node_path.default.join(this.outDir, safeName));
186
- return true;
187
- } catch {
188
- return false;
189
- }
177
+ const safeName = sanitizeFilename(filename);
178
+ return this.runtime.exists(import_node_path.default.join(this.outDir, safeName));
190
179
  }
191
180
  /**
192
181
  * Returns the full public URL for a sitemap file.
@@ -419,10 +408,6 @@ var RedisChangeTracker = class {
419
408
 
420
409
  // src/core/DiffCalculator.ts
421
410
  var DiffCalculator = class {
422
- batchSize;
423
- constructor(options = {}) {
424
- this.batchSize = options.batchSize || 1e4;
425
- }
426
411
  /**
427
412
  * Calculates the difference between two sets of sitemap entries.
428
413
  *
@@ -633,9 +618,11 @@ var ShadowProcessor = class {
633
618
  };
634
619
 
635
620
  // src/core/SitemapIndex.ts
621
+ var import_core2 = require("@gravito/core");
636
622
  var SitemapIndex = class {
637
623
  options;
638
624
  entries = [];
625
+ escapeHtml = (0, import_core2.getEscapeHtml)();
639
626
  constructor(options) {
640
627
  this.options = { ...options };
641
628
  if (this.options.baseUrl.endsWith("/")) {
@@ -691,7 +678,7 @@ var SitemapIndex = class {
691
678
  loc = baseUrl + loc;
692
679
  }
693
680
  xml += `${indent}<sitemap>${nl}`;
694
- xml += `${subIndent}<loc>${this.escape(loc)}</loc>${nl}`;
681
+ xml += `${subIndent}<loc>${this.escapeHtml(loc)}</loc>${nl}`;
695
682
  if (entry.lastmod) {
696
683
  const date = entry.lastmod instanceof Date ? entry.lastmod : new Date(entry.lastmod);
697
684
  xml += `${subIndent}<lastmod>${date.toISOString().split("T")[0]}</lastmod>${nl}`;
@@ -704,12 +691,10 @@ var SitemapIndex = class {
704
691
  /**
705
692
  * Escapes special XML characters in a string.
706
693
  */
707
- escape(str) {
708
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
709
- }
710
694
  };
711
695
 
712
696
  // src/core/SitemapStream.ts
697
+ var import_core3 = require("@gravito/core");
713
698
  var SitemapStream = class {
714
699
  options;
715
700
  entries = [];
@@ -719,6 +704,7 @@ var SitemapStream = class {
719
704
  hasNews: false,
720
705
  hasAlternates: false
721
706
  };
707
+ escapeHtml = (0, import_core3.getEscapeHtml)();
722
708
  constructor(options) {
723
709
  this.options = { ...options };
724
710
  if (this.options.baseUrl.endsWith("/")) {
@@ -798,8 +784,19 @@ var SitemapStream = class {
798
784
  yield '<?xml version="1.0" encoding="UTF-8"?>\n';
799
785
  yield this.buildUrlsetOpenTag();
800
786
  const { baseUrl, pretty } = this.options;
787
+ let buffer = "";
788
+ let count = 0;
801
789
  for (const entry of this.entries) {
802
- yield this.renderUrl(entry, baseUrl, pretty);
790
+ buffer += this.renderUrl(entry, baseUrl, pretty);
791
+ count++;
792
+ if (count >= 5e3) {
793
+ yield buffer;
794
+ buffer = "";
795
+ count = 0;
796
+ }
797
+ }
798
+ if (buffer) {
799
+ yield buffer;
803
800
  }
804
801
  yield "</urlset>";
805
802
  }
@@ -852,7 +849,7 @@ var SitemapStream = class {
852
849
  loc = baseUrl + loc;
853
850
  }
854
851
  parts.push(`${indent}<url>${nl}`);
855
- parts.push(`${subIndent}<loc>${this.escape(loc)}</loc>${nl}`);
852
+ parts.push(`${subIndent}<loc>${this.escapeHtml(loc)}</loc>${nl}`);
856
853
  if (entry.lastmod) {
857
854
  const date = entry.lastmod instanceof Date ? entry.lastmod : new Date(entry.lastmod);
858
855
  parts.push(`${subIndent}<lastmod>${date.toISOString().split("T")[0]}</lastmod>${nl}`);
@@ -873,7 +870,7 @@ var SitemapStream = class {
873
870
  altLoc = baseUrl + altLoc;
874
871
  }
875
872
  parts.push(
876
- `${subIndent}<xhtml:link rel="alternate" hreflang="${alt.lang}" href="${this.escape(altLoc)}"/>${nl}`
873
+ `${subIndent}<xhtml:link rel="alternate" hreflang="${alt.lang}" href="${this.escapeHtml(altLoc)}"/>${nl}`
877
874
  );
878
875
  }
879
876
  }
@@ -886,7 +883,7 @@ var SitemapStream = class {
886
883
  canonicalUrl = baseUrl + canonicalUrl;
887
884
  }
888
885
  parts.push(
889
- `${subIndent}<xhtml:link rel="canonical" href="${this.escape(canonicalUrl)}"/>${nl}`
886
+ `${subIndent}<xhtml:link rel="canonical" href="${this.escapeHtml(canonicalUrl)}"/>${nl}`
890
887
  );
891
888
  }
892
889
  if (entry.redirect && !entry.redirect.canonical) {
@@ -904,23 +901,23 @@ var SitemapStream = class {
904
901
  loc2 = baseUrl + loc2;
905
902
  }
906
903
  parts.push(`${subIndent}<image:image>${nl}`);
907
- parts.push(`${subIndent} <image:loc>${this.escape(loc2)}</image:loc>${nl}`);
904
+ parts.push(`${subIndent} <image:loc>${this.escapeHtml(loc2)}</image:loc>${nl}`);
908
905
  if (img.title) {
909
- parts.push(`${subIndent} <image:title>${this.escape(img.title)}</image:title>${nl}`);
906
+ parts.push(`${subIndent} <image:title>${this.escapeHtml(img.title)}</image:title>${nl}`);
910
907
  }
911
908
  if (img.caption) {
912
909
  parts.push(
913
- `${subIndent} <image:caption>${this.escape(img.caption)}</image:caption>${nl}`
910
+ `${subIndent} <image:caption>${this.escapeHtml(img.caption)}</image:caption>${nl}`
914
911
  );
915
912
  }
916
913
  if (img.geo_location) {
917
914
  parts.push(
918
- `${subIndent} <image:geo_location>${this.escape(img.geo_location)}</image:geo_location>${nl}`
915
+ `${subIndent} <image:geo_location>${this.escapeHtml(img.geo_location)}</image:geo_location>${nl}`
919
916
  );
920
917
  }
921
918
  if (img.license) {
922
919
  parts.push(
923
- `${subIndent} <image:license>${this.escape(img.license)}</image:license>${nl}`
920
+ `${subIndent} <image:license>${this.escapeHtml(img.license)}</image:license>${nl}`
924
921
  );
925
922
  }
926
923
  parts.push(`${subIndent}</image:image>${nl}`);
@@ -930,20 +927,20 @@ var SitemapStream = class {
930
927
  for (const video of entry.videos) {
931
928
  parts.push(`${subIndent}<video:video>${nl}`);
932
929
  parts.push(
933
- `${subIndent} <video:thumbnail_loc>${this.escape(video.thumbnail_loc)}</video:thumbnail_loc>${nl}`
930
+ `${subIndent} <video:thumbnail_loc>${this.escapeHtml(video.thumbnail_loc)}</video:thumbnail_loc>${nl}`
934
931
  );
935
- parts.push(`${subIndent} <video:title>${this.escape(video.title)}</video:title>${nl}`);
932
+ parts.push(`${subIndent} <video:title>${this.escapeHtml(video.title)}</video:title>${nl}`);
936
933
  parts.push(
937
- `${subIndent} <video:description>${this.escape(video.description)}</video:description>${nl}`
934
+ `${subIndent} <video:description>${this.escapeHtml(video.description)}</video:description>${nl}`
938
935
  );
939
936
  if (video.content_loc) {
940
937
  parts.push(
941
- `${subIndent} <video:content_loc>${this.escape(video.content_loc)}</video:content_loc>${nl}`
938
+ `${subIndent} <video:content_loc>${this.escapeHtml(video.content_loc)}</video:content_loc>${nl}`
942
939
  );
943
940
  }
944
941
  if (video.player_loc) {
945
942
  parts.push(
946
- `${subIndent} <video:player_loc>${this.escape(video.player_loc)}</video:player_loc>${nl}`
943
+ `${subIndent} <video:player_loc>${this.escapeHtml(video.player_loc)}</video:player_loc>${nl}`
947
944
  );
948
945
  }
949
946
  if (video.duration) {
@@ -965,7 +962,7 @@ var SitemapStream = class {
965
962
  }
966
963
  if (video.tag) {
967
964
  for (const tag of video.tag) {
968
- parts.push(`${subIndent} <video:tag>${this.escape(tag)}</video:tag>${nl}`);
965
+ parts.push(`${subIndent} <video:tag>${this.escapeHtml(tag)}</video:tag>${nl}`);
969
966
  }
970
967
  }
971
968
  parts.push(`${subIndent}</video:video>${nl}`);
@@ -975,25 +972,25 @@ var SitemapStream = class {
975
972
  parts.push(`${subIndent}<news:news>${nl}`);
976
973
  parts.push(`${subIndent} <news:publication>${nl}`);
977
974
  parts.push(
978
- `${subIndent} <news:name>${this.escape(entry.news.publication.name)}</news:name>${nl}`
975
+ `${subIndent} <news:name>${this.escapeHtml(entry.news.publication.name)}</news:name>${nl}`
979
976
  );
980
977
  parts.push(
981
- `${subIndent} <news:language>${this.escape(entry.news.publication.language)}</news:language>${nl}`
978
+ `${subIndent} <news:language>${this.escapeHtml(entry.news.publication.language)}</news:language>${nl}`
982
979
  );
983
980
  parts.push(`${subIndent} </news:publication>${nl}`);
984
981
  const pubDate = entry.news.publication_date instanceof Date ? entry.news.publication_date : new Date(entry.news.publication_date);
985
982
  parts.push(
986
983
  `${subIndent} <news:publication_date>${pubDate.toISOString()}</news:publication_date>${nl}`
987
984
  );
988
- parts.push(`${subIndent} <news:title>${this.escape(entry.news.title)}</news:title>${nl}`);
985
+ parts.push(`${subIndent} <news:title>${this.escapeHtml(entry.news.title)}</news:title>${nl}`);
989
986
  if (entry.news.genres) {
990
987
  parts.push(
991
- `${subIndent} <news:genres>${this.escape(entry.news.genres)}</news:genres>${nl}`
988
+ `${subIndent} <news:genres>${this.escapeHtml(entry.news.genres)}</news:genres>${nl}`
992
989
  );
993
990
  }
994
991
  if (entry.news.keywords) {
995
992
  parts.push(
996
- `${subIndent} <news:keywords>${entry.news.keywords.map((k) => this.escape(k)).join(", ")}</news:keywords>${nl}`
993
+ `${subIndent} <news:keywords>${entry.news.keywords.map((k) => this.escapeHtml(k)).join(", ")}</news:keywords>${nl}`
997
994
  );
998
995
  }
999
996
  parts.push(`${subIndent}</news:news>${nl}`);
@@ -1004,9 +1001,6 @@ var SitemapStream = class {
1004
1001
  /**
1005
1002
  * Escapes special XML characters in a string.
1006
1003
  */
1007
- escape(str) {
1008
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1009
- }
1010
1004
  /**
1011
1005
  * Returns all entries currently in the stream.
1012
1006
  *
@@ -1326,7 +1320,6 @@ var SitemapParser = class {
1326
1320
  var IncrementalGenerator = class {
1327
1321
  options;
1328
1322
  changeTracker;
1329
- diffCalculator;
1330
1323
  generator;
1331
1324
  mutex = new Mutex();
1332
1325
  constructor(options) {
@@ -1336,7 +1329,6 @@ var IncrementalGenerator = class {
1336
1329
  ...options
1337
1330
  };
1338
1331
  this.changeTracker = this.options.changeTracker;
1339
- this.diffCalculator = this.options.diffCalculator || new DiffCalculator();
1340
1332
  this.generator = new SitemapGenerator(this.options);
1341
1333
  }
1342
1334
  /**
@@ -1584,6 +1576,7 @@ var ProgressTracker = class {
1584
1576
  console.error("[ProgressTracker] Failed to flush progress:", err);
1585
1577
  });
1586
1578
  }, this.updateInterval);
1579
+ this.updateTimer.unref?.();
1587
1580
  }
1588
1581
  }
1589
1582
  /**
@@ -2041,7 +2034,6 @@ var MemoryLock = class {
2041
2034
  };
2042
2035
 
2043
2036
  // src/locks/RedisLock.ts
2044
- var import_node_crypto = require("crypto");
2045
2037
  var RedisLock = class {
2046
2038
  /**
2047
2039
  * Constructs a new RedisLock instance with the specified configuration.
@@ -2074,7 +2066,7 @@ var RedisLock = class {
2074
2066
  * UUIDs are sufficiently random to prevent lock hijacking across instances.
2075
2067
  * However, they are stored in plain text in Redis (not encrypted).
2076
2068
  */
2077
- lockId = (0, import_node_crypto.randomUUID)();
2069
+ lockId = crypto.randomUUID();
2078
2070
  /**
2079
2071
  * Redis key prefix for all locks acquired through this instance.
2080
2072
  *
@@ -2304,9 +2296,6 @@ var RedisLock = class {
2304
2296
  }
2305
2297
  };
2306
2298
 
2307
- // src/OrbitSitemap.ts
2308
- var import_node_crypto2 = require("crypto");
2309
-
2310
2299
  // src/redirect/RedirectHandler.ts
2311
2300
  var RedirectHandler = class {
2312
2301
  options;
@@ -2321,7 +2310,6 @@ var RedirectHandler = class {
2321
2310
  */
2322
2311
  async processEntries(entries) {
2323
2312
  const { manager, strategy, followChains, maxChainLength } = this.options;
2324
- const _processedEntries = [];
2325
2313
  const redirectMap = /* @__PURE__ */ new Map();
2326
2314
  for (const entry of entries) {
2327
2315
  const redirectTarget = await manager.resolve(entry.url, followChains, maxChainLength);
@@ -2745,7 +2733,7 @@ var OrbitSitemap = class _OrbitSitemap {
2745
2733
  throw new Error("generateAsync() can only be called in static mode");
2746
2734
  }
2747
2735
  const opts = this.options;
2748
- const jobId = (0, import_node_crypto2.randomUUID)();
2736
+ const jobId = crypto.randomUUID();
2749
2737
  let storage = opts.storage;
2750
2738
  if (!storage) {
2751
2739
  const { DiskSitemapStorage: DiskSitemapStorage2 } = await Promise.resolve().then(() => (init_DiskSitemapStorage(), DiskSitemapStorage_exports));
@@ -3002,7 +2990,11 @@ var RedirectDetector = class {
3002
2990
  }
3003
2991
  try {
3004
2992
  const { connection, table, columns } = database;
3005
- const query = `SELECT ${columns.from}, ${columns.to}, ${columns.type} FROM ${table} WHERE ${columns.from} = ? LIMIT 1`;
2993
+ const safeTable = this.assertSafeIdentifier(table);
2994
+ const safeFrom = this.assertSafeIdentifier(columns.from);
2995
+ const safeTo = this.assertSafeIdentifier(columns.to);
2996
+ const safeType = this.assertSafeIdentifier(columns.type);
2997
+ const query = `SELECT ${safeFrom}, ${safeTo}, ${safeType} FROM ${safeTable} WHERE ${safeFrom} = ? LIMIT 1`;
3006
2998
  const results = await connection.query(query, [url]);
3007
2999
  if (results.length === 0) {
3008
3000
  return null;
@@ -3026,8 +3018,8 @@ var RedirectDetector = class {
3026
3018
  return null;
3027
3019
  }
3028
3020
  try {
3029
- const fs2 = await import("fs/promises");
3030
- const data = await fs2.readFile(config.path, "utf-8");
3021
+ const fs = await import("fs/promises");
3022
+ const data = await fs.readFile(config.path, "utf-8");
3031
3023
  const redirects = JSON.parse(data);
3032
3024
  const rule = redirects.find((r) => r.from === url);
3033
3025
  return rule || null;
@@ -3048,6 +3040,7 @@ var RedirectDetector = class {
3048
3040
  const timeout = autoDetect.timeout || 5e3;
3049
3041
  const controller = new AbortController();
3050
3042
  const timeoutId = setTimeout(() => controller.abort(), timeout);
3043
+ timeoutId.unref?.();
3051
3044
  try {
3052
3045
  const response = await fetch(fullUrl, {
3053
3046
  method: "HEAD",
@@ -3089,6 +3082,12 @@ var RedirectDetector = class {
3089
3082
  expires: Date.now() + ttl
3090
3083
  });
3091
3084
  }
3085
+ assertSafeIdentifier(identifier) {
3086
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
3087
+ throw new Error(`Invalid database identifier: ${identifier}`);
3088
+ }
3089
+ return identifier;
3090
+ }
3092
3091
  };
3093
3092
 
3094
3093
  // src/redirect/RedirectManager.ts
@@ -3278,8 +3277,8 @@ var RedisRedirectManager = class {
3278
3277
  init_DiskSitemapStorage();
3279
3278
 
3280
3279
  // src/storage/GCPSitemapStorage.ts
3281
- var import_node_stream3 = require("stream");
3282
- var import_promises3 = require("stream/promises");
3280
+ var import_node_stream2 = require("stream");
3281
+ var import_promises2 = require("stream/promises");
3283
3282
  init_Compression();
3284
3283
  var GCPSitemapStorage = class {
3285
3284
  bucket;
@@ -3357,12 +3356,12 @@ var GCPSitemapStorage = class {
3357
3356
  cacheControl: "public, max-age=3600"
3358
3357
  }
3359
3358
  });
3360
- const readable = import_node_stream3.Readable.from(stream, { encoding: "utf-8" });
3359
+ const readable = import_node_stream2.Readable.from(stream, { encoding: "utf-8" });
3361
3360
  if (options?.compress) {
3362
3361
  const gzip = createCompressionStream();
3363
- await (0, import_promises3.pipeline)(readable, gzip, writeStream);
3362
+ await (0, import_promises2.pipeline)(readable, gzip, writeStream);
3364
3363
  } else {
3365
- await (0, import_promises3.pipeline)(readable, writeStream);
3364
+ await (0, import_promises2.pipeline)(readable, writeStream);
3366
3365
  }
3367
3366
  }
3368
3367
  /**
@@ -3487,7 +3486,6 @@ var GCPSitemapStorage = class {
3487
3486
  });
3488
3487
  for (const shadowFile of shadowFiles) {
3489
3488
  const originalKey = shadowFile.name.replace(/\.shadow\.[^/]+$/, "");
3490
- const _originalFilename = originalKey.replace(prefix, "");
3491
3489
  if (this.shadowMode === "atomic") {
3492
3490
  await shadowFile.copy(bucket.file(originalKey));
3493
3491
  await shadowFile.delete();
@@ -3975,7 +3973,6 @@ var S3SitemapStorage = class {
3975
3973
  continue;
3976
3974
  }
3977
3975
  const originalKey = shadowFile.Key.replace(/\.shadow\.[^/]+$/, "");
3978
- const _originalFilename = originalKey.replace(prefix, "");
3979
3976
  if (this.shadowMode === "atomic") {
3980
3977
  await s3.client.send(
3981
3978
  new s3.CopyObjectCommand({
package/dist/index.d.cts CHANGED
@@ -677,16 +677,6 @@ interface DiffResult {
677
677
  /** URLs that were present in the old set but are missing from the new one. */
678
678
  removed: string[];
679
679
  }
680
- /**
681
- * Options for configuring the `DiffCalculator`.
682
- *
683
- * @public
684
- * @since 3.0.0
685
- */
686
- interface DiffCalculatorOptions {
687
- /** Batch size for processing large datasets. @default 10000 */
688
- batchSize?: number;
689
- }
690
680
  /**
691
681
  * DiffCalculator compares two sets of sitemap entries to identify changes.
692
682
  *
@@ -698,8 +688,6 @@ interface DiffCalculatorOptions {
698
688
  * @since 3.0.0
699
689
  */
700
690
  declare class DiffCalculator {
701
- private batchSize;
702
- constructor(options?: DiffCalculatorOptions);
703
691
  /**
704
692
  * Calculates the difference between two sets of sitemap entries.
705
693
  *
@@ -922,7 +910,6 @@ interface IncrementalGeneratorOptions extends SitemapGeneratorOptions {
922
910
  declare class IncrementalGenerator {
923
911
  private options;
924
912
  private changeTracker;
925
- private diffCalculator;
926
913
  private generator;
927
914
  private mutex;
928
915
  constructor(options: IncrementalGeneratorOptions);
@@ -1063,6 +1050,7 @@ declare class ProgressTracker {
1063
1050
  declare class SitemapIndex {
1064
1051
  private options;
1065
1052
  private entries;
1053
+ private escapeHtml;
1066
1054
  constructor(options: SitemapStreamOptions);
1067
1055
  /**
1068
1056
  * Adds a single entry to the sitemap index.
@@ -1084,10 +1072,6 @@ declare class SitemapIndex {
1084
1072
  * @returns The complete XML string for the sitemap index.
1085
1073
  */
1086
1074
  toXML(): string;
1087
- /**
1088
- * Escapes special XML characters in a string.
1089
- */
1090
- private escape;
1091
1075
  }
1092
1076
 
1093
1077
  /**
@@ -1114,6 +1098,7 @@ declare class SitemapStream {
1114
1098
  private options;
1115
1099
  private entries;
1116
1100
  private flags;
1101
+ private escapeHtml;
1117
1102
  constructor(options: SitemapStreamOptions);
1118
1103
  /**
1119
1104
  * Adds a single entry to the sitemap stream.
@@ -1173,7 +1158,6 @@ declare class SitemapStream {
1173
1158
  /**
1174
1159
  * Escapes special XML characters in a string.
1175
1160
  */
1176
- private escape;
1177
1161
  /**
1178
1162
  * Returns all entries currently in the stream.
1179
1163
  *
@@ -2341,6 +2325,7 @@ declare class RedirectDetector {
2341
2325
  * Caches the detection result for a URL.
2342
2326
  */
2343
2327
  private cacheResult;
2328
+ private assertSafeIdentifier;
2344
2329
  }
2345
2330
 
2346
2331
  /**
@@ -2555,6 +2540,7 @@ declare class RedisRedirectManager implements RedirectManager {
2555
2540
  declare class DiskSitemapStorage implements SitemapStorage {
2556
2541
  private outDir;
2557
2542
  private baseUrl;
2543
+ private runtime;
2558
2544
  constructor(outDir: string, baseUrl: string);
2559
2545
  /**
2560
2546
  * Writes sitemap content to a file on the local disk.
@@ -3065,6 +3051,10 @@ interface CompressionConfig {
3065
3051
  * 將 AsyncIterable<string> 壓縮為 Buffer。
3066
3052
  * 適用於需要完整壓縮結果的場景(如 S3 Upload)。
3067
3053
  *
3054
+ * 使用 RuntimeCompressionAdapter 自動選擇最佳壓縮實作:
3055
+ * - Bun: 原生 C++ 壓縮(2-5x 更快)
3056
+ * - Node.js: node:zlib fallback
3057
+ *
3068
3058
  * @param source - 輸入的字串串流
3069
3059
  * @param config - 壓縮設定
3070
3060
  * @returns 壓縮後的 Buffer
package/dist/index.d.ts CHANGED
@@ -677,16 +677,6 @@ interface DiffResult {
677
677
  /** URLs that were present in the old set but are missing from the new one. */
678
678
  removed: string[];
679
679
  }
680
- /**
681
- * Options for configuring the `DiffCalculator`.
682
- *
683
- * @public
684
- * @since 3.0.0
685
- */
686
- interface DiffCalculatorOptions {
687
- /** Batch size for processing large datasets. @default 10000 */
688
- batchSize?: number;
689
- }
690
680
  /**
691
681
  * DiffCalculator compares two sets of sitemap entries to identify changes.
692
682
  *
@@ -698,8 +688,6 @@ interface DiffCalculatorOptions {
698
688
  * @since 3.0.0
699
689
  */
700
690
  declare class DiffCalculator {
701
- private batchSize;
702
- constructor(options?: DiffCalculatorOptions);
703
691
  /**
704
692
  * Calculates the difference between two sets of sitemap entries.
705
693
  *
@@ -922,7 +910,6 @@ interface IncrementalGeneratorOptions extends SitemapGeneratorOptions {
922
910
  declare class IncrementalGenerator {
923
911
  private options;
924
912
  private changeTracker;
925
- private diffCalculator;
926
913
  private generator;
927
914
  private mutex;
928
915
  constructor(options: IncrementalGeneratorOptions);
@@ -1063,6 +1050,7 @@ declare class ProgressTracker {
1063
1050
  declare class SitemapIndex {
1064
1051
  private options;
1065
1052
  private entries;
1053
+ private escapeHtml;
1066
1054
  constructor(options: SitemapStreamOptions);
1067
1055
  /**
1068
1056
  * Adds a single entry to the sitemap index.
@@ -1084,10 +1072,6 @@ declare class SitemapIndex {
1084
1072
  * @returns The complete XML string for the sitemap index.
1085
1073
  */
1086
1074
  toXML(): string;
1087
- /**
1088
- * Escapes special XML characters in a string.
1089
- */
1090
- private escape;
1091
1075
  }
1092
1076
 
1093
1077
  /**
@@ -1114,6 +1098,7 @@ declare class SitemapStream {
1114
1098
  private options;
1115
1099
  private entries;
1116
1100
  private flags;
1101
+ private escapeHtml;
1117
1102
  constructor(options: SitemapStreamOptions);
1118
1103
  /**
1119
1104
  * Adds a single entry to the sitemap stream.
@@ -1173,7 +1158,6 @@ declare class SitemapStream {
1173
1158
  /**
1174
1159
  * Escapes special XML characters in a string.
1175
1160
  */
1176
- private escape;
1177
1161
  /**
1178
1162
  * Returns all entries currently in the stream.
1179
1163
  *
@@ -2341,6 +2325,7 @@ declare class RedirectDetector {
2341
2325
  * Caches the detection result for a URL.
2342
2326
  */
2343
2327
  private cacheResult;
2328
+ private assertSafeIdentifier;
2344
2329
  }
2345
2330
 
2346
2331
  /**
@@ -2555,6 +2540,7 @@ declare class RedisRedirectManager implements RedirectManager {
2555
2540
  declare class DiskSitemapStorage implements SitemapStorage {
2556
2541
  private outDir;
2557
2542
  private baseUrl;
2543
+ private runtime;
2558
2544
  constructor(outDir: string, baseUrl: string);
2559
2545
  /**
2560
2546
  * Writes sitemap content to a file on the local disk.
@@ -3065,6 +3051,10 @@ interface CompressionConfig {
3065
3051
  * 將 AsyncIterable<string> 壓縮為 Buffer。
3066
3052
  * 適用於需要完整壓縮結果的場景(如 S3 Upload)。
3067
3053
  *
3054
+ * 使用 RuntimeCompressionAdapter 自動選擇最佳壓縮實作:
3055
+ * - Bun: 原生 C++ 壓縮(2-5x 更快)
3056
+ * - Node.js: node:zlib fallback
3057
+ *
3068
3058
  * @param source - 輸入的字串串流
3069
3059
  * @param config - 壓縮設定
3070
3060
  * @returns 壓縮後的 Buffer
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  createCompressionStream,
5
5
  fromGzipFilename,
6
6
  toGzipFilename
7
- } from "./chunk-3IZTXYU7.js";
7
+ } from "./chunk-O3SCFF44.js";
8
8
 
9
9
  // src/core/ChangeTracker.ts
10
10
  var MemoryChangeTracker = class {
@@ -185,10 +185,6 @@ var RedisChangeTracker = class {
185
185
 
186
186
  // src/core/DiffCalculator.ts
187
187
  var DiffCalculator = class {
188
- batchSize;
189
- constructor(options = {}) {
190
- this.batchSize = options.batchSize || 1e4;
191
- }
192
188
  /**
193
189
  * Calculates the difference between two sets of sitemap entries.
194
190
  *
@@ -396,9 +392,11 @@ var ShadowProcessor = class {
396
392
  };
397
393
 
398
394
  // src/core/SitemapIndex.ts
395
+ import { getEscapeHtml } from "@gravito/core";
399
396
  var SitemapIndex = class {
400
397
  options;
401
398
  entries = [];
399
+ escapeHtml = getEscapeHtml();
402
400
  constructor(options) {
403
401
  this.options = { ...options };
404
402
  if (this.options.baseUrl.endsWith("/")) {
@@ -454,7 +452,7 @@ var SitemapIndex = class {
454
452
  loc = baseUrl + loc;
455
453
  }
456
454
  xml += `${indent}<sitemap>${nl}`;
457
- xml += `${subIndent}<loc>${this.escape(loc)}</loc>${nl}`;
455
+ xml += `${subIndent}<loc>${this.escapeHtml(loc)}</loc>${nl}`;
458
456
  if (entry.lastmod) {
459
457
  const date = entry.lastmod instanceof Date ? entry.lastmod : new Date(entry.lastmod);
460
458
  xml += `${subIndent}<lastmod>${date.toISOString().split("T")[0]}</lastmod>${nl}`;
@@ -467,12 +465,10 @@ var SitemapIndex = class {
467
465
  /**
468
466
  * Escapes special XML characters in a string.
469
467
  */
470
- escape(str) {
471
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
472
- }
473
468
  };
474
469
 
475
470
  // src/core/SitemapStream.ts
471
+ import { getEscapeHtml as getEscapeHtml2 } from "@gravito/core";
476
472
  var SitemapStream = class {
477
473
  options;
478
474
  entries = [];
@@ -482,6 +478,7 @@ var SitemapStream = class {
482
478
  hasNews: false,
483
479
  hasAlternates: false
484
480
  };
481
+ escapeHtml = getEscapeHtml2();
485
482
  constructor(options) {
486
483
  this.options = { ...options };
487
484
  if (this.options.baseUrl.endsWith("/")) {
@@ -561,8 +558,19 @@ var SitemapStream = class {
561
558
  yield '<?xml version="1.0" encoding="UTF-8"?>\n';
562
559
  yield this.buildUrlsetOpenTag();
563
560
  const { baseUrl, pretty } = this.options;
561
+ let buffer = "";
562
+ let count = 0;
564
563
  for (const entry of this.entries) {
565
- yield this.renderUrl(entry, baseUrl, pretty);
564
+ buffer += this.renderUrl(entry, baseUrl, pretty);
565
+ count++;
566
+ if (count >= 5e3) {
567
+ yield buffer;
568
+ buffer = "";
569
+ count = 0;
570
+ }
571
+ }
572
+ if (buffer) {
573
+ yield buffer;
566
574
  }
567
575
  yield "</urlset>";
568
576
  }
@@ -615,7 +623,7 @@ var SitemapStream = class {
615
623
  loc = baseUrl + loc;
616
624
  }
617
625
  parts.push(`${indent}<url>${nl}`);
618
- parts.push(`${subIndent}<loc>${this.escape(loc)}</loc>${nl}`);
626
+ parts.push(`${subIndent}<loc>${this.escapeHtml(loc)}</loc>${nl}`);
619
627
  if (entry.lastmod) {
620
628
  const date = entry.lastmod instanceof Date ? entry.lastmod : new Date(entry.lastmod);
621
629
  parts.push(`${subIndent}<lastmod>${date.toISOString().split("T")[0]}</lastmod>${nl}`);
@@ -636,7 +644,7 @@ var SitemapStream = class {
636
644
  altLoc = baseUrl + altLoc;
637
645
  }
638
646
  parts.push(
639
- `${subIndent}<xhtml:link rel="alternate" hreflang="${alt.lang}" href="${this.escape(altLoc)}"/>${nl}`
647
+ `${subIndent}<xhtml:link rel="alternate" hreflang="${alt.lang}" href="${this.escapeHtml(altLoc)}"/>${nl}`
640
648
  );
641
649
  }
642
650
  }
@@ -649,7 +657,7 @@ var SitemapStream = class {
649
657
  canonicalUrl = baseUrl + canonicalUrl;
650
658
  }
651
659
  parts.push(
652
- `${subIndent}<xhtml:link rel="canonical" href="${this.escape(canonicalUrl)}"/>${nl}`
660
+ `${subIndent}<xhtml:link rel="canonical" href="${this.escapeHtml(canonicalUrl)}"/>${nl}`
653
661
  );
654
662
  }
655
663
  if (entry.redirect && !entry.redirect.canonical) {
@@ -667,23 +675,23 @@ var SitemapStream = class {
667
675
  loc2 = baseUrl + loc2;
668
676
  }
669
677
  parts.push(`${subIndent}<image:image>${nl}`);
670
- parts.push(`${subIndent} <image:loc>${this.escape(loc2)}</image:loc>${nl}`);
678
+ parts.push(`${subIndent} <image:loc>${this.escapeHtml(loc2)}</image:loc>${nl}`);
671
679
  if (img.title) {
672
- parts.push(`${subIndent} <image:title>${this.escape(img.title)}</image:title>${nl}`);
680
+ parts.push(`${subIndent} <image:title>${this.escapeHtml(img.title)}</image:title>${nl}`);
673
681
  }
674
682
  if (img.caption) {
675
683
  parts.push(
676
- `${subIndent} <image:caption>${this.escape(img.caption)}</image:caption>${nl}`
684
+ `${subIndent} <image:caption>${this.escapeHtml(img.caption)}</image:caption>${nl}`
677
685
  );
678
686
  }
679
687
  if (img.geo_location) {
680
688
  parts.push(
681
- `${subIndent} <image:geo_location>${this.escape(img.geo_location)}</image:geo_location>${nl}`
689
+ `${subIndent} <image:geo_location>${this.escapeHtml(img.geo_location)}</image:geo_location>${nl}`
682
690
  );
683
691
  }
684
692
  if (img.license) {
685
693
  parts.push(
686
- `${subIndent} <image:license>${this.escape(img.license)}</image:license>${nl}`
694
+ `${subIndent} <image:license>${this.escapeHtml(img.license)}</image:license>${nl}`
687
695
  );
688
696
  }
689
697
  parts.push(`${subIndent}</image:image>${nl}`);
@@ -693,20 +701,20 @@ var SitemapStream = class {
693
701
  for (const video of entry.videos) {
694
702
  parts.push(`${subIndent}<video:video>${nl}`);
695
703
  parts.push(
696
- `${subIndent} <video:thumbnail_loc>${this.escape(video.thumbnail_loc)}</video:thumbnail_loc>${nl}`
704
+ `${subIndent} <video:thumbnail_loc>${this.escapeHtml(video.thumbnail_loc)}</video:thumbnail_loc>${nl}`
697
705
  );
698
- parts.push(`${subIndent} <video:title>${this.escape(video.title)}</video:title>${nl}`);
706
+ parts.push(`${subIndent} <video:title>${this.escapeHtml(video.title)}</video:title>${nl}`);
699
707
  parts.push(
700
- `${subIndent} <video:description>${this.escape(video.description)}</video:description>${nl}`
708
+ `${subIndent} <video:description>${this.escapeHtml(video.description)}</video:description>${nl}`
701
709
  );
702
710
  if (video.content_loc) {
703
711
  parts.push(
704
- `${subIndent} <video:content_loc>${this.escape(video.content_loc)}</video:content_loc>${nl}`
712
+ `${subIndent} <video:content_loc>${this.escapeHtml(video.content_loc)}</video:content_loc>${nl}`
705
713
  );
706
714
  }
707
715
  if (video.player_loc) {
708
716
  parts.push(
709
- `${subIndent} <video:player_loc>${this.escape(video.player_loc)}</video:player_loc>${nl}`
717
+ `${subIndent} <video:player_loc>${this.escapeHtml(video.player_loc)}</video:player_loc>${nl}`
710
718
  );
711
719
  }
712
720
  if (video.duration) {
@@ -728,7 +736,7 @@ var SitemapStream = class {
728
736
  }
729
737
  if (video.tag) {
730
738
  for (const tag of video.tag) {
731
- parts.push(`${subIndent} <video:tag>${this.escape(tag)}</video:tag>${nl}`);
739
+ parts.push(`${subIndent} <video:tag>${this.escapeHtml(tag)}</video:tag>${nl}`);
732
740
  }
733
741
  }
734
742
  parts.push(`${subIndent}</video:video>${nl}`);
@@ -738,25 +746,25 @@ var SitemapStream = class {
738
746
  parts.push(`${subIndent}<news:news>${nl}`);
739
747
  parts.push(`${subIndent} <news:publication>${nl}`);
740
748
  parts.push(
741
- `${subIndent} <news:name>${this.escape(entry.news.publication.name)}</news:name>${nl}`
749
+ `${subIndent} <news:name>${this.escapeHtml(entry.news.publication.name)}</news:name>${nl}`
742
750
  );
743
751
  parts.push(
744
- `${subIndent} <news:language>${this.escape(entry.news.publication.language)}</news:language>${nl}`
752
+ `${subIndent} <news:language>${this.escapeHtml(entry.news.publication.language)}</news:language>${nl}`
745
753
  );
746
754
  parts.push(`${subIndent} </news:publication>${nl}`);
747
755
  const pubDate = entry.news.publication_date instanceof Date ? entry.news.publication_date : new Date(entry.news.publication_date);
748
756
  parts.push(
749
757
  `${subIndent} <news:publication_date>${pubDate.toISOString()}</news:publication_date>${nl}`
750
758
  );
751
- parts.push(`${subIndent} <news:title>${this.escape(entry.news.title)}</news:title>${nl}`);
759
+ parts.push(`${subIndent} <news:title>${this.escapeHtml(entry.news.title)}</news:title>${nl}`);
752
760
  if (entry.news.genres) {
753
761
  parts.push(
754
- `${subIndent} <news:genres>${this.escape(entry.news.genres)}</news:genres>${nl}`
762
+ `${subIndent} <news:genres>${this.escapeHtml(entry.news.genres)}</news:genres>${nl}`
755
763
  );
756
764
  }
757
765
  if (entry.news.keywords) {
758
766
  parts.push(
759
- `${subIndent} <news:keywords>${entry.news.keywords.map((k) => this.escape(k)).join(", ")}</news:keywords>${nl}`
767
+ `${subIndent} <news:keywords>${entry.news.keywords.map((k) => this.escapeHtml(k)).join(", ")}</news:keywords>${nl}`
760
768
  );
761
769
  }
762
770
  parts.push(`${subIndent}</news:news>${nl}`);
@@ -767,9 +775,6 @@ var SitemapStream = class {
767
775
  /**
768
776
  * Escapes special XML characters in a string.
769
777
  */
770
- escape(str) {
771
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
772
- }
773
778
  /**
774
779
  * Returns all entries currently in the stream.
775
780
  *
@@ -1089,7 +1094,6 @@ var SitemapParser = class {
1089
1094
  var IncrementalGenerator = class {
1090
1095
  options;
1091
1096
  changeTracker;
1092
- diffCalculator;
1093
1097
  generator;
1094
1098
  mutex = new Mutex();
1095
1099
  constructor(options) {
@@ -1099,7 +1103,6 @@ var IncrementalGenerator = class {
1099
1103
  ...options
1100
1104
  };
1101
1105
  this.changeTracker = this.options.changeTracker;
1102
- this.diffCalculator = this.options.diffCalculator || new DiffCalculator();
1103
1106
  this.generator = new SitemapGenerator(this.options);
1104
1107
  }
1105
1108
  /**
@@ -1347,6 +1350,7 @@ var ProgressTracker = class {
1347
1350
  console.error("[ProgressTracker] Failed to flush progress:", err);
1348
1351
  });
1349
1352
  }, this.updateInterval);
1353
+ this.updateTimer.unref?.();
1350
1354
  }
1351
1355
  }
1352
1356
  /**
@@ -1804,7 +1808,6 @@ var MemoryLock = class {
1804
1808
  };
1805
1809
 
1806
1810
  // src/locks/RedisLock.ts
1807
- import { randomUUID } from "crypto";
1808
1811
  var RedisLock = class {
1809
1812
  /**
1810
1813
  * Constructs a new RedisLock instance with the specified configuration.
@@ -1837,7 +1840,7 @@ var RedisLock = class {
1837
1840
  * UUIDs are sufficiently random to prevent lock hijacking across instances.
1838
1841
  * However, they are stored in plain text in Redis (not encrypted).
1839
1842
  */
1840
- lockId = randomUUID();
1843
+ lockId = crypto.randomUUID();
1841
1844
  /**
1842
1845
  * Redis key prefix for all locks acquired through this instance.
1843
1846
  *
@@ -2067,9 +2070,6 @@ var RedisLock = class {
2067
2070
  }
2068
2071
  };
2069
2072
 
2070
- // src/OrbitSitemap.ts
2071
- import { randomUUID as randomUUID2 } from "crypto";
2072
-
2073
2073
  // src/redirect/RedirectHandler.ts
2074
2074
  var RedirectHandler = class {
2075
2075
  options;
@@ -2084,7 +2084,6 @@ var RedirectHandler = class {
2084
2084
  */
2085
2085
  async processEntries(entries) {
2086
2086
  const { manager, strategy, followChains, maxChainLength } = this.options;
2087
- const _processedEntries = [];
2088
2087
  const redirectMap = /* @__PURE__ */ new Map();
2089
2088
  for (const entry of entries) {
2090
2089
  const redirectTarget = await manager.resolve(entry.url, followChains, maxChainLength);
@@ -2435,7 +2434,7 @@ var OrbitSitemap = class _OrbitSitemap {
2435
2434
  const opts = this.options;
2436
2435
  let storage = opts.storage;
2437
2436
  if (!storage) {
2438
- const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-VLN5I24C.js");
2437
+ const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-6VXPMKLB.js");
2439
2438
  storage = new DiskSitemapStorage2(opts.outDir, opts.baseUrl);
2440
2439
  }
2441
2440
  let providers = opts.providers;
@@ -2481,7 +2480,7 @@ var OrbitSitemap = class _OrbitSitemap {
2481
2480
  }
2482
2481
  let storage = opts.storage;
2483
2482
  if (!storage) {
2484
- const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-VLN5I24C.js");
2483
+ const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-6VXPMKLB.js");
2485
2484
  storage = new DiskSitemapStorage2(opts.outDir, opts.baseUrl);
2486
2485
  }
2487
2486
  const incrementalGenerator = new IncrementalGenerator({
@@ -2507,10 +2506,10 @@ var OrbitSitemap = class _OrbitSitemap {
2507
2506
  throw new Error("generateAsync() can only be called in static mode");
2508
2507
  }
2509
2508
  const opts = this.options;
2510
- const jobId = randomUUID2();
2509
+ const jobId = crypto.randomUUID();
2511
2510
  let storage = opts.storage;
2512
2511
  if (!storage) {
2513
- const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-VLN5I24C.js");
2512
+ const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-6VXPMKLB.js");
2514
2513
  storage = new DiskSitemapStorage2(opts.outDir, opts.baseUrl);
2515
2514
  }
2516
2515
  let providers = opts.providers;
@@ -2764,7 +2763,11 @@ var RedirectDetector = class {
2764
2763
  }
2765
2764
  try {
2766
2765
  const { connection, table, columns } = database;
2767
- const query = `SELECT ${columns.from}, ${columns.to}, ${columns.type} FROM ${table} WHERE ${columns.from} = ? LIMIT 1`;
2766
+ const safeTable = this.assertSafeIdentifier(table);
2767
+ const safeFrom = this.assertSafeIdentifier(columns.from);
2768
+ const safeTo = this.assertSafeIdentifier(columns.to);
2769
+ const safeType = this.assertSafeIdentifier(columns.type);
2770
+ const query = `SELECT ${safeFrom}, ${safeTo}, ${safeType} FROM ${safeTable} WHERE ${safeFrom} = ? LIMIT 1`;
2768
2771
  const results = await connection.query(query, [url]);
2769
2772
  if (results.length === 0) {
2770
2773
  return null;
@@ -2810,6 +2813,7 @@ var RedirectDetector = class {
2810
2813
  const timeout = autoDetect.timeout || 5e3;
2811
2814
  const controller = new AbortController();
2812
2815
  const timeoutId = setTimeout(() => controller.abort(), timeout);
2816
+ timeoutId.unref?.();
2813
2817
  try {
2814
2818
  const response = await fetch(fullUrl, {
2815
2819
  method: "HEAD",
@@ -2851,6 +2855,12 @@ var RedirectDetector = class {
2851
2855
  expires: Date.now() + ttl
2852
2856
  });
2853
2857
  }
2858
+ assertSafeIdentifier(identifier) {
2859
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(identifier)) {
2860
+ throw new Error(`Invalid database identifier: ${identifier}`);
2861
+ }
2862
+ return identifier;
2863
+ }
2854
2864
  };
2855
2865
 
2856
2866
  // src/redirect/RedirectManager.ts
@@ -3245,7 +3255,6 @@ var GCPSitemapStorage = class {
3245
3255
  });
3246
3256
  for (const shadowFile of shadowFiles) {
3247
3257
  const originalKey = shadowFile.name.replace(/\.shadow\.[^/]+$/, "");
3248
- const _originalFilename = originalKey.replace(prefix, "");
3249
3258
  if (this.shadowMode === "atomic") {
3250
3259
  await shadowFile.copy(bucket.file(originalKey));
3251
3260
  await shadowFile.delete();
@@ -3732,7 +3741,6 @@ var S3SitemapStorage = class {
3732
3741
  continue;
3733
3742
  }
3734
3743
  const originalKey = shadowFile.Key.replace(/\.shadow\.[^/]+$/, "");
3735
- const _originalFilename = originalKey.replace(prefix, "");
3736
3744
  if (this.shadowMode === "atomic") {
3737
3745
  await s3.client.send(
3738
3746
  new s3.CopyObjectCommand({
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@gravito/constellation",
3
- "version": "3.1.1",
3
+ "sideEffects": false,
4
+ "version": "3.1.3",
4
5
  "description": "Powerful sitemap generation for Gravito applications with dynamic/static support, sharding, and caching.",
5
6
  "main": "./dist/index.cjs",
6
7
  "module": "./dist/index.js",
@@ -8,6 +9,7 @@
8
9
  "types": "./dist/index.d.ts",
9
10
  "exports": {
10
11
  ".": {
12
+ "bun": "./dist/index.js",
11
13
  "types": "./dist/index.d.ts",
12
14
  "import": "./dist/index.js",
13
15
  "require": "./dist/index.cjs"
@@ -20,6 +22,7 @@
20
22
  ],
21
23
  "scripts": {
22
24
  "build": "bun run build.ts",
25
+ "build:dts": "bun run build.ts --dts-only",
23
26
  "typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
24
27
  "test": "bun test --timeout=10000",
25
28
  "test:coverage": "bun test --timeout=10000 --coverage --coverage-reporter=lcov --coverage-dir coverage && bun run --bun scripts/check-coverage.ts",
@@ -28,18 +31,18 @@
28
31
  "test:integration": "test $(find tests -name '*.integration.test.ts' 2>/dev/null | wc -l) -gt 0 && find tests -name '*.integration.test.ts' -print0 | xargs -0 bun test --timeout=10000 || echo 'No integration tests found'"
29
32
  },
30
33
  "peerDependencies": {
31
- "@gravito/core": "^1.6.1"
34
+ "@gravito/core": "^2.0.0"
32
35
  },
33
36
  "dependencies": {
34
37
  "@aws-sdk/client-s3": "^3.956.0",
35
38
  "@google-cloud/storage": "^7.18.0",
36
- "@gravito/stream": "^2.0.2",
37
- "@gravito/photon": "^1.0.1"
39
+ "@gravito/stream": "^2.1.1",
40
+ "@gravito/photon": "^1.1.3"
38
41
  },
39
42
  "devDependencies": {
40
43
  "bun-plugin-dts": "^0.3.0",
41
44
  "bun-types": "latest",
42
- "@gravito/core": "^1.6.1",
45
+ "@gravito/core": "^2.0.6",
43
46
  "tsup": "^8.5.1",
44
47
  "typescript": "^5.9.3"
45
48
  },