@gravito/constellation 3.0.1 → 3.1.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/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Job } from '@gravito/stream';
2
2
  import { GravitoOrbit, PlanetCore } from '@gravito/core';
3
+ import { Transform } from 'node:stream';
3
4
 
4
5
  /**
5
6
  * Supported change frequency values for sitemap entries.
@@ -176,11 +177,26 @@ interface SitemapProvider {
176
177
  * @since 3.0.0
177
178
  */
178
179
  interface SitemapStreamOptions {
179
- /** Base domain used to normalize relative URLs. */
180
+ /**
181
+ * Base domain used to normalize relative URLs.
182
+ * Example: 'https://example.com'
183
+ */
180
184
  baseUrl: string;
181
185
  /** Whether to output formatted and indented XML. @default false */
182
186
  pretty?: boolean | undefined;
183
187
  }
188
+ /**
189
+ * Options for stream-based write operations.
190
+ *
191
+ * @public
192
+ * @since 3.1.0
193
+ */
194
+ interface WriteStreamOptions {
195
+ /** Whether to enable gzip compression for the output. @default false */
196
+ compress?: boolean | undefined;
197
+ /** Optional content type override. @default 'application/xml' */
198
+ contentType?: string | undefined;
199
+ }
184
200
  /**
185
201
  * Persistence layer for storing generated sitemap files.
186
202
  *
@@ -198,6 +214,17 @@ interface SitemapStorage {
198
214
  * @param content - The raw XML content.
199
215
  */
200
216
  write(filename: string, content: string): Promise<void>;
217
+ /**
218
+ * Write sitemap content using a streaming approach to reduce memory usage.
219
+ *
220
+ * If implemented, this method will be preferred over `write()` for large sitemaps.
221
+ *
222
+ * @param filename - The destination filename.
223
+ * @param stream - An async iterable that yields XML string chunks.
224
+ * @param options - Optional write configuration including compression.
225
+ * @since 3.1.0
226
+ */
227
+ writeStream?(filename: string, stream: AsyncIterable<string>, options?: WriteStreamOptions): Promise<void>;
201
228
  /**
202
229
  * Read sitemap content from the storage backend.
203
230
  *
@@ -205,6 +232,7 @@ interface SitemapStorage {
205
232
  * @returns The XML content as a string, or null if not found.
206
233
  */
207
234
  read(filename: string): Promise<string | null>;
235
+ readStream?(filename: string): Promise<AsyncIterable<string> | null>;
208
236
  /**
209
237
  * Check if a sitemap file exists in storage.
210
238
  *
@@ -441,6 +469,33 @@ interface RedirectRule {
441
469
  /** Timestamp when the redirect rule was created. */
442
470
  createdAt?: Date;
443
471
  }
472
+ /**
473
+ * Represents a manifest file that tracks URL-to-shard mappings.
474
+ *
475
+ * @public
476
+ * @since 3.0.1
477
+ */
478
+ interface ShardManifest {
479
+ version: number;
480
+ generatedAt: Date | string;
481
+ baseUrl: string;
482
+ maxEntriesPerShard: number;
483
+ sort: string;
484
+ shards: ShardInfo[];
485
+ }
486
+ /**
487
+ * Metadata about a single sitemap shard.
488
+ *
489
+ * @public
490
+ * @since 3.0.1
491
+ */
492
+ interface ShardInfo {
493
+ filename: string;
494
+ from: string;
495
+ to: string;
496
+ count: number;
497
+ lastmod: Date | string;
498
+ }
444
499
  /**
445
500
  * Manager for registering and resolving redirect rules.
446
501
  *
@@ -483,6 +538,20 @@ interface RedirectManager {
483
538
  */
484
539
  resolve(url: string, followChains?: boolean, maxChainLength?: number): Promise<string | null>;
485
540
  }
541
+ /**
542
+ * Compression configuration options.
543
+ *
544
+ * @public
545
+ * @since 3.1.0
546
+ */
547
+ interface CompressionOptions {
548
+ /** Whether compression is enabled. @default false */
549
+ enabled: boolean;
550
+ /** Compression format. @default 'gzip' */
551
+ format?: 'gzip' | undefined;
552
+ /** Compression level (1-9, where 1 is fastest and 9 is best compression). @default 6 */
553
+ level?: number | undefined;
554
+ }
486
555
 
487
556
  /**
488
557
  * Options for configuring the `MemoryChangeTracker`.
@@ -505,11 +574,34 @@ interface MemoryChangeTrackerOptions {
505
574
  */
506
575
  declare class MemoryChangeTracker implements ChangeTracker {
507
576
  private changes;
577
+ private urlIndex;
508
578
  private maxChanges;
509
579
  constructor(options?: MemoryChangeTrackerOptions);
580
+ /**
581
+ * Record a new site structure change in memory.
582
+ *
583
+ * @param change - The change event to record.
584
+ */
510
585
  track(change: SitemapChange): Promise<void>;
586
+ /**
587
+ * Retrieve all changes recorded in memory since a specific time.
588
+ *
589
+ * @param since - Optional start date for the query.
590
+ * @returns An array of change events.
591
+ */
511
592
  getChanges(since?: Date): Promise<SitemapChange[]>;
593
+ /**
594
+ * Retrieve the full change history for a specific URL from memory.
595
+ *
596
+ * @param url - The URL to query history for.
597
+ * @returns An array of change events.
598
+ */
512
599
  getChangesByUrl(url: string): Promise<SitemapChange[]>;
600
+ /**
601
+ * Purge old change records from memory storage.
602
+ *
603
+ * @param since - If provided, only records older than this date will be cleared.
604
+ */
513
605
  clear(since?: Date): Promise<void>;
514
606
  }
515
607
  /**
@@ -543,9 +635,31 @@ declare class RedisChangeTracker implements ChangeTracker {
543
635
  constructor(options: RedisChangeTrackerOptions);
544
636
  private getKey;
545
637
  private getListKey;
638
+ /**
639
+ * Record a new site structure change in Redis.
640
+ *
641
+ * @param change - The change event to record.
642
+ */
546
643
  track(change: SitemapChange): Promise<void>;
644
+ /**
645
+ * Retrieve all changes recorded in Redis since a specific time.
646
+ *
647
+ * @param since - Optional start date for the query.
648
+ * @returns An array of change events.
649
+ */
547
650
  getChanges(since?: Date): Promise<SitemapChange[]>;
651
+ /**
652
+ * Retrieve the full change history for a specific URL from Redis.
653
+ *
654
+ * @param url - The URL to query history for.
655
+ * @returns An array of change events.
656
+ */
548
657
  getChangesByUrl(url: string): Promise<SitemapChange[]>;
658
+ /**
659
+ * Purge old change records from Redis storage.
660
+ *
661
+ * @param since - If provided, only records older than this date will be cleared.
662
+ */
549
663
  clear(since?: Date): Promise<void>;
550
664
  }
551
665
 
@@ -587,19 +701,35 @@ declare class DiffCalculator {
587
701
  private batchSize;
588
702
  constructor(options?: DiffCalculatorOptions);
589
703
  /**
590
- * 計算兩個 sitemap 狀態的差異
704
+ * Calculates the difference between two sets of sitemap entries.
705
+ *
706
+ * @param oldEntries - The previous set of sitemap entries.
707
+ * @param newEntries - The current set of sitemap entries.
708
+ * @returns A DiffResult containing added, updated, and removed entries.
591
709
  */
592
710
  calculate(oldEntries: SitemapEntry[], newEntries: SitemapEntry[]): DiffResult;
593
711
  /**
594
- * 批次計算差異(用於大量 URL)
712
+ * Batch calculates differences for large datasets using async iterables.
713
+ *
714
+ * @param oldEntries - An async iterable of the previous sitemap entries.
715
+ * @param newEntries - An async iterable of the current sitemap entries.
716
+ * @returns A promise resolving to the DiffResult.
595
717
  */
596
718
  calculateBatch(oldEntries: AsyncIterable<SitemapEntry>, newEntries: AsyncIterable<SitemapEntry>): Promise<DiffResult>;
597
719
  /**
598
- * 從變更記錄計算差異
720
+ * Calculates differences based on a sequence of change records.
721
+ *
722
+ * @param baseEntries - The base set of sitemap entries.
723
+ * @param changes - An array of change records to apply to the base set.
724
+ * @returns A DiffResult comparing the base set with the applied changes.
599
725
  */
600
726
  calculateFromChanges(baseEntries: SitemapEntry[], changes: SitemapChange[]): DiffResult;
601
727
  /**
602
- * 檢查 entry 是否有變更
728
+ * Checks if a sitemap entry has changed by comparing its key properties.
729
+ *
730
+ * @param oldEntry - The previous sitemap entry.
731
+ * @param newEntry - The current sitemap entry.
732
+ * @returns True if the entry has changed, false otherwise.
603
733
  */
604
734
  private hasChanged;
605
735
  }
@@ -650,25 +780,34 @@ declare class ShadowProcessor {
650
780
  private options;
651
781
  private shadowId;
652
782
  private operations;
783
+ private mutex;
653
784
  constructor(options: ShadowProcessorOptions);
654
785
  /**
655
- * 添加一個影子操作
786
+ * Adds a single file write operation to the current shadow session.
787
+ *
788
+ * If shadow processing is disabled, the file is written directly to the
789
+ * final destination in storage. Otherwise, it is written to the shadow staging area.
790
+ *
791
+ * @param operation - The shadow write operation details.
656
792
  */
657
793
  addOperation(operation: ShadowOperation): Promise<void>;
658
794
  /**
659
- * 提交所有影子操作
795
+ * Commits all staged shadow operations to the final production location.
796
+ *
797
+ * Depending on the `mode`, this will either perform an atomic swap of all files
798
+ * or commit each file individually (potentially creating new versions).
660
799
  */
661
800
  commit(): Promise<void>;
662
801
  /**
663
- * 取消所有影子操作
802
+ * Cancels all staged shadow operations without committing them.
664
803
  */
665
804
  rollback(): Promise<void>;
666
805
  /**
667
- * 獲取當前影子 ID
806
+ * Returns the unique identifier for the current shadow session.
668
807
  */
669
808
  getShadowId(): string;
670
809
  /**
671
- * 獲取所有操作
810
+ * Returns an array of all staged shadow operations.
672
811
  */
673
812
  getOperations(): ShadowOperation[];
674
813
  }
@@ -688,6 +827,8 @@ interface SitemapGeneratorOptions extends SitemapStreamOptions {
688
827
  maxEntriesPerFile?: number;
689
828
  /** The name of the main sitemap or sitemap index file. @default 'sitemap.xml' */
690
829
  filename?: string;
830
+ /** Whether to generate a shard manifest file. @default false */
831
+ generateManifest?: boolean;
691
832
  /** Configuration for staging files before atomic deployment. */
692
833
  shadow?: {
693
834
  /** Whether shadow processing is enabled. */
@@ -701,6 +842,8 @@ interface SitemapGeneratorOptions extends SitemapStreamOptions {
701
842
  total: number;
702
843
  percentage: number;
703
844
  }) => void;
845
+ /** Compression configuration for reducing sitemap file sizes. @since 3.1.0 */
846
+ compression?: CompressionOptions;
704
847
  }
705
848
  /**
706
849
  * SitemapGenerator is the primary orchestrator for creating sitemaps in Gravito.
@@ -726,9 +869,28 @@ declare class SitemapGenerator {
726
869
  private options;
727
870
  private shadowProcessor;
728
871
  constructor(options: SitemapGeneratorOptions);
872
+ /**
873
+ * Orchestrates the sitemap generation process.
874
+ *
875
+ * This method scans all providers, handles sharding, generates the XML files,
876
+ * and optionally creates a sitemap index and manifest.
877
+ */
729
878
  run(): Promise<void>;
730
879
  /**
731
- * 獲取影子處理器(如果啟用)
880
+ * 統一的 sitemap 寫入方法,優先使用串流寫入以降低記憶體使用。
881
+ *
882
+ * @param stream - SitemapStream 實例
883
+ * @param filename - 檔案名稱(不含 .gz)
884
+ * @returns 實際寫入的檔名(可能包含 .gz)
885
+ * @since 3.1.0
886
+ */
887
+ private writeSitemap;
888
+ /**
889
+ * Normalizes a URL to an absolute URL using the base URL.
890
+ */
891
+ private normalizeUrl;
892
+ /**
893
+ * Returns the shadow processor instance if enabled.
732
894
  */
733
895
  getShadowProcessor(): ShadowProcessor | null;
734
896
  }
@@ -736,25 +898,23 @@ declare class SitemapGenerator {
736
898
  /**
737
899
  * Options for configuring the `IncrementalGenerator`.
738
900
  *
739
- * Extends `SitemapGeneratorOptions` to include change tracking and difference
740
- * calculation components.
741
- *
742
901
  * @public
743
902
  * @since 3.0.0
744
903
  */
745
904
  interface IncrementalGeneratorOptions extends SitemapGeneratorOptions {
746
- /** The change tracker used to store and retrieve sitemap changes. */
905
+ /** The change tracker used to retrieve and record structural site changes. */
747
906
  changeTracker: ChangeTracker;
748
- /** Optional difference calculator. Defaults to a new `DiffCalculator` instance. */
907
+ /** Optional diff calculator to identify added, updated, or removed entries. */
749
908
  diffCalculator?: DiffCalculator;
750
- /** Whether to automatically track changes during full generation. @default true */
909
+ /** Whether to automatically track new entries discovered during full generation. @default true */
751
910
  autoTrack?: boolean;
752
911
  }
753
912
  /**
754
- * IncrementalGenerator optimizes sitemap updates by processing only changed URLs.
913
+ * IncrementalGenerator manages partial updates to the sitemap based on detected changes.
755
914
  *
756
- * Instead of regenerating the entire sitemap from scratch, it uses a `ChangeTracker`
757
- * to identify new, updated, or removed URLs and updates the sitemap incrementally.
915
+ * It provides methods for performing both full and incremental generations,
916
+ * using a `ChangeTracker` to determine which parts of the sitemap need updating.
917
+ * This is particularly efficient for large sites where full regeneration is costly.
758
918
  *
759
919
  * @public
760
920
  * @since 3.0.0
@@ -764,33 +924,51 @@ declare class IncrementalGenerator {
764
924
  private changeTracker;
765
925
  private diffCalculator;
766
926
  private generator;
927
+ private mutex;
767
928
  constructor(options: IncrementalGeneratorOptions);
768
929
  /**
769
- * 生成完整的 sitemap(首次生成)
930
+ * Performs a full sitemap generation and optionally records all entries in the change tracker.
770
931
  */
771
932
  generateFull(): Promise<void>;
772
933
  /**
773
- * 增量生成(只更新變更的部分)
934
+ * Performs an incremental sitemap update based on changes recorded since a specific time.
935
+ *
936
+ * If the number of changes exceeds a certain threshold (e.g., 30% of total URLs),
937
+ * a full generation is triggered instead to ensure consistency.
938
+ *
939
+ * @param since - Optional start date for the incremental update.
774
940
  */
775
941
  generateIncremental(since?: Date): Promise<void>;
776
942
  /**
777
- * 手動追蹤變更
943
+ * Internal implementation of full sitemap generation.
778
944
  */
779
- trackChange(change: SitemapChange): Promise<void>;
945
+ private performFullGeneration;
780
946
  /**
781
- * 獲取變更記錄
947
+ * Internal implementation of incremental sitemap generation.
782
948
  */
783
- getChanges(since?: Date): Promise<SitemapChange[]>;
949
+ private performIncrementalGeneration;
950
+ /**
951
+ * Normalizes a URL to an absolute URL using the base URL.
952
+ */
953
+ private normalizeUrl;
954
+ /**
955
+ * Loads the sitemap shard manifest from storage.
956
+ */
957
+ private loadManifest;
958
+ /**
959
+ * Identifies which shards are affected by the given set of changes.
960
+ */
961
+ private getAffectedShards;
784
962
  /**
785
- * 載入基礎 entries(從現有 sitemap)
963
+ * Updates the affected shards in storage.
786
964
  */
787
- private loadBaseEntries;
965
+ private updateShards;
788
966
  /**
789
- * 生成差異部分
967
+ * Applies changes to a set of sitemap entries and returns the updated, sorted list.
790
968
  */
791
- private generateDiff;
969
+ private applyChanges;
792
970
  /**
793
- * AsyncIterable 轉換為陣列
971
+ * Helper to convert an async iterable into an array.
794
972
  */
795
973
  private toArray;
796
974
  }
@@ -823,31 +1001,44 @@ declare class ProgressTracker {
823
1001
  private updateTimer;
824
1002
  constructor(options: ProgressTrackerOptions);
825
1003
  /**
826
- * 初始化進度追蹤
1004
+ * Initializes progress tracking for a new job.
1005
+ *
1006
+ * @param jobId - Unique identifier for the generation job.
1007
+ * @param total - Total number of entries to be processed.
827
1008
  */
828
1009
  init(jobId: string, total: number): Promise<void>;
829
1010
  /**
830
- * 更新進度
1011
+ * Updates the current progress of the job.
1012
+ *
1013
+ * Updates are debounced and flushed to storage at regular intervals
1014
+ * specified by `updateInterval` to avoid excessive write operations.
1015
+ *
1016
+ * @param processed - Number of entries processed so far.
1017
+ * @param status - Optional new status for the job.
831
1018
  */
832
1019
  update(processed: number, status?: SitemapProgress['status']): Promise<void>;
833
1020
  /**
834
- * 完成進度追蹤
1021
+ * Marks the current job as successfully completed.
835
1022
  */
836
1023
  complete(): Promise<void>;
837
1024
  /**
838
- * 標記為失敗
1025
+ * Marks the current job as failed with an error message.
1026
+ *
1027
+ * @param error - The error message describing why the job failed.
839
1028
  */
840
1029
  fail(error: string): Promise<void>;
841
1030
  /**
842
- * 刷新進度到儲存
1031
+ * Flushes the current progress state to the storage backend.
843
1032
  */
844
1033
  private flush;
845
1034
  /**
846
- * 停止更新計時器
1035
+ * Stops the periodic update timer.
847
1036
  */
848
1037
  private stop;
849
1038
  /**
850
- * 獲取當前進度
1039
+ * Returns a copy of the current progress state.
1040
+ *
1041
+ * @returns The current SitemapProgress object, or null if no job is active.
851
1042
  */
852
1043
  getCurrentProgress(): SitemapProgress | null;
853
1044
  }
@@ -873,9 +1064,29 @@ declare class SitemapIndex {
873
1064
  private options;
874
1065
  private entries;
875
1066
  constructor(options: SitemapStreamOptions);
1067
+ /**
1068
+ * Adds a single entry to the sitemap index.
1069
+ *
1070
+ * @param entry - A sitemap filename or a `SitemapIndexEntry` object.
1071
+ * @returns The `SitemapIndex` instance for chaining.
1072
+ */
876
1073
  add(entry: string | SitemapIndexEntry): this;
1074
+ /**
1075
+ * Adds multiple entries to the sitemap index.
1076
+ *
1077
+ * @param entries - An array of sitemap filenames or `SitemapIndexEntry` objects.
1078
+ * @returns The `SitemapIndex` instance for chaining.
1079
+ */
877
1080
  addAll(entries: (string | SitemapIndexEntry)[]): this;
1081
+ /**
1082
+ * Generates the sitemap index XML content.
1083
+ *
1084
+ * @returns The complete XML string for the sitemap index.
1085
+ */
878
1086
  toXML(): string;
1087
+ /**
1088
+ * Escapes special XML characters in a string.
1089
+ */
879
1090
  private escape;
880
1091
  }
881
1092
 
@@ -902,16 +1113,73 @@ declare class SitemapIndex {
902
1113
  declare class SitemapStream {
903
1114
  private options;
904
1115
  private entries;
1116
+ private flags;
905
1117
  constructor(options: SitemapStreamOptions);
1118
+ /**
1119
+ * Adds a single entry to the sitemap stream.
1120
+ *
1121
+ * @param entry - A URL string or a complete `SitemapEntry` object.
1122
+ * @returns The `SitemapStream` instance for chaining.
1123
+ */
906
1124
  add(entry: string | SitemapEntry): this;
1125
+ /**
1126
+ * Adds multiple entries to the sitemap stream.
1127
+ *
1128
+ * @param entries - An array of URL strings or `SitemapEntry` objects.
1129
+ * @returns The `SitemapStream` instance for chaining.
1130
+ */
907
1131
  addAll(entries: (string | SitemapEntry)[]): this;
1132
+ /**
1133
+ * Generates the sitemap XML content.
1134
+ *
1135
+ * Automatically includes the necessary XML namespaces for images, videos, news,
1136
+ * and internationalization if the entries contain such metadata.
1137
+ *
1138
+ * @returns The complete XML string for the sitemap.
1139
+ */
908
1140
  toXML(): string;
1141
+ /**
1142
+ * 以 AsyncGenerator 方式產生 XML 內容。
1143
+ * 每次 yield 一個邏輯區塊,適合串流寫入場景,可減少記憶體峰值。
1144
+ *
1145
+ * @returns AsyncGenerator 產生 XML 字串片段
1146
+ *
1147
+ * @example
1148
+ * ```typescript
1149
+ * const stream = new SitemapStream({ baseUrl: 'https://example.com' })
1150
+ * stream.add({ url: '/page1' })
1151
+ * stream.add({ url: '/page2' })
1152
+ *
1153
+ * for await (const chunk of stream.toAsyncIterable()) {
1154
+ * process.stdout.write(chunk)
1155
+ * }
1156
+ * ```
1157
+ *
1158
+ * @since 3.1.0
1159
+ */
1160
+ toAsyncIterable(): AsyncGenerator<string, void, unknown>;
1161
+ /**
1162
+ * 同步版本的 iterable,供 toXML() 使用。
1163
+ */
1164
+ private toSyncIterable;
1165
+ /**
1166
+ * 建立 urlset 開標籤與所有必要的 XML 命名空間。
1167
+ */
1168
+ private buildUrlsetOpenTag;
1169
+ /**
1170
+ * Renders a single sitemap entry into its XML representation.
1171
+ */
909
1172
  private renderUrl;
910
- private hasImages;
911
- private hasVideos;
912
- private hasNews;
913
- private hasAlternates;
1173
+ /**
1174
+ * Escapes special XML characters in a string.
1175
+ */
914
1176
  private escape;
1177
+ /**
1178
+ * Returns all entries currently in the stream.
1179
+ *
1180
+ * @returns An array of `SitemapEntry` objects.
1181
+ */
1182
+ getEntries(): SitemapEntry[];
915
1183
  }
916
1184
 
917
1185
  /**
@@ -922,10 +1190,18 @@ type I18nSitemapEntryOptions = Omit<SitemapEntry, 'url' | 'alternates'>;
922
1190
  /**
923
1191
  * Generate fully cross-referenced SitemapEntries for multiple locales.
924
1192
  *
925
- * @param path The path relative to the locale prefix (e.g. '/docs/intro')
926
- * @param locales List of supported locales (e.g. ['en', 'zh', 'jp'])
927
- * @param baseUrl The domain root (e.g. 'https://gravito.dev'). IF provided, URLs will be absolute. If not, they remain relative paths but include the locale prefix.
928
- * @param options Additional SitemapEntry options (lastmod, priority, etc.)
1193
+ * This helper creates a set of sitemap entries where each entry represents a
1194
+ * specific locale version of a page, and all entries are linked via `xhtml:link`
1195
+ * alternate tags to satisfy Google's internationalization requirements.
1196
+ *
1197
+ * @param path - The path relative to the locale prefix (e.g., '/docs/intro').
1198
+ * @param locales - List of supported language/region codes (e.g., ['en', 'zh-TW']).
1199
+ * @param baseUrl - The domain root (e.g., 'https://example.com').
1200
+ * @param options - Additional SitemapEntry properties like `lastmod` or `priority`.
1201
+ * @returns An array of interconnected `SitemapEntry` objects.
1202
+ *
1203
+ * @public
1204
+ * @since 3.0.0
929
1205
  */
930
1206
  declare function generateI18nEntries(path: string, locales: string[], baseUrl?: string, options?: I18nSitemapEntryOptions): SitemapEntry[];
931
1207
 
@@ -971,154 +1247,880 @@ declare class GenerateSitemapJob extends Job {
971
1247
  private totalEntries;
972
1248
  private processedEntries;
973
1249
  constructor(options: GenerateSitemapJobOptions);
1250
+ /**
1251
+ * Main entry point for the job execution.
1252
+ *
1253
+ * Orchestrates the full lifecycle of sitemap generation, including progress
1254
+ * initialization, generation, shadow commit, and error handling.
1255
+ */
974
1256
  handle(): Promise<void>;
975
1257
  /**
976
- * 計算總 URL
1258
+ * Calculates the total number of URL entries from all providers.
1259
+ *
1260
+ * @returns A promise resolving to the total entry count.
977
1261
  */
978
1262
  private calculateTotal;
979
1263
  /**
980
- * 帶進度追蹤的生成
1264
+ * Performs sitemap generation while reporting progress to the tracker and callback.
981
1265
  */
982
1266
  private generateWithProgress;
983
1267
  }
984
1268
 
985
1269
  /**
986
- * Configuration for a dynamically generated sitemap that is built upon request.
987
- *
988
- * Useful for small to medium sites where fresh data is critical. Dynamic sitemaps
989
- * are generated on-the-fly and can be cached at the HTTP level.
990
- *
991
- * @public
992
- * @since 3.0.0
993
- */
994
- interface DynamicSitemapOptions extends SitemapStreamOptions {
995
- /** The URL path where the sitemap will be exposed. @default '/sitemap.xml' */
996
- path?: string | undefined;
997
- /** List of sitemap entry providers to scan for content. */
998
- providers: SitemapProvider[];
999
- /** Cache duration in seconds for the HTTP response. @default undefined (no-cache) */
1000
- cacheSeconds?: number | undefined;
1001
- /** Persistence backend for cached XML files. Defaults to MemorySitemapStorage. */
1002
- storage?: SitemapStorage | undefined;
1003
- /** Optional distributed lock to prevent "cache stampede" during heavy generation. */
1004
- lock?: SitemapLock | undefined;
1005
- }
1006
- /**
1007
- * Configuration for a statically pre-generated sitemap.
1270
+ * In-memory lock implementation for single-instance sitemap generation.
1008
1271
  *
1009
- * Recommended for large sites or high-traffic applications. Static sitemaps
1010
- * are built (usually as part of a build process) and served as static files.
1272
+ * Provides mutex-style mutual exclusion within a single Node.js process using a Map-based
1273
+ * storage mechanism. Designed for development environments and single-instance deployments
1274
+ * where distributed coordination is not required.
1011
1275
  *
1012
- * @public
1013
- * @since 3.0.0
1014
- */
1015
- interface StaticSitemapOptions extends SitemapStreamOptions {
1016
- /** Local directory where generated XML files will be saved. */
1017
- outDir: string;
1018
- /** The name of the root sitemap file. @default 'sitemap.xml' */
1019
- filename?: string | undefined;
1020
- /** List of sitemap entry providers to scan for content. */
1021
- providers: SitemapProvider[];
1022
- /** Custom storage backend. Defaults to DiskSitemapStorage using `outDir`. */
1023
- storage?: SitemapStorage | undefined;
1024
- /** Optional incremental generation settings to only update changed URLs. */
1025
- incremental?: {
1026
- /** Whether to enable incremental builds. */
1027
- enabled: boolean;
1028
- /** Backend for tracking structural site changes. */
1029
- changeTracker: ChangeTracker;
1030
- /** Whether to automatically record new changes during scan. @default false */
1031
- autoTrack?: boolean;
1032
- };
1033
- /** Optional SEO redirect orchestration settings. */
1034
- redirect?: {
1035
- /** Whether to automatically handle 301/302 redirects found in sitemap. */
1036
- enabled: boolean;
1037
- /** Backend for managing and resolving redirect rules. */
1038
- manager: RedirectManager;
1039
- /** Strategy for resolving conflicting or chained redirects. @default 'remove_old_add_new' */
1040
- strategy?: 'remove_old_add_new' | 'keep_relation' | 'update_url' | 'dual_mark';
1041
- /** Whether to resolve redirect chains into a single jump. @default false */
1042
- followChains?: boolean;
1043
- /** Maximum number of redirect jumps to follow. @default 5 */
1044
- maxChainLength?: number;
1045
- };
1046
- /** Deployment settings for atomic sitemap updates via shadow staging. */
1047
- shadow?: {
1048
- /** Whether to enable "shadow" (atomic staging) mode. */
1049
- enabled: boolean;
1050
- /** The update strategy: 'atomic' (full swap) or 'versioned' (archived). @default 'atomic' */
1051
- mode: 'atomic' | 'versioned';
1052
- };
1053
- /** Persistence backend for long-running job progress tracking. */
1054
- progressStorage?: SitemapProgressStorage;
1055
- }
1056
- /**
1057
- * OrbitSitemap is the enterprise SEO orchestration module for Gravito.
1276
+ * **When to use:**
1277
+ * - Development and testing environments
1278
+ * - Single-instance production deployments (e.g., single Docker container)
1279
+ * - Scenarios where Redis infrastructure is unavailable
1058
1280
  *
1059
- * It provides advanced sitemap generation supporting large-scale indexing,
1060
- * automatic 301/302 redirect handling, and atomic sitemap deployments.
1281
+ * **When NOT to use:**
1282
+ * - Kubernetes or multi-instance deployments (use {@link RedisLock} instead)
1283
+ * - Horizontally scaled applications
1284
+ * - Any environment requiring cross-process synchronization
1061
1285
  *
1062
- * It can operate in two modes:
1063
- * 1. **Dynamic**: Sitemaps are generated on-the-fly and cached.
1064
- * 2. **Static**: Sitemaps are pre-built (e.g., during CI/CD) and served from disk or cloud storage.
1286
+ * **Design rationale:**
1287
+ * - Zero external dependencies (no Redis, no database)
1288
+ * - Automatic TTL-based lock expiration to prevent deadlocks
1289
+ * - Synchronous cleanup of expired locks during read operations
1290
+ * - Fast in-process performance with O(1) lock operations
1065
1291
  *
1066
- * @example Dynamic Mode
1292
+ * @example Basic usage with try-finally pattern
1067
1293
  * ```typescript
1068
- * const sitemap = OrbitSitemap.dynamic({
1069
- * baseUrl: 'https://example.com',
1070
- * providers: [new PostSitemapProvider()]
1071
- * });
1072
- * core.addOrbit(sitemap);
1294
+ * import { MemoryLock } from '@gravito/constellation'
1295
+ *
1296
+ * const lock = new MemoryLock()
1297
+ *
1298
+ * // Attempt to acquire lock with 60-second TTL
1299
+ * const acquired = await lock.acquire('sitemap-generation', 60000)
1300
+ * if (acquired) {
1301
+ * try {
1302
+ * // Perform exclusive operation
1303
+ * await generateSitemap()
1304
+ * } finally {
1305
+ * // Always release lock to prevent resource leakage
1306
+ * await lock.release('sitemap-generation')
1307
+ * }
1308
+ * } else {
1309
+ * console.log('Another process is generating sitemap, skipping')
1310
+ * }
1073
1311
  * ```
1074
1312
  *
1075
- * @example Static Mode
1313
+ * @example Handling lock contention
1076
1314
  * ```typescript
1077
- * const sitemap = OrbitSitemap.static({
1078
- * baseUrl: 'https://example.com',
1079
- * outDir: './public',
1080
- * shadow: { enabled: true, mode: 'atomic' }
1081
- * });
1082
- * await sitemap.generate();
1315
+ * const lock = new MemoryLock()
1316
+ *
1317
+ * if (!await lock.acquire('expensive-operation', 30000)) {
1318
+ * return new Response('Service busy, try again later', {
1319
+ * status: 503,
1320
+ * headers: { 'Retry-After': '30' }
1321
+ * })
1322
+ * }
1083
1323
  * ```
1084
1324
  *
1085
1325
  * @public
1086
- * @since 3.0.0
1326
+ * @since 3.1.0
1087
1327
  */
1088
- declare class OrbitSitemap implements GravitoOrbit {
1089
- private options;
1090
- private mode;
1091
- private constructor();
1328
+ declare class MemoryLock implements SitemapLock {
1092
1329
  /**
1093
- * Create a dynamic sitemap configuration.
1330
+ * Internal map storing resource identifiers to their lock expiration timestamps.
1094
1331
  *
1095
- * @param options - The dynamic sitemap options.
1096
- * @returns An OrbitSitemap instance configured for dynamic generation.
1332
+ * Keys represent unique resource identifiers (e.g., 'sitemap-generation').
1333
+ * Values are Unix timestamps in milliseconds representing when the lock expires.
1334
+ * Expired locks are automatically cleaned up during `acquire()` and `isLocked()` calls.
1097
1335
  */
1098
- static dynamic(options: DynamicSitemapOptions): OrbitSitemap;
1336
+ private locks;
1099
1337
  /**
1100
- * Create a static sitemap configuration.
1338
+ * Attempts to acquire an exclusive lock on the specified resource.
1101
1339
  *
1102
- * @param options - The static sitemap options.
1103
- * @returns An OrbitSitemap instance configured for static generation.
1340
+ * Uses a test-and-set approach: checks if the lock exists and is valid, then atomically
1341
+ * sets the lock if available. Expired locks are treated as available and automatically
1342
+ * replaced during acquisition.
1343
+ *
1344
+ * **Behavior:**
1345
+ * - Returns `true` if lock was successfully acquired
1346
+ * - Returns `false` if resource is already locked by another caller
1347
+ * - Automatically replaces expired locks (acts as self-healing mechanism)
1348
+ * - Lock automatically expires after TTL milliseconds
1349
+ *
1350
+ * **Race condition handling:**
1351
+ * Safe within a single process due to JavaScript's single-threaded event loop.
1352
+ * NOT safe across multiple processes or instances (use RedisLock for that).
1353
+ *
1354
+ * @param resource - Unique identifier for the resource to lock (e.g., 'sitemap-generation', 'blog-index').
1355
+ * Should be consistent across all callers attempting to lock the same resource.
1356
+ * @param ttl - Time-to-live in milliseconds. Lock automatically expires after this duration.
1357
+ * Recommended: 2-5x the expected operation duration to prevent premature expiration.
1358
+ * @returns Promise resolving to `true` if lock acquired, `false` if already locked.
1359
+ *
1360
+ * @example Preventing concurrent sitemap generation
1361
+ * ```typescript
1362
+ * const lock = new MemoryLock()
1363
+ * const acquired = await lock.acquire('sitemap-generation', 60000)
1364
+ *
1365
+ * if (!acquired) {
1366
+ * console.log('Another process is already generating the sitemap')
1367
+ * return new Response('Generation in progress', { status: 503 })
1368
+ * }
1369
+ *
1370
+ * try {
1371
+ * await generateSitemap()
1372
+ * } finally {
1373
+ * await lock.release('sitemap-generation')
1374
+ * }
1375
+ * ```
1376
+ *
1377
+ * @example Setting appropriate TTL
1378
+ * ```typescript
1379
+ * // For fast operations (< 1 second), use short TTL
1380
+ * await lock.acquire('cache-refresh', 5000)
1381
+ *
1382
+ * // For slow operations (minutes), use longer TTL
1383
+ * await lock.acquire('full-reindex', 300000) // 5 minutes
1384
+ * ```
1104
1385
  */
1105
- static static(options: StaticSitemapOptions): OrbitSitemap;
1386
+ acquire(resource: string, ttl: number): Promise<boolean>;
1106
1387
  /**
1107
- * Install the sitemap module into PlanetCore.
1388
+ * Releases the lock on the specified resource, allowing others to acquire it.
1108
1389
  *
1109
- * @param core - The PlanetCore instance.
1390
+ * Immediately removes the lock from memory without any ownership validation.
1391
+ * Unlike RedisLock, this does NOT verify that the caller is the lock owner,
1392
+ * so callers must ensure they only release locks they acquired.
1393
+ *
1394
+ * **Best practices:**
1395
+ * - Always call `release()` in a `finally` block to prevent lock leakage
1396
+ * - Only release locks you successfully acquired
1397
+ * - If operation fails, still release the lock to allow retry
1398
+ *
1399
+ * **Idempotency:**
1400
+ * Safe to call multiple times on the same resource. Releasing a non-existent
1401
+ * lock is a no-op.
1402
+ *
1403
+ * @param resource - The resource identifier to unlock. Must match the identifier
1404
+ * used in the corresponding `acquire()` call.
1405
+ *
1406
+ * @example Proper release pattern with try-finally
1407
+ * ```typescript
1408
+ * const acquired = await lock.acquire('sitemap-generation', 60000)
1409
+ * if (!acquired) return
1410
+ *
1411
+ * try {
1412
+ * await generateSitemap()
1413
+ * } finally {
1414
+ * // Always release, even if operation throws
1415
+ * await lock.release('sitemap-generation')
1416
+ * }
1417
+ * ```
1418
+ *
1419
+ * @example Handling operation failures
1420
+ * ```typescript
1421
+ * const acquired = await lock.acquire('data-import', 120000)
1422
+ * if (!acquired) return
1423
+ *
1424
+ * try {
1425
+ * await importData()
1426
+ * } catch (error) {
1427
+ * console.error('Import failed:', error)
1428
+ * // Lock still released in finally block
1429
+ * throw error
1430
+ * } finally {
1431
+ * await lock.release('data-import')
1432
+ * }
1433
+ * ```
1434
+ */
1435
+ release(resource: string): Promise<void>;
1436
+ /**
1437
+ * Checks whether a resource is currently locked and has not expired.
1438
+ *
1439
+ * Performs automatic cleanup by removing expired locks during the check,
1440
+ * ensuring the internal map doesn't accumulate stale entries over time.
1441
+ *
1442
+ * **Use cases:**
1443
+ * - Pre-flight checks before attempting expensive operations
1444
+ * - Status monitoring and health checks
1445
+ * - Implementing custom retry logic
1446
+ * - Debugging and testing
1447
+ *
1448
+ * **Side effects:**
1449
+ * Automatically deletes expired locks as a garbage collection mechanism.
1450
+ * This is intentional to prevent memory leaks from abandoned locks.
1451
+ *
1452
+ * @param resource - The resource identifier to check for lock status.
1453
+ * @returns Promise resolving to `true` if resource is actively locked (not expired),
1454
+ * `false` if unlocked or lock has expired.
1455
+ *
1456
+ * @example Pre-flight check before starting work
1457
+ * ```typescript
1458
+ * const lock = new MemoryLock()
1459
+ *
1460
+ * if (await lock.isLocked('sitemap-generation')) {
1461
+ * console.log('Sitemap generation already in progress')
1462
+ * return
1463
+ * }
1464
+ *
1465
+ * // Safe to proceed
1466
+ * await lock.acquire('sitemap-generation', 60000)
1467
+ * ```
1468
+ *
1469
+ * @example Health check endpoint
1470
+ * ```typescript
1471
+ * app.get('/health/locks', async (c) => {
1472
+ * const isGenerating = await lock.isLocked('sitemap-generation')
1473
+ * const isIndexing = await lock.isLocked('search-indexing')
1474
+ *
1475
+ * return c.json({
1476
+ * sitemapGeneration: isGenerating ? 'in-progress' : 'idle',
1477
+ * searchIndexing: isIndexing ? 'in-progress' : 'idle'
1478
+ * })
1479
+ * })
1480
+ * ```
1481
+ *
1482
+ * @example Custom retry logic
1483
+ * ```typescript
1484
+ * let attempts = 0
1485
+ * while (attempts < 5) {
1486
+ * if (!await lock.isLocked('resource')) {
1487
+ * const acquired = await lock.acquire('resource', 10000)
1488
+ * if (acquired) break
1489
+ * }
1490
+ * await sleep(1000)
1491
+ * attempts++
1492
+ * }
1493
+ * ```
1494
+ */
1495
+ isLocked(resource: string): Promise<boolean>;
1496
+ /**
1497
+ * Clears all locks from memory, including both active and expired locks.
1498
+ *
1499
+ * **Use cases:**
1500
+ * - Test cleanup between test cases to ensure isolation
1501
+ * - Application shutdown to release all resources
1502
+ * - Manual intervention during debugging
1503
+ * - Resetting state after catastrophic errors
1504
+ *
1505
+ * **Warning:**
1506
+ * This forcibly releases ALL locks without any ownership validation.
1507
+ * Should not be called during normal operation in production environments.
1508
+ *
1509
+ * @example Test cleanup with beforeEach hook
1510
+ * ```typescript
1511
+ * import { describe, beforeEach, test } from 'vitest'
1512
+ *
1513
+ * const lock = new MemoryLock()
1514
+ *
1515
+ * beforeEach(async () => {
1516
+ * await lock.clear() // Ensure clean state for each test
1517
+ * })
1518
+ *
1519
+ * test('lock acquisition', async () => {
1520
+ * const acquired = await lock.acquire('test-resource', 5000)
1521
+ * expect(acquired).toBe(true)
1522
+ * })
1523
+ * ```
1524
+ *
1525
+ * @example Graceful shutdown handler
1526
+ * ```typescript
1527
+ * process.on('SIGTERM', async () => {
1528
+ * console.log('Shutting down, releasing all locks...')
1529
+ * await lock.clear()
1530
+ * process.exit(0)
1531
+ * })
1532
+ * ```
1533
+ */
1534
+ clear(): Promise<void>;
1535
+ /**
1536
+ * Returns the number of lock entries currently stored in memory.
1537
+ *
1538
+ * **Important:** This includes BOTH active and expired locks. Expired locks
1539
+ * are only cleaned up during `acquire()` or `isLocked()` calls, so this count
1540
+ * may include stale entries.
1541
+ *
1542
+ * **Use cases:**
1543
+ * - Monitoring memory usage and lock accumulation
1544
+ * - Debugging lock leakage issues
1545
+ * - Testing lock lifecycle behavior
1546
+ * - Detecting abnormal lock retention patterns
1547
+ *
1548
+ * **Not suitable for:**
1549
+ * - Determining number of ACTIVE locks (use `isLocked()` on each resource)
1550
+ * - Production health checks (includes expired locks)
1551
+ *
1552
+ * @returns The total number of lock entries in the internal Map, including expired ones.
1553
+ *
1554
+ * @example Monitoring lock accumulation
1555
+ * ```typescript
1556
+ * const lock = new MemoryLock()
1557
+ *
1558
+ * setInterval(() => {
1559
+ * const count = lock.size()
1560
+ * if (count > 100) {
1561
+ * console.warn(`High lock count detected: ${count}`)
1562
+ * // May indicate lock leakage or missing release() calls
1563
+ * }
1564
+ * }, 60000)
1565
+ * ```
1566
+ *
1567
+ * @example Testing lock cleanup behavior
1568
+ * ```typescript
1569
+ * import { test, expect } from 'vitest'
1570
+ *
1571
+ * test('expired locks are cleaned up', async () => {
1572
+ * const lock = new MemoryLock()
1573
+ *
1574
+ * await lock.acquire('resource', 10)
1575
+ * expect(lock.size()).toBe(1)
1576
+ *
1577
+ * await sleep(20) // Wait for expiration
1578
+ * expect(lock.size()).toBe(1) // Still in map (not cleaned yet)
1579
+ *
1580
+ * await lock.isLocked('resource') // Triggers cleanup
1581
+ * expect(lock.size()).toBe(0) // Now removed
1582
+ * })
1583
+ * ```
1584
+ */
1585
+ size(): number;
1586
+ }
1587
+
1588
+ /**
1589
+ * Minimal Redis client interface required for distributed locking operations.
1590
+ *
1591
+ * Designed to be compatible with popular Redis clients (node-redis, ioredis)
1592
+ * while keeping dependencies minimal. Only requires SET NX EX and EVAL commands
1593
+ * for atomic lock operations.
1594
+ *
1595
+ * @public
1596
+ * @since 3.1.0
1597
+ */
1598
+ interface RedisClient {
1599
+ /**
1600
+ * Atomically sets a key with value only if key doesn't exist (NX) and expires after TTL seconds (EX).
1601
+ *
1602
+ * This is the core primitive for distributed lock acquisition. The combination of
1603
+ * NX (Not eXists) and EX (EXpiration) flags ensures atomicity and auto-release.
1604
+ *
1605
+ * @param key - Redis key for the lock (e.g., 'sitemap:lock:generation')
1606
+ * @param value - Unique lock identifier (UUID) for ownership validation
1607
+ * @param mode - Must be 'EX' for expiration in seconds
1608
+ * @param ttl - Time-to-live in seconds before auto-release
1609
+ * @param flag - Must be 'NX' to set only if not exists
1610
+ * @returns 'OK' if lock acquired, null if key already exists
1611
+ */
1612
+ set(key: string, value: string, mode: 'EX', ttl: number, flag: 'NX'): Promise<string | null>;
1613
+ /**
1614
+ * Executes a Lua script atomically on the Redis server.
1615
+ *
1616
+ * Used for safe lock release: compares lock owner and deletes in a single atomic operation.
1617
+ * This prevents accidentally releasing locks held by other instances.
1618
+ *
1619
+ * @param script - Lua script source code
1620
+ * @param numKeys - Number of Redis keys referenced in the script
1621
+ * @param args - Keys and arguments for the script (KEYS[], ARGV[])
1622
+ * @returns Script execution result (1 if deleted, 0 if not owner)
1623
+ */
1624
+ eval(script: string, numKeys: number, ...args: string[]): Promise<number>;
1625
+ }
1626
+ /**
1627
+ * Configuration options for RedisLock distributed locking.
1628
+ *
1629
+ * @public
1630
+ * @since 3.1.0
1631
+ */
1632
+ interface RedisLockOptions {
1633
+ /**
1634
+ * Connected Redis client instance.
1635
+ *
1636
+ * Must be already connected and ready for commands.
1637
+ * Supports node-redis, ioredis, or any client implementing RedisClient interface.
1638
+ */
1639
+ client: RedisClient;
1640
+ /**
1641
+ * Prefix for all lock keys in Redis.
1642
+ *
1643
+ * Recommended to include namespace and entity type for clarity.
1644
+ *
1645
+ * @defaultValue 'sitemap:lock:'
1646
+ * @example 'myapp:sitemap:lock:'
1647
+ */
1648
+ keyPrefix?: string;
1649
+ /**
1650
+ * Number of retry attempts if lock acquisition fails.
1651
+ *
1652
+ * Set to 0 for fail-fast behavior (no retries).
1653
+ * Recommended: 3-5 retries for transient contention.
1654
+ *
1655
+ * @defaultValue 0
1656
+ */
1657
+ retryCount?: number;
1658
+ /**
1659
+ * Delay in milliseconds between retry attempts.
1660
+ *
1661
+ * Should be tuned based on expected lock hold time.
1662
+ * Too short: wastes CPU, too long: increases latency.
1663
+ *
1664
+ * @defaultValue 100
1665
+ */
1666
+ retryDelay?: number;
1667
+ }
1668
+ /**
1669
+ * Redis-based distributed lock for multi-instance sitemap generation.
1670
+ *
1671
+ * Implements distributed mutual exclusion using Redis atomic operations (SET NX EX)
1672
+ * and Lua scripts for safe ownership-validated release. Designed for production
1673
+ * environments where multiple instances compete for exclusive access to resources.
1674
+ *
1675
+ * **When to use:**
1676
+ * - Kubernetes or Docker Swarm multi-replica deployments
1677
+ * - Horizontally scaled applications (multiple instances/VMs)
1678
+ * - Any environment requiring cross-process/cross-machine synchronization
1679
+ * - Production systems where cache stampede prevention is critical
1680
+ *
1681
+ * **When NOT to use:**
1682
+ * - Single-instance deployments (use {@link MemoryLock} instead)
1683
+ * - Development/testing environments without Redis infrastructure
1684
+ * - Scenarios where eventual consistency is acceptable
1685
+ *
1686
+ * **Design rationale:**
1687
+ * - Atomic operations prevent race conditions at Redis level
1688
+ * - Unique lock ID (UUID) ensures only owner can release lock
1689
+ * - Auto-expiration (TTL) prevents deadlocks from crashed instances
1690
+ * - Retry mechanism handles transient contention gracefully
1691
+ * - Lua scripts provide atomicity for compare-and-delete operations
1692
+ *
1693
+ * **Comparison with RedLock:**
1694
+ * This implementation uses a single Redis instance. For Redis Cluster deployments,
1695
+ * consider implementing the RedLock algorithm (multiple Redis masters) for higher
1696
+ * availability. Current implementation trades off some availability for simplicity.
1697
+ *
1698
+ * @example Basic usage with node-redis
1699
+ * ```typescript
1700
+ * import { RedisLock } from '@gravito/constellation'
1701
+ * import { createClient } from 'redis'
1702
+ *
1703
+ * const redisClient = createClient({ url: 'redis://localhost:6379' })
1704
+ * await redisClient.connect()
1705
+ *
1706
+ * const lock = new RedisLock({
1707
+ * client: redisClient,
1708
+ * keyPrefix: 'sitemap:lock:',
1709
+ * retryCount: 3,
1710
+ * retryDelay: 100
1711
+ * })
1712
+ *
1713
+ * const acquired = await lock.acquire('sitemap-generation', 60000)
1714
+ * if (acquired) {
1715
+ * try {
1716
+ * await generateSitemap()
1717
+ * } finally {
1718
+ * await lock.release('sitemap-generation')
1719
+ * }
1720
+ * }
1721
+ * ```
1722
+ *
1723
+ * @example Production deployment with Kubernetes
1724
+ * ```typescript
1725
+ * // In Kubernetes, multiple pods compete for the lock
1726
+ * const lock = new RedisLock({
1727
+ * client: redisClient,
1728
+ * keyPrefix: `${process.env.K8S_NAMESPACE}:sitemap:lock:`,
1729
+ * retryCount: 5,
1730
+ * retryDelay: 200
1731
+ * })
1732
+ *
1733
+ * // Pod 1 acquires lock
1734
+ * const acquired = await lock.acquire('generation', 300000)
1735
+ * if (!acquired) {
1736
+ * // Pod 2, 3, ... receive 503 and retry later
1737
+ * return new Response('Another pod is generating sitemap', {
1738
+ * status: 503,
1739
+ * headers: { 'Retry-After': '60' }
1740
+ * })
1741
+ * }
1742
+ * ```
1743
+ *
1744
+ * @example Handling Redis connection failures
1745
+ * ```typescript
1746
+ * const lock = new RedisLock({ client: redisClient })
1747
+ *
1748
+ * try {
1749
+ * const acquired = await lock.acquire('resource', 30000)
1750
+ * // If Redis is down, acquire() returns false (logged internally)
1751
+ * if (!acquired) {
1752
+ * console.log('Lock acquisition failed (may be Redis connection issue)')
1753
+ * }
1754
+ * } catch (error) {
1755
+ * // Network errors are caught and logged, not thrown
1756
+ * console.error('Unexpected error:', error)
1757
+ * }
1758
+ * ```
1759
+ *
1760
+ * @public
1761
+ * @since 3.1.0
1762
+ */
1763
+ declare class RedisLock implements SitemapLock {
1764
+ private options;
1765
+ /**
1766
+ * Unique identifier for this lock instance.
1767
+ *
1768
+ * Generated once during construction and used for all locks acquired by this instance.
1769
+ * Enables ownership validation: only the instance that acquired the lock can release it.
1770
+ *
1771
+ * **Security consideration:**
1772
+ * UUIDs are sufficiently random to prevent lock hijacking across instances.
1773
+ * However, they are stored in plain text in Redis (not encrypted).
1774
+ */
1775
+ private lockId;
1776
+ /**
1777
+ * Redis key prefix for all locks acquired through this instance.
1778
+ *
1779
+ * Combined with resource name to form full Redis key (e.g., 'sitemap:lock:generation').
1780
+ * Allows namespace isolation and easier debugging in Redis CLI.
1781
+ */
1782
+ private keyPrefix;
1783
+ /**
1784
+ * Maximum number of retry attempts when lock is held by another instance.
1785
+ *
1786
+ * Set to 0 for fail-fast behavior. Higher values increase acquisition success
1787
+ * rate but also increase latency under contention.
1788
+ */
1789
+ private retryCount;
1790
+ /**
1791
+ * Delay in milliseconds between consecutive retry attempts.
1792
+ *
1793
+ * Should be tuned based on expected lock hold time. Typical values: 50-500ms.
1794
+ */
1795
+ private retryDelay;
1796
+ /**
1797
+ * Constructs a new RedisLock instance with the specified configuration.
1798
+ *
1799
+ * @param options - Configuration including Redis client and retry parameters.
1800
+ *
1801
+ * @example With custom retry strategy
1802
+ * ```typescript
1803
+ * const lock = new RedisLock({
1804
+ * client: redisClient,
1805
+ * keyPrefix: 'app:locks:',
1806
+ * retryCount: 10, // More retries for high-contention scenarios
1807
+ * retryDelay: 50 // Shorter delay for low-latency requirements
1808
+ * })
1809
+ * ```
1810
+ */
1811
+ constructor(options: RedisLockOptions);
1812
+ /**
1813
+ * Attempts to acquire a distributed lock using Redis SET NX EX command.
1814
+ *
1815
+ * Uses atomic Redis operations to ensure only one instance across your entire
1816
+ * infrastructure can hold the lock at any given time. Implements retry logic
1817
+ * with exponential backoff for handling transient contention.
1818
+ *
1819
+ * **Algorithm:**
1820
+ * 1. Convert TTL from milliseconds to seconds (Redis requirement)
1821
+ * 2. Attempt Redis SET key lockId EX ttl NX (atomic operation)
1822
+ * 3. If successful (returns 'OK'), lock acquired
1823
+ * 4. If failed (returns null), retry up to retryCount times with retryDelay
1824
+ * 5. Return true if acquired, false if all attempts exhausted
1825
+ *
1826
+ * **Atomicity guarantee:**
1827
+ * The combination of NX (set if Not eXists) and EX (set EXpiration) in a single
1828
+ * Redis command ensures no race conditions. Either the lock is acquired or it isn't.
1829
+ *
1830
+ * **Error handling:**
1831
+ * Redis connection errors are caught, logged to console, and treated as acquisition
1832
+ * failure (returns false). This fail-safe behavior prevents exceptions from bubbling
1833
+ * up to application code.
1834
+ *
1835
+ * **Performance:**
1836
+ * - Single instance: O(1) Redis operation
1837
+ * - With retry: O(retryCount) worst case
1838
+ * - Network latency: ~1-5ms per attempt (depends on Redis location)
1839
+ *
1840
+ * @param resource - Unique identifier for the resource to lock.
1841
+ * Combined with keyPrefix to form Redis key.
1842
+ * @param ttl - Time-to-live in milliseconds. Lock auto-expires after this duration.
1843
+ * Recommended: 2-5x expected operation time to handle slowdowns.
1844
+ * Minimum: 1000ms (1 second) for practical use.
1845
+ * @returns Promise resolving to `true` if lock acquired, `false` if held by another instance
1846
+ * or Redis connection failed.
1847
+ *
1848
+ * @example Basic acquisition in distributed environment
1849
+ * ```typescript
1850
+ * const lock = new RedisLock({ client: redisClient })
1851
+ * const acquired = await lock.acquire('sitemap-generation', 60000)
1852
+ *
1853
+ * if (!acquired) {
1854
+ * // Another instance (e.g., different Kubernetes pod) holds the lock
1855
+ * console.log('Sitemap generation in progress on another instance')
1856
+ * return new Response('Service busy', {
1857
+ * status: 503,
1858
+ * headers: { 'Retry-After': '30' }
1859
+ * })
1860
+ * }
1861
+ *
1862
+ * try {
1863
+ * await generateSitemap()
1864
+ * } finally {
1865
+ * await lock.release('sitemap-generation')
1866
+ * }
1867
+ * ```
1868
+ *
1869
+ * @example With retry logic for transient contention
1870
+ * ```typescript
1871
+ * const lock = new RedisLock({
1872
+ * client: redisClient,
1873
+ * retryCount: 5,
1874
+ * retryDelay: 200
1875
+ * })
1876
+ *
1877
+ * // Will retry 5 times with 200ms delay between attempts
1878
+ * const acquired = await lock.acquire('data-import', 120000)
1879
+ * ```
1880
+ *
1881
+ * @example Setting appropriate TTL
1882
+ * ```typescript
1883
+ * // Fast operation: short TTL
1884
+ * await lock.acquire('cache-rebuild', 10000) // 10 seconds
1885
+ *
1886
+ * // Slow operation: longer TTL with buffer
1887
+ * await lock.acquire('full-sitemap', 300000) // 5 minutes
1888
+ *
1889
+ * // Very slow operation: generous TTL
1890
+ * await lock.acquire('data-migration', 1800000) // 30 minutes
1891
+ * ```
1892
+ */
1893
+ acquire(resource: string, ttl: number): Promise<boolean>;
1894
+ /**
1895
+ * Releases the distributed lock using Lua script for atomic ownership validation.
1896
+ *
1897
+ * Uses a Lua script to atomically check if the current instance owns the lock
1898
+ * (by comparing lockId) and delete it if so. This prevents accidentally releasing
1899
+ * locks held by other instances, which could cause data corruption in distributed systems.
1900
+ *
1901
+ * **Lua script logic:**
1902
+ * ```lua
1903
+ * if redis.call("get", KEYS[1]) == ARGV[1] then
1904
+ * return redis.call("del", KEYS[1]) -- Delete only if owner matches
1905
+ * else
1906
+ * return 0 -- Not owner or lock expired, do nothing
1907
+ * end
1908
+ * ```
1909
+ *
1910
+ * **Why Lua scripts?**
1911
+ * - Atomicity: GET + comparison + DEL execute as one atomic operation
1912
+ * - Prevents race conditions: Lock cannot change between check and delete
1913
+ * - Server-side execution: No network round trips between steps
1914
+ *
1915
+ * **Error handling:**
1916
+ * Errors during release (e.g., Redis connection loss) are logged but do NOT throw.
1917
+ * This is intentional: if instance crashed or TTL expired, lock is already released.
1918
+ * Silent failure here prevents cascading errors in finally blocks.
1919
+ *
1920
+ * **Idempotency:**
1921
+ * Safe to call multiple times. Releasing an already-released or expired lock is a no-op.
1922
+ *
1923
+ * @param resource - The resource identifier to unlock. Must match the identifier
1924
+ * used in the corresponding `acquire()` call.
1925
+ *
1926
+ * @example Proper release pattern with try-finally
1927
+ * ```typescript
1928
+ * const acquired = await lock.acquire('sitemap-generation', 60000)
1929
+ * if (!acquired) return
1930
+ *
1931
+ * try {
1932
+ * await generateSitemap()
1933
+ * } finally {
1934
+ * // Always release, even if operation throws
1935
+ * await lock.release('sitemap-generation')
1936
+ * }
1937
+ * ```
1938
+ *
1939
+ * @example Handling operation failures
1940
+ * ```typescript
1941
+ * const acquired = await lock.acquire('data-processing', 120000)
1942
+ * if (!acquired) {
1943
+ * throw new Error('Could not acquire lock')
1944
+ * }
1945
+ *
1946
+ * try {
1947
+ * await processData()
1948
+ * } catch (error) {
1949
+ * console.error('Processing failed:', error)
1950
+ * // Lock still released in finally block
1951
+ * throw error
1952
+ * } finally {
1953
+ * await lock.release('data-processing')
1954
+ * }
1955
+ * ```
1956
+ *
1957
+ * @example Why ownership validation matters
1958
+ * ```typescript
1959
+ * // Instance A acquires lock with 10-second TTL
1960
+ * const lockA = new RedisLock({ client: redisClientA })
1961
+ * await lockA.acquire('task', 10000)
1962
+ *
1963
+ * // ... 11 seconds pass, lock auto-expires ...
1964
+ *
1965
+ * // Instance B acquires the now-expired lock
1966
+ * const lockB = new RedisLock({ client: redisClientB })
1967
+ * await lockB.acquire('task', 10000)
1968
+ *
1969
+ * // Instance A tries to release (after slowdown/GC pause)
1970
+ * await lockA.release('task')
1971
+ * // ✅ Lua script detects lockId mismatch, does NOT delete B's lock
1972
+ * // ❌ Without Lua: Would delete B's lock, causing data corruption
1973
+ * ```
1974
+ */
1975
+ release(resource: string): Promise<void>;
1976
+ /**
1977
+ * Internal utility for sleeping between retry attempts.
1978
+ *
1979
+ * @param ms - Duration to sleep in milliseconds
1980
+ */
1981
+ private sleep;
1982
+ }
1983
+
1984
+ /**
1985
+ * Configuration for a dynamically generated sitemap that is built upon request.
1986
+ *
1987
+ * Useful for small to medium sites where fresh data is critical. Dynamic sitemaps
1988
+ * are generated on-the-fly and can be cached at the HTTP level.
1989
+ *
1990
+ * @public
1991
+ * @since 3.0.0
1992
+ */
1993
+ interface DynamicSitemapOptions extends SitemapStreamOptions {
1994
+ /** The URL path where the sitemap will be exposed. @default '/sitemap.xml' */
1995
+ path?: string | undefined;
1996
+ /** List of sitemap entry providers to scan for content. */
1997
+ providers: SitemapProvider[];
1998
+ /** Cache duration in seconds for the HTTP response. @default undefined (no-cache) */
1999
+ cacheSeconds?: number | undefined;
2000
+ /** Persistence backend for cached XML files. Defaults to MemorySitemapStorage. */
2001
+ storage?: SitemapStorage | undefined;
2002
+ /** Optional distributed lock to prevent "cache stampede" during heavy generation. */
2003
+ lock?: SitemapLock | undefined;
2004
+ }
2005
+ /**
2006
+ * Configuration for a statically pre-generated sitemap.
2007
+ *
2008
+ * Recommended for large sites or high-traffic applications. Static sitemaps
2009
+ * are built (usually as part of a build process) and served as static files.
2010
+ *
2011
+ * @public
2012
+ * @since 3.0.0
2013
+ */
2014
+ interface StaticSitemapOptions extends SitemapStreamOptions {
2015
+ /** Local directory where generated XML files will be saved. */
2016
+ outDir: string;
2017
+ /** The name of the root sitemap file. @default 'sitemap.xml' */
2018
+ filename?: string | undefined;
2019
+ /** List of sitemap entry providers to scan for content. */
2020
+ providers: SitemapProvider[];
2021
+ /** Custom storage backend. Defaults to DiskSitemapStorage using `outDir`. */
2022
+ storage?: SitemapStorage | undefined;
2023
+ /** Optional incremental generation settings to only update changed URLs. */
2024
+ incremental?: {
2025
+ /** Whether to enable incremental builds. */
2026
+ enabled: boolean;
2027
+ /** Backend for tracking structural site changes. */
2028
+ changeTracker: ChangeTracker;
2029
+ /** Whether to automatically record new changes during scan. @default false */
2030
+ autoTrack?: boolean;
2031
+ };
2032
+ /** Optional SEO redirect orchestration settings. */
2033
+ redirect?: {
2034
+ /** Whether to automatically handle 301/302 redirects found in sitemap. */
2035
+ enabled: boolean;
2036
+ /** Backend for managing and resolving redirect rules. */
2037
+ manager: RedirectManager;
2038
+ /** Strategy for resolving conflicting or chained redirects. @default 'remove_old_add_new' */
2039
+ strategy?: 'remove_old_add_new' | 'keep_relation' | 'update_url' | 'dual_mark';
2040
+ /** Whether to resolve redirect chains into a single jump. @default false */
2041
+ followChains?: boolean;
2042
+ /** Maximum number of redirect jumps to follow. @default 5 */
2043
+ maxChainLength?: number;
2044
+ };
2045
+ /** Deployment settings for atomic sitemap updates via shadow staging. */
2046
+ shadow?: {
2047
+ /** Whether to enable "shadow" (atomic staging) mode. */
2048
+ enabled: boolean;
2049
+ /** The update strategy: 'atomic' (full swap) or 'versioned' (archived). @default 'atomic' */
2050
+ mode: 'atomic' | 'versioned';
2051
+ };
2052
+ /** Persistence backend for long-running job progress tracking. */
2053
+ progressStorage?: SitemapProgressStorage;
2054
+ }
2055
+ /**
2056
+ * OrbitSitemap is the enterprise SEO orchestration module for Gravito.
2057
+ *
2058
+ * It provides advanced sitemap generation supporting large-scale indexing,
2059
+ * automatic 301/302 redirect handling, and atomic sitemap deployments.
2060
+ *
2061
+ * It can operate in two modes:
2062
+ * 1. **Dynamic**: Sitemaps are generated on-the-fly and cached.
2063
+ * 2. **Static**: Sitemaps are pre-built (e.g., during CI/CD) and served from disk or cloud storage.
2064
+ *
2065
+ * @example Dynamic Mode
2066
+ * ```typescript
2067
+ * const sitemap = OrbitSitemap.dynamic({
2068
+ * baseUrl: 'https://example.com',
2069
+ * providers: [new PostSitemapProvider()]
2070
+ * });
2071
+ * core.addOrbit(sitemap);
2072
+ * ```
2073
+ *
2074
+ * @example Static Mode
2075
+ * ```typescript
2076
+ * const sitemap = OrbitSitemap.static({
2077
+ * baseUrl: 'https://example.com',
2078
+ * outDir: './public',
2079
+ * shadow: { enabled: true, mode: 'atomic' }
2080
+ * });
2081
+ * await sitemap.generate();
2082
+ * ```
2083
+ *
2084
+ * @public
2085
+ * @since 3.0.0
2086
+ */
2087
+ declare class OrbitSitemap implements GravitoOrbit {
2088
+ private options;
2089
+ private mode;
2090
+ private constructor();
2091
+ /**
2092
+ * Create a dynamic sitemap configuration.
2093
+ *
2094
+ * @param options - The dynamic sitemap options.
2095
+ * @returns An OrbitSitemap instance configured for dynamic generation.
2096
+ */
2097
+ static dynamic(options: DynamicSitemapOptions): OrbitSitemap;
2098
+ /**
2099
+ * Create a static sitemap configuration.
2100
+ *
2101
+ * @param options - The static sitemap options.
2102
+ * @returns An OrbitSitemap instance configured for static generation.
2103
+ */
2104
+ static static(options: StaticSitemapOptions): OrbitSitemap;
2105
+ /**
2106
+ * Installs the sitemap module into PlanetCore.
2107
+ *
2108
+ * @param core - The PlanetCore instance.
1110
2109
  */
1111
2110
  install(core: PlanetCore): void;
2111
+ /**
2112
+ * Internal method to set up dynamic sitemap routes.
2113
+ */
1112
2114
  private installDynamic;
1113
2115
  /**
1114
- * Generate the sitemap (static mode only).
2116
+ * Generates the sitemap (static mode only).
1115
2117
  *
1116
2118
  * @returns A promise that resolves when generation is complete.
1117
2119
  * @throws {Error} If called in dynamic mode.
1118
2120
  */
1119
2121
  generate(): Promise<void>;
1120
2122
  /**
1121
- * Generate incremental sitemap updates (static mode only).
2123
+ * Generates incremental sitemap updates (static mode only).
1122
2124
  *
1123
2125
  * @param since - Only include items modified since this date.
1124
2126
  * @returns A promise that resolves when incremental generation is complete.
@@ -1126,7 +2128,7 @@ declare class OrbitSitemap implements GravitoOrbit {
1126
2128
  */
1127
2129
  generateIncremental(since?: Date): Promise<void>;
1128
2130
  /**
1129
- * Generate sitemap asynchronously in the background (static mode only).
2131
+ * Generates sitemap asynchronously in the background (static mode only).
1130
2132
  *
1131
2133
  * @param options - Options for the async generation job.
1132
2134
  * @returns A promise resolving to the job ID.
@@ -1144,7 +2146,7 @@ declare class OrbitSitemap implements GravitoOrbit {
1144
2146
  onError?: (error: Error) => void;
1145
2147
  }): Promise<string>;
1146
2148
  /**
1147
- * Install API endpoints for triggering and monitoring sitemap generation.
2149
+ * Installs API endpoints for triggering and monitoring sitemap generation.
1148
2150
  *
1149
2151
  * @param core - The PlanetCore instance.
1150
2152
  * @param basePath - The base path for the API endpoints (default: '/admin/sitemap').
@@ -1193,9 +2195,12 @@ declare class RouteScanner implements SitemapProvider {
1193
2195
  private options;
1194
2196
  constructor(router: any, options?: RouteScannerOptions);
1195
2197
  /**
1196
- * Scan the router and return discovered entries.
2198
+ * Scans the router and returns discovered static GET routes as sitemap entries.
1197
2199
  *
1198
- * @returns An array of sitemap entries.
2200
+ * This method iterates through all registered routes in the Gravito router,
2201
+ * applying inclusion/exclusion filters and defaulting metadata for matching routes.
2202
+ *
2203
+ * @returns An array of `SitemapEntry` objects.
1199
2204
  */
1200
2205
  getEntries(): SitemapEntry[];
1201
2206
  private extractRoutes;
@@ -1305,27 +2310,33 @@ declare class RedirectDetector {
1305
2310
  private cache;
1306
2311
  constructor(options: RedirectDetectorOptions);
1307
2312
  /**
1308
- * 偵測單一 URL 的轉址
2313
+ * Detects redirects for a single URL using multiple strategies.
2314
+ *
2315
+ * @param url - The URL path to probe for redirects.
2316
+ * @returns A promise resolving to a `RedirectRule` if a redirect is found, or null.
1309
2317
  */
1310
2318
  detect(url: string): Promise<RedirectRule | null>;
1311
2319
  /**
1312
- * 批次偵測轉址
2320
+ * Batch detects redirects for multiple URLs with concurrency control.
2321
+ *
2322
+ * @param urls - An array of URL paths to probe.
2323
+ * @returns A promise resolving to a Map of URLs to their respective `RedirectRule` or null.
1313
2324
  */
1314
2325
  detectBatch(urls: string[]): Promise<Map<string, RedirectRule | null>>;
1315
2326
  /**
1316
- * 從資料庫偵測
2327
+ * Detects a redirect from the configured database table.
1317
2328
  */
1318
2329
  private detectFromDatabase;
1319
2330
  /**
1320
- * 從設定檔偵測
2331
+ * Detects a redirect from a static JSON configuration file.
1321
2332
  */
1322
2333
  private detectFromConfig;
1323
2334
  /**
1324
- * 自動偵測(透過 HTTP 請求)
2335
+ * Auto-detects a redirect by sending an HTTP HEAD request.
1325
2336
  */
1326
2337
  private detectAuto;
1327
2338
  /**
1328
- * 快取結果
2339
+ * Caches the detection result for a URL.
1329
2340
  */
1330
2341
  private cacheResult;
1331
2342
  }
@@ -1374,23 +2385,26 @@ declare class RedirectHandler {
1374
2385
  private options;
1375
2386
  constructor(options: RedirectHandlerOptions);
1376
2387
  /**
1377
- * 處理 entries 中的轉址
2388
+ * Processes a list of sitemap entries and handles redirects according to the configured strategy.
2389
+ *
2390
+ * @param entries - The original list of sitemap entries.
2391
+ * @returns A promise resolving to the processed list of entries.
1378
2392
  */
1379
2393
  processEntries(entries: SitemapEntry[]): Promise<SitemapEntry[]>;
1380
2394
  /**
1381
- * 策略一:移除舊 URL,加入新 URL
2395
+ * Strategy 1: Remove old URL and add the new destination URL.
1382
2396
  */
1383
2397
  private handleRemoveOldAddNew;
1384
2398
  /**
1385
- * 策略二:保留關聯,使用 canonical link
2399
+ * Strategy 2: Keep the original URL but mark the destination as canonical.
1386
2400
  */
1387
2401
  private handleKeepRelation;
1388
2402
  /**
1389
- * 策略三:僅更新 URL
2403
+ * Strategy 3: Silently update the URL to the destination.
1390
2404
  */
1391
2405
  private handleUpdateUrl;
1392
2406
  /**
1393
- * 策略四:雙重標記
2407
+ * Strategy 4: Include both the original and destination URLs.
1394
2408
  */
1395
2409
  private handleDualMark;
1396
2410
  }
@@ -1418,10 +2432,39 @@ declare class MemoryRedirectManager implements RedirectManager {
1418
2432
  private rules;
1419
2433
  private maxRules;
1420
2434
  constructor(options?: MemoryRedirectManagerOptions);
2435
+ /**
2436
+ * Registers a single redirect rule in memory.
2437
+ *
2438
+ * @param redirect - The redirect rule to add.
2439
+ */
1421
2440
  register(redirect: RedirectRule): Promise<void>;
2441
+ /**
2442
+ * Registers multiple redirect rules in memory.
2443
+ *
2444
+ * @param redirects - An array of redirect rules.
2445
+ */
1422
2446
  registerBatch(redirects: RedirectRule[]): Promise<void>;
2447
+ /**
2448
+ * Retrieves a specific redirect rule by its source path from memory.
2449
+ *
2450
+ * @param from - The source path.
2451
+ * @returns A promise resolving to the redirect rule, or null if not found.
2452
+ */
1423
2453
  get(from: string): Promise<RedirectRule | null>;
2454
+ /**
2455
+ * Retrieves all registered redirect rules from memory.
2456
+ *
2457
+ * @returns A promise resolving to an array of all redirect rules.
2458
+ */
1424
2459
  getAll(): Promise<RedirectRule[]>;
2460
+ /**
2461
+ * Resolves a URL to its final destination through the redirect table.
2462
+ *
2463
+ * @param url - The URL to resolve.
2464
+ * @param followChains - Whether to recursively resolve chained redirects.
2465
+ * @param maxChainLength - Maximum depth for chain resolution.
2466
+ * @returns A promise resolving to the final destination URL.
2467
+ */
1425
2468
  resolve(url: string, followChains?: boolean, maxChainLength?: number): Promise<string | null>;
1426
2469
  }
1427
2470
  /**
@@ -1455,10 +2498,39 @@ declare class RedisRedirectManager implements RedirectManager {
1455
2498
  constructor(options: RedisRedirectManagerOptions);
1456
2499
  private getKey;
1457
2500
  private getListKey;
2501
+ /**
2502
+ * Registers a single redirect rule in Redis.
2503
+ *
2504
+ * @param redirect - The redirect rule to add.
2505
+ */
1458
2506
  register(redirect: RedirectRule): Promise<void>;
2507
+ /**
2508
+ * Registers multiple redirect rules in Redis.
2509
+ *
2510
+ * @param redirects - An array of redirect rules.
2511
+ */
1459
2512
  registerBatch(redirects: RedirectRule[]): Promise<void>;
2513
+ /**
2514
+ * Retrieves a specific redirect rule by its source path from Redis.
2515
+ *
2516
+ * @param from - The source path.
2517
+ * @returns A promise resolving to the redirect rule, or null if not found.
2518
+ */
1460
2519
  get(from: string): Promise<RedirectRule | null>;
2520
+ /**
2521
+ * Retrieves all registered redirect rules from Redis.
2522
+ *
2523
+ * @returns A promise resolving to an array of all redirect rules.
2524
+ */
1461
2525
  getAll(): Promise<RedirectRule[]>;
2526
+ /**
2527
+ * Resolves a URL to its final destination through the Redis redirect table.
2528
+ *
2529
+ * @param url - The URL to resolve.
2530
+ * @param followChains - Whether to recursively resolve chained redirects.
2531
+ * @param maxChainLength - Maximum depth for chain resolution.
2532
+ * @returns A promise resolving to the final destination URL.
2533
+ */
1462
2534
  resolve(url: string, followChains?: boolean, maxChainLength?: number): Promise<string | null>;
1463
2535
  }
1464
2536
 
@@ -1482,9 +2554,51 @@ declare class DiskSitemapStorage implements SitemapStorage {
1482
2554
  private outDir;
1483
2555
  private baseUrl;
1484
2556
  constructor(outDir: string, baseUrl: string);
2557
+ /**
2558
+ * Writes sitemap content to a file on the local disk.
2559
+ *
2560
+ * @param filename - The name of the file to write.
2561
+ * @param content - The XML or JSON content.
2562
+ */
1485
2563
  write(filename: string, content: string): Promise<void>;
2564
+ /**
2565
+ * 使用串流方式寫入 sitemap 檔案,可選擇性啟用 gzip 壓縮。
2566
+ * 此方法可大幅降低大型 sitemap 的記憶體峰值。
2567
+ *
2568
+ * @param filename - 檔案名稱
2569
+ * @param stream - XML 內容的 AsyncIterable
2570
+ * @param options - 寫入選項(如壓縮、content type)
2571
+ *
2572
+ * @since 3.1.0
2573
+ */
2574
+ writeStream(filename: string, stream: AsyncIterable<string>, options?: WriteStreamOptions): Promise<void>;
2575
+ /**
2576
+ * Reads sitemap content from a file on the local disk.
2577
+ *
2578
+ * @param filename - The name of the file to read.
2579
+ * @returns A promise resolving to the file content as a string, or null if not found.
2580
+ */
1486
2581
  read(filename: string): Promise<string | null>;
2582
+ /**
2583
+ * Returns a readable stream for a sitemap file on the local disk.
2584
+ *
2585
+ * @param filename - The name of the file to stream.
2586
+ * @returns A promise resolving to an async iterable of file chunks, or null if not found.
2587
+ */
2588
+ readStream(filename: string): Promise<AsyncIterable<string> | null>;
2589
+ /**
2590
+ * Checks if a sitemap file exists on the local disk.
2591
+ *
2592
+ * @param filename - The name of the file to check.
2593
+ * @returns A promise resolving to true if the file exists, false otherwise.
2594
+ */
1487
2595
  exists(filename: string): Promise<boolean>;
2596
+ /**
2597
+ * Returns the full public URL for a sitemap file.
2598
+ *
2599
+ * @param filename - The name of the sitemap file.
2600
+ * @returns The public URL as a string.
2601
+ */
1488
2602
  getUrl(filename: string): string;
1489
2603
  }
1490
2604
 
@@ -1542,26 +2656,125 @@ declare class GCPSitemapStorage implements SitemapStorage {
1542
2656
  constructor(options: GCPSitemapStorageOptions);
1543
2657
  private getStorageClient;
1544
2658
  private getKey;
2659
+ /**
2660
+ * Writes sitemap content to a Google Cloud Storage object.
2661
+ *
2662
+ * @param filename - The name of the file to write.
2663
+ * @param content - The XML or JSON content.
2664
+ */
1545
2665
  write(filename: string, content: string): Promise<void>;
2666
+ /**
2667
+ * 使用串流方式寫入 sitemap 至 GCP Cloud Storage,可選擇性啟用 gzip 壓縮。
2668
+ *
2669
+ * @param filename - 檔案名稱
2670
+ * @param stream - XML 內容的 AsyncIterable
2671
+ * @param options - 寫入選項(如壓縮、content type)
2672
+ *
2673
+ * @since 3.1.0
2674
+ */
2675
+ writeStream(filename: string, stream: AsyncIterable<string>, options?: WriteStreamOptions): Promise<void>;
2676
+ /**
2677
+ * Reads sitemap content from a Google Cloud Storage object.
2678
+ *
2679
+ * @param filename - The name of the file to read.
2680
+ * @returns A promise resolving to the file content as a string, or null if not found.
2681
+ */
1546
2682
  read(filename: string): Promise<string | null>;
2683
+ /**
2684
+ * Returns a readable stream for a Google Cloud Storage object.
2685
+ *
2686
+ * @param filename - The name of the file to stream.
2687
+ * @returns A promise resolving to an async iterable of file chunks, or null if not found.
2688
+ */
2689
+ readStream(filename: string): Promise<AsyncIterable<string> | null>;
2690
+ /**
2691
+ * Checks if a Google Cloud Storage object exists.
2692
+ *
2693
+ * @param filename - The name of the file to check.
2694
+ * @returns A promise resolving to true if the file exists, false otherwise.
2695
+ */
1547
2696
  exists(filename: string): Promise<boolean>;
2697
+ /**
2698
+ * Returns the full public URL for a Google Cloud Storage object.
2699
+ *
2700
+ * @param filename - The name of the sitemap file.
2701
+ * @returns The public URL as a string.
2702
+ */
1548
2703
  getUrl(filename: string): string;
2704
+ /**
2705
+ * Writes content to a shadow (staged) location in Google Cloud Storage.
2706
+ *
2707
+ * @param filename - The name of the file to write.
2708
+ * @param content - The XML or JSON content.
2709
+ * @param shadowId - Optional unique session identifier.
2710
+ */
1549
2711
  writeShadow(filename: string, content: string, shadowId?: string): Promise<void>;
2712
+ /**
2713
+ * Commits all staged shadow objects in a session to production in Google Cloud Storage.
2714
+ *
2715
+ * @param shadowId - The identifier of the session to commit.
2716
+ */
1550
2717
  commitShadow(shadowId: string): Promise<void>;
2718
+ /**
2719
+ * Lists all archived versions of a specific sitemap in Google Cloud Storage.
2720
+ *
2721
+ * @param filename - The sitemap filename.
2722
+ * @returns A promise resolving to an array of version identifiers.
2723
+ */
1551
2724
  listVersions(filename: string): Promise<string[]>;
2725
+ /**
2726
+ * Reverts a sitemap to a previously archived version in Google Cloud Storage.
2727
+ *
2728
+ * @param filename - The sitemap filename.
2729
+ * @param version - The version identifier to switch to.
2730
+ */
1552
2731
  switchVersion(filename: string, version: string): Promise<void>;
1553
2732
  }
1554
2733
 
1555
2734
  /**
1556
- * 記憶體進度儲存實作
1557
- * 適用於單一進程或開發環境
2735
+ * MemoryProgressStorage is a non-persistent, in-memory implementation of the `SitemapProgressStorage`.
2736
+ *
2737
+ * It is suitable for single-process applications or development environments where
2738
+ * persistence of job progress across application restarts is not required.
2739
+ *
2740
+ * @public
2741
+ * @since 3.0.0
1558
2742
  */
1559
2743
  declare class MemoryProgressStorage implements SitemapProgressStorage {
1560
2744
  private storage;
2745
+ /**
2746
+ * Retrieves the progress of a specific generation job from memory.
2747
+ *
2748
+ * @param jobId - Unique identifier for the job.
2749
+ * @returns A promise resolving to the `SitemapProgress` object, or null if not found.
2750
+ */
1561
2751
  get(jobId: string): Promise<SitemapProgress | null>;
2752
+ /**
2753
+ * Initializes or overwrites a progress record in memory.
2754
+ *
2755
+ * @param jobId - Unique identifier for the job.
2756
+ * @param progress - The initial or current state of the job progress.
2757
+ */
1562
2758
  set(jobId: string, progress: SitemapProgress): Promise<void>;
2759
+ /**
2760
+ * Updates specific fields of an existing progress record in memory.
2761
+ *
2762
+ * @param jobId - Unique identifier for the job.
2763
+ * @param updates - Object containing the fields to update.
2764
+ */
1563
2765
  update(jobId: string, updates: Partial<SitemapProgress>): Promise<void>;
2766
+ /**
2767
+ * Deletes a progress record from memory.
2768
+ *
2769
+ * @param jobId - Unique identifier for the job to remove.
2770
+ */
1564
2771
  delete(jobId: string): Promise<void>;
2772
+ /**
2773
+ * Lists the most recent sitemap generation jobs from memory.
2774
+ *
2775
+ * @param limit - Maximum number of records to return.
2776
+ * @returns A promise resolving to an array of `SitemapProgress` objects, sorted by start time.
2777
+ */
1565
2778
  list(limit?: number): Promise<SitemapProgress[]>;
1566
2779
  }
1567
2780
 
@@ -1584,9 +2797,51 @@ declare class MemorySitemapStorage implements SitemapStorage {
1584
2797
  private baseUrl;
1585
2798
  private files;
1586
2799
  constructor(baseUrl: string);
2800
+ /**
2801
+ * Writes sitemap content to memory.
2802
+ *
2803
+ * @param filename - The name of the file to store.
2804
+ * @param content - The XML or JSON content.
2805
+ */
1587
2806
  write(filename: string, content: string): Promise<void>;
2807
+ /**
2808
+ * 使用串流方式寫入 sitemap 至記憶體,可選擇性啟用 gzip 壓縮。
2809
+ * 記憶體儲存會收集串流為完整字串。
2810
+ *
2811
+ * @param filename - 檔案名稱
2812
+ * @param stream - XML 內容的 AsyncIterable
2813
+ * @param options - 寫入選項(如壓縮)
2814
+ *
2815
+ * @since 3.1.0
2816
+ */
2817
+ writeStream(filename: string, stream: AsyncIterable<string>, options?: WriteStreamOptions): Promise<void>;
2818
+ /**
2819
+ * Reads sitemap content from memory.
2820
+ *
2821
+ * @param filename - The name of the file to read.
2822
+ * @returns A promise resolving to the file content as a string, or null if not found.
2823
+ */
1588
2824
  read(filename: string): Promise<string | null>;
2825
+ /**
2826
+ * Returns a readable stream for a sitemap file in memory.
2827
+ *
2828
+ * @param filename - The name of the file to stream.
2829
+ * @returns A promise resolving to an async iterable of file chunks, or null if not found.
2830
+ */
2831
+ readStream(filename: string): Promise<AsyncIterable<string> | null>;
2832
+ /**
2833
+ * Checks if a sitemap file exists in memory.
2834
+ *
2835
+ * @param filename - The name of the file to check.
2836
+ * @returns A promise resolving to true if the file exists, false otherwise.
2837
+ */
1589
2838
  exists(filename: string): Promise<boolean>;
2839
+ /**
2840
+ * Returns the full public URL for a sitemap file.
2841
+ *
2842
+ * @param filename - The name of the sitemap file.
2843
+ * @returns The public URL as a string.
2844
+ */
1590
2845
  getUrl(filename: string): string;
1591
2846
  }
1592
2847
 
@@ -1620,10 +2875,39 @@ declare class RedisProgressStorage implements SitemapProgressStorage {
1620
2875
  constructor(options: RedisProgressStorageOptions);
1621
2876
  private getKey;
1622
2877
  private getListKey;
2878
+ /**
2879
+ * Retrieves the progress of a specific generation job from Redis.
2880
+ *
2881
+ * @param jobId - Unique identifier for the job.
2882
+ * @returns A promise resolving to the `SitemapProgress` object, or null if not found.
2883
+ */
1623
2884
  get(jobId: string): Promise<SitemapProgress | null>;
2885
+ /**
2886
+ * Initializes or overwrites a progress record in Redis.
2887
+ *
2888
+ * @param jobId - Unique identifier for the job.
2889
+ * @param progress - The initial or current state of the job progress.
2890
+ */
1624
2891
  set(jobId: string, progress: SitemapProgress): Promise<void>;
2892
+ /**
2893
+ * Updates specific fields of an existing progress record in Redis.
2894
+ *
2895
+ * @param jobId - Unique identifier for the job.
2896
+ * @param updates - Object containing the fields to update.
2897
+ */
1625
2898
  update(jobId: string, updates: Partial<SitemapProgress>): Promise<void>;
2899
+ /**
2900
+ * Deletes a progress record from Redis.
2901
+ *
2902
+ * @param jobId - Unique identifier for the job to remove.
2903
+ */
1626
2904
  delete(jobId: string): Promise<void>;
2905
+ /**
2906
+ * Lists the most recent sitemap generation jobs from Redis.
2907
+ *
2908
+ * @param limit - Maximum number of records to return.
2909
+ * @returns A promise resolving to an array of `SitemapProgress` objects, sorted by start time.
2910
+ */
1627
2911
  list(limit?: number): Promise<SitemapProgress[]>;
1628
2912
  }
1629
2913
 
@@ -1684,14 +2968,156 @@ declare class S3SitemapStorage implements SitemapStorage {
1684
2968
  constructor(options: S3SitemapStorageOptions);
1685
2969
  private getS3Client;
1686
2970
  private getKey;
2971
+ /**
2972
+ * Writes sitemap content to an S3 object.
2973
+ *
2974
+ * @param filename - The name of the file to write.
2975
+ * @param content - The XML or JSON content.
2976
+ */
1687
2977
  write(filename: string, content: string): Promise<void>;
2978
+ /**
2979
+ * 使用串流方式寫入 sitemap 至 S3,可選擇性啟用 gzip 壓縮。
2980
+ * S3 需要知道 Content-Length,因此會先收集串流為 Buffer。
2981
+ *
2982
+ * @param filename - 檔案名稱
2983
+ * @param stream - XML 內容的 AsyncIterable
2984
+ * @param options - 寫入選項(如壓縮、content type)
2985
+ *
2986
+ * @since 3.1.0
2987
+ */
2988
+ writeStream(filename: string, stream: AsyncIterable<string>, options?: WriteStreamOptions): Promise<void>;
2989
+ /**
2990
+ * Reads sitemap content from an S3 object.
2991
+ *
2992
+ * @param filename - The name of the file to read.
2993
+ * @returns A promise resolving to the file content as a string, or null if not found.
2994
+ */
1688
2995
  read(filename: string): Promise<string | null>;
2996
+ /**
2997
+ * Returns a readable stream for an S3 object.
2998
+ *
2999
+ * @param filename - The name of the file to stream.
3000
+ * @returns A promise resolving to an async iterable of file chunks, or null if not found.
3001
+ */
3002
+ readStream(filename: string): Promise<AsyncIterable<string> | null>;
3003
+ /**
3004
+ * Checks if an S3 object exists.
3005
+ *
3006
+ * @param filename - The name of the file to check.
3007
+ * @returns A promise resolving to true if the file exists, false otherwise.
3008
+ */
1689
3009
  exists(filename: string): Promise<boolean>;
3010
+ /**
3011
+ * Returns the full public URL for an S3 object.
3012
+ *
3013
+ * @param filename - The name of the sitemap file.
3014
+ * @returns The public URL as a string.
3015
+ */
1690
3016
  getUrl(filename: string): string;
3017
+ /**
3018
+ * Writes content to a shadow (staged) location in S3.
3019
+ *
3020
+ * @param filename - The name of the file to write.
3021
+ * @param content - The XML or JSON content.
3022
+ * @param shadowId - Optional unique session identifier.
3023
+ */
1691
3024
  writeShadow(filename: string, content: string, shadowId?: string): Promise<void>;
3025
+ /**
3026
+ * Commits all staged shadow objects in a session to production.
3027
+ *
3028
+ * @param shadowId - The identifier of the session to commit.
3029
+ */
1692
3030
  commitShadow(shadowId: string): Promise<void>;
3031
+ /**
3032
+ * Lists all archived versions of a specific sitemap in S3.
3033
+ *
3034
+ * @param filename - The sitemap filename.
3035
+ * @returns A promise resolving to an array of version identifiers.
3036
+ */
1693
3037
  listVersions(filename: string): Promise<string[]>;
3038
+ /**
3039
+ * Reverts a sitemap to a previously archived version in S3.
3040
+ *
3041
+ * @param filename - The sitemap filename.
3042
+ * @param version - The version identifier to switch to.
3043
+ */
1694
3044
  switchVersion(filename: string, version: string): Promise<void>;
1695
3045
  }
1696
3046
 
1697
- export { type AlternateUrl, type ChangeFreq, type ChangeTracker, type ChangeType, DiffCalculator, DiskSitemapStorage, type DynamicSitemapOptions, GCPSitemapStorage, type GCPSitemapStorageOptions, GenerateSitemapJob, type GenerateSitemapJobOptions, type I18nSitemapEntryOptions, IncrementalGenerator, type IncrementalGeneratorOptions, MemoryChangeTracker, MemoryProgressStorage, MemoryRedirectManager, type MemoryRedirectManagerOptions, MemorySitemapStorage, OrbitSitemap, ProgressTracker, type ProgressTrackerOptions, RedirectDetector, type RedirectDetectorOptions, RedirectHandler, type RedirectHandlerOptions, type RedirectHandlingStrategy, type RedirectManager, type RedirectRule, RedisChangeTracker, RedisProgressStorage, type RedisProgressStorageOptions, RedisRedirectManager, type RedisRedirectManagerOptions, RouteScanner, type RouteScannerOptions, S3SitemapStorage, type S3SitemapStorageOptions, ShadowProcessor, type ShadowProcessorOptions, type SitemapCache, type SitemapChange, type SitemapEntry, SitemapGenerator, type SitemapGeneratorOptions, type SitemapImage, SitemapIndex, type SitemapIndexEntry, type SitemapLock, type SitemapNews, type SitemapProgress, type SitemapProgressStorage, type SitemapProvider, type SitemapStorage, SitemapStream, type SitemapStreamOptions, type SitemapVideo, type StaticSitemapOptions, generateI18nEntries, routeScanner };
3047
+ /**
3048
+ * @gravito/constellation - Compression utilities
3049
+ * @module utils/Compression
3050
+ * @since 3.1.0
3051
+ */
3052
+
3053
+ /**
3054
+ * Compression configuration.
3055
+ */
3056
+ interface CompressionConfig {
3057
+ /** Compression format. @default 'gzip' */
3058
+ format?: 'gzip' | undefined;
3059
+ /** Compression level (1-9). @default 6 */
3060
+ level?: number | undefined;
3061
+ }
3062
+ /**
3063
+ * 將 AsyncIterable<string> 壓縮為 Buffer。
3064
+ * 適用於需要完整壓縮結果的場景(如 S3 Upload)。
3065
+ *
3066
+ * @param source - 輸入的字串串流
3067
+ * @param config - 壓縮設定
3068
+ * @returns 壓縮後的 Buffer
3069
+ *
3070
+ * @example
3071
+ * ```typescript
3072
+ * const source = (async function*() {
3073
+ * yield '<?xml version="1.0"?>'
3074
+ * yield '<urlset>...</urlset>'
3075
+ * })()
3076
+ * const compressed = await compressToBuffer(source)
3077
+ * ```
3078
+ */
3079
+ declare function compressToBuffer(source: AsyncIterable<string>, config?: CompressionConfig): Promise<Buffer>;
3080
+ /**
3081
+ * 回傳壓縮串流 Transform。
3082
+ * 適用於可直接 pipe 的場景(如 Disk write)。
3083
+ *
3084
+ * @param config - 壓縮設定
3085
+ * @returns Transform stream
3086
+ *
3087
+ * @example
3088
+ * ```typescript
3089
+ * const readable = Readable.from(source)
3090
+ * const gzip = createCompressionStream({ level: 9 })
3091
+ * const writeStream = createWriteStream('output.xml.gz')
3092
+ * await pipeline(readable, gzip, writeStream)
3093
+ * ```
3094
+ */
3095
+ declare function createCompressionStream(config?: CompressionConfig): Transform;
3096
+ /**
3097
+ * 將檔名轉換為 gzip 格式(加上 .gz 副檔名)。
3098
+ *
3099
+ * @param filename - 原始檔名
3100
+ * @returns 帶有 .gz 副檔名的檔名
3101
+ *
3102
+ * @example
3103
+ * ```typescript
3104
+ * toGzipFilename('sitemap.xml') // 'sitemap.xml.gz'
3105
+ * toGzipFilename('sitemap.xml.gz') // 'sitemap.xml.gz' (不重複添加)
3106
+ * ```
3107
+ */
3108
+ declare function toGzipFilename(filename: string): string;
3109
+ /**
3110
+ * 移除檔名的 .gz 副檔名。
3111
+ *
3112
+ * @param filename - gzip 檔名
3113
+ * @returns 移除 .gz 後的檔名
3114
+ *
3115
+ * @example
3116
+ * ```typescript
3117
+ * fromGzipFilename('sitemap.xml.gz') // 'sitemap.xml'
3118
+ * fromGzipFilename('sitemap.xml') // 'sitemap.xml'
3119
+ * ```
3120
+ */
3121
+ declare function fromGzipFilename(filename: string): string;
3122
+
3123
+ export { type AlternateUrl, type ChangeFreq, type ChangeTracker, type ChangeType, type CompressionConfig, type CompressionOptions, DiffCalculator, DiskSitemapStorage, type DynamicSitemapOptions, GCPSitemapStorage, type GCPSitemapStorageOptions, GenerateSitemapJob, type GenerateSitemapJobOptions, type I18nSitemapEntryOptions, IncrementalGenerator, type IncrementalGeneratorOptions, MemoryChangeTracker, MemoryLock, MemoryProgressStorage, MemoryRedirectManager, type MemoryRedirectManagerOptions, MemorySitemapStorage, OrbitSitemap, ProgressTracker, type ProgressTrackerOptions, RedirectDetector, type RedirectDetectorOptions, RedirectHandler, type RedirectHandlerOptions, type RedirectHandlingStrategy, type RedirectManager, type RedirectRule, RedisChangeTracker, type RedisClient, RedisLock, type RedisLockOptions, RedisProgressStorage, type RedisProgressStorageOptions, RedisRedirectManager, type RedisRedirectManagerOptions, RouteScanner, type RouteScannerOptions, S3SitemapStorage, type S3SitemapStorageOptions, ShadowProcessor, type ShadowProcessorOptions, type ShardInfo, type ShardManifest, type SitemapCache, type SitemapChange, type SitemapEntry, SitemapGenerator, type SitemapGeneratorOptions, type SitemapImage, SitemapIndex, type SitemapIndexEntry, type SitemapLock, type SitemapNews, type SitemapProgress, type SitemapProgressStorage, type SitemapProvider, type SitemapStorage, SitemapStream, type SitemapStreamOptions, type SitemapVideo, type StaticSitemapOptions, type WriteStreamOptions, compressToBuffer, createCompressionStream, fromGzipFilename, generateI18nEntries, routeScanner, toGzipFilename };