@gravito/constellation 3.0.2 → 3.1.1
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 +96 -251
- package/README.zh-TW.md +130 -13
- package/dist/{DiskSitemapStorage-WP6RITUN.js → DiskSitemapStorage-VLN5I24C.js} +1 -1
- package/dist/chunk-3IZTXYU7.js +166 -0
- package/dist/index.cjs +1400 -67
- package/dist/index.d.cts +1579 -148
- package/dist/index.d.ts +1579 -148
- package/dist/index.js +1293 -70
- package/package.json +11 -9
- package/dist/chunk-IS2H7U6M.js +0 -68
package/dist/index.d.ts
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
|
-
/**
|
|
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
|
*
|
|
@@ -511,6 +538,20 @@ interface RedirectManager {
|
|
|
511
538
|
*/
|
|
512
539
|
resolve(url: string, followChains?: boolean, maxChainLength?: number): Promise<string | null>;
|
|
513
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
|
+
}
|
|
514
555
|
|
|
515
556
|
/**
|
|
516
557
|
* Options for configuring the `MemoryChangeTracker`.
|
|
@@ -536,9 +577,31 @@ declare class MemoryChangeTracker implements ChangeTracker {
|
|
|
536
577
|
private urlIndex;
|
|
537
578
|
private maxChanges;
|
|
538
579
|
constructor(options?: MemoryChangeTrackerOptions);
|
|
580
|
+
/**
|
|
581
|
+
* Record a new site structure change in memory.
|
|
582
|
+
*
|
|
583
|
+
* @param change - The change event to record.
|
|
584
|
+
*/
|
|
539
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
|
+
*/
|
|
540
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
|
+
*/
|
|
541
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
|
+
*/
|
|
542
605
|
clear(since?: Date): Promise<void>;
|
|
543
606
|
}
|
|
544
607
|
/**
|
|
@@ -572,9 +635,31 @@ declare class RedisChangeTracker implements ChangeTracker {
|
|
|
572
635
|
constructor(options: RedisChangeTrackerOptions);
|
|
573
636
|
private getKey;
|
|
574
637
|
private getListKey;
|
|
638
|
+
/**
|
|
639
|
+
* Record a new site structure change in Redis.
|
|
640
|
+
*
|
|
641
|
+
* @param change - The change event to record.
|
|
642
|
+
*/
|
|
575
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
|
+
*/
|
|
576
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
|
+
*/
|
|
577
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
|
+
*/
|
|
578
663
|
clear(since?: Date): Promise<void>;
|
|
579
664
|
}
|
|
580
665
|
|
|
@@ -616,19 +701,35 @@ declare class DiffCalculator {
|
|
|
616
701
|
private batchSize;
|
|
617
702
|
constructor(options?: DiffCalculatorOptions);
|
|
618
703
|
/**
|
|
619
|
-
*
|
|
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.
|
|
620
709
|
*/
|
|
621
710
|
calculate(oldEntries: SitemapEntry[], newEntries: SitemapEntry[]): DiffResult;
|
|
622
711
|
/**
|
|
623
|
-
*
|
|
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.
|
|
624
717
|
*/
|
|
625
718
|
calculateBatch(oldEntries: AsyncIterable<SitemapEntry>, newEntries: AsyncIterable<SitemapEntry>): Promise<DiffResult>;
|
|
626
719
|
/**
|
|
627
|
-
*
|
|
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.
|
|
628
725
|
*/
|
|
629
726
|
calculateFromChanges(baseEntries: SitemapEntry[], changes: SitemapChange[]): DiffResult;
|
|
630
727
|
/**
|
|
631
|
-
*
|
|
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.
|
|
632
733
|
*/
|
|
633
734
|
private hasChanged;
|
|
634
735
|
}
|
|
@@ -682,23 +783,31 @@ declare class ShadowProcessor {
|
|
|
682
783
|
private mutex;
|
|
683
784
|
constructor(options: ShadowProcessorOptions);
|
|
684
785
|
/**
|
|
685
|
-
*
|
|
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.
|
|
686
792
|
*/
|
|
687
793
|
addOperation(operation: ShadowOperation): Promise<void>;
|
|
688
794
|
/**
|
|
689
|
-
*
|
|
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).
|
|
690
799
|
*/
|
|
691
800
|
commit(): Promise<void>;
|
|
692
801
|
/**
|
|
693
|
-
*
|
|
802
|
+
* Cancels all staged shadow operations without committing them.
|
|
694
803
|
*/
|
|
695
804
|
rollback(): Promise<void>;
|
|
696
805
|
/**
|
|
697
|
-
*
|
|
806
|
+
* Returns the unique identifier for the current shadow session.
|
|
698
807
|
*/
|
|
699
808
|
getShadowId(): string;
|
|
700
809
|
/**
|
|
701
|
-
*
|
|
810
|
+
* Returns an array of all staged shadow operations.
|
|
702
811
|
*/
|
|
703
812
|
getOperations(): ShadowOperation[];
|
|
704
813
|
}
|
|
@@ -733,6 +842,8 @@ interface SitemapGeneratorOptions extends SitemapStreamOptions {
|
|
|
733
842
|
total: number;
|
|
734
843
|
percentage: number;
|
|
735
844
|
}) => void;
|
|
845
|
+
/** Compression configuration for reducing sitemap file sizes. @since 3.1.0 */
|
|
846
|
+
compression?: CompressionOptions;
|
|
736
847
|
}
|
|
737
848
|
/**
|
|
738
849
|
* SitemapGenerator is the primary orchestrator for creating sitemaps in Gravito.
|
|
@@ -758,19 +869,56 @@ declare class SitemapGenerator {
|
|
|
758
869
|
private options;
|
|
759
870
|
private shadowProcessor;
|
|
760
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
|
+
*/
|
|
761
878
|
run(): Promise<void>;
|
|
879
|
+
/**
|
|
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
|
+
*/
|
|
762
891
|
private normalizeUrl;
|
|
763
892
|
/**
|
|
764
|
-
*
|
|
893
|
+
* Returns the shadow processor instance if enabled.
|
|
765
894
|
*/
|
|
766
895
|
getShadowProcessor(): ShadowProcessor | null;
|
|
767
896
|
}
|
|
768
897
|
|
|
898
|
+
/**
|
|
899
|
+
* Options for configuring the `IncrementalGenerator`.
|
|
900
|
+
*
|
|
901
|
+
* @public
|
|
902
|
+
* @since 3.0.0
|
|
903
|
+
*/
|
|
769
904
|
interface IncrementalGeneratorOptions extends SitemapGeneratorOptions {
|
|
905
|
+
/** The change tracker used to retrieve and record structural site changes. */
|
|
770
906
|
changeTracker: ChangeTracker;
|
|
907
|
+
/** Optional diff calculator to identify added, updated, or removed entries. */
|
|
771
908
|
diffCalculator?: DiffCalculator;
|
|
909
|
+
/** Whether to automatically track new entries discovered during full generation. @default true */
|
|
772
910
|
autoTrack?: boolean;
|
|
773
911
|
}
|
|
912
|
+
/**
|
|
913
|
+
* IncrementalGenerator manages partial updates to the sitemap based on detected changes.
|
|
914
|
+
*
|
|
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.
|
|
918
|
+
*
|
|
919
|
+
* @public
|
|
920
|
+
* @since 3.0.0
|
|
921
|
+
*/
|
|
774
922
|
declare class IncrementalGenerator {
|
|
775
923
|
private options;
|
|
776
924
|
private changeTracker;
|
|
@@ -778,15 +926,50 @@ declare class IncrementalGenerator {
|
|
|
778
926
|
private generator;
|
|
779
927
|
private mutex;
|
|
780
928
|
constructor(options: IncrementalGeneratorOptions);
|
|
929
|
+
/**
|
|
930
|
+
* Performs a full sitemap generation and optionally records all entries in the change tracker.
|
|
931
|
+
*/
|
|
781
932
|
generateFull(): Promise<void>;
|
|
933
|
+
/**
|
|
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.
|
|
940
|
+
*/
|
|
782
941
|
generateIncremental(since?: Date): Promise<void>;
|
|
942
|
+
/**
|
|
943
|
+
* Internal implementation of full sitemap generation.
|
|
944
|
+
*/
|
|
783
945
|
private performFullGeneration;
|
|
946
|
+
/**
|
|
947
|
+
* Internal implementation of incremental sitemap generation.
|
|
948
|
+
*/
|
|
784
949
|
private performIncrementalGeneration;
|
|
950
|
+
/**
|
|
951
|
+
* Normalizes a URL to an absolute URL using the base URL.
|
|
952
|
+
*/
|
|
785
953
|
private normalizeUrl;
|
|
954
|
+
/**
|
|
955
|
+
* Loads the sitemap shard manifest from storage.
|
|
956
|
+
*/
|
|
786
957
|
private loadManifest;
|
|
958
|
+
/**
|
|
959
|
+
* Identifies which shards are affected by the given set of changes.
|
|
960
|
+
*/
|
|
787
961
|
private getAffectedShards;
|
|
962
|
+
/**
|
|
963
|
+
* Updates the affected shards in storage.
|
|
964
|
+
*/
|
|
788
965
|
private updateShards;
|
|
966
|
+
/**
|
|
967
|
+
* Applies changes to a set of sitemap entries and returns the updated, sorted list.
|
|
968
|
+
*/
|
|
789
969
|
private applyChanges;
|
|
970
|
+
/**
|
|
971
|
+
* Helper to convert an async iterable into an array.
|
|
972
|
+
*/
|
|
790
973
|
private toArray;
|
|
791
974
|
}
|
|
792
975
|
|
|
@@ -818,31 +1001,44 @@ declare class ProgressTracker {
|
|
|
818
1001
|
private updateTimer;
|
|
819
1002
|
constructor(options: ProgressTrackerOptions);
|
|
820
1003
|
/**
|
|
821
|
-
*
|
|
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.
|
|
822
1008
|
*/
|
|
823
1009
|
init(jobId: string, total: number): Promise<void>;
|
|
824
1010
|
/**
|
|
825
|
-
*
|
|
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.
|
|
826
1018
|
*/
|
|
827
1019
|
update(processed: number, status?: SitemapProgress['status']): Promise<void>;
|
|
828
1020
|
/**
|
|
829
|
-
*
|
|
1021
|
+
* Marks the current job as successfully completed.
|
|
830
1022
|
*/
|
|
831
1023
|
complete(): Promise<void>;
|
|
832
1024
|
/**
|
|
833
|
-
*
|
|
1025
|
+
* Marks the current job as failed with an error message.
|
|
1026
|
+
*
|
|
1027
|
+
* @param error - The error message describing why the job failed.
|
|
834
1028
|
*/
|
|
835
1029
|
fail(error: string): Promise<void>;
|
|
836
1030
|
/**
|
|
837
|
-
*
|
|
1031
|
+
* Flushes the current progress state to the storage backend.
|
|
838
1032
|
*/
|
|
839
1033
|
private flush;
|
|
840
1034
|
/**
|
|
841
|
-
*
|
|
1035
|
+
* Stops the periodic update timer.
|
|
842
1036
|
*/
|
|
843
1037
|
private stop;
|
|
844
1038
|
/**
|
|
845
|
-
*
|
|
1039
|
+
* Returns a copy of the current progress state.
|
|
1040
|
+
*
|
|
1041
|
+
* @returns The current SitemapProgress object, or null if no job is active.
|
|
846
1042
|
*/
|
|
847
1043
|
getCurrentProgress(): SitemapProgress | null;
|
|
848
1044
|
}
|
|
@@ -868,9 +1064,29 @@ declare class SitemapIndex {
|
|
|
868
1064
|
private options;
|
|
869
1065
|
private entries;
|
|
870
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
|
+
*/
|
|
871
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
|
+
*/
|
|
872
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
|
+
*/
|
|
873
1086
|
toXML(): string;
|
|
1087
|
+
/**
|
|
1088
|
+
* Escapes special XML characters in a string.
|
|
1089
|
+
*/
|
|
874
1090
|
private escape;
|
|
875
1091
|
}
|
|
876
1092
|
|
|
@@ -899,11 +1115,70 @@ declare class SitemapStream {
|
|
|
899
1115
|
private entries;
|
|
900
1116
|
private flags;
|
|
901
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
|
+
*/
|
|
902
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
|
+
*/
|
|
903
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
|
+
*/
|
|
904
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
|
+
*/
|
|
905
1172
|
private renderUrl;
|
|
1173
|
+
/**
|
|
1174
|
+
* Escapes special XML characters in a string.
|
|
1175
|
+
*/
|
|
906
1176
|
private escape;
|
|
1177
|
+
/**
|
|
1178
|
+
* Returns all entries currently in the stream.
|
|
1179
|
+
*
|
|
1180
|
+
* @returns An array of `SitemapEntry` objects.
|
|
1181
|
+
*/
|
|
907
1182
|
getEntries(): SitemapEntry[];
|
|
908
1183
|
}
|
|
909
1184
|
|
|
@@ -915,10 +1190,18 @@ type I18nSitemapEntryOptions = Omit<SitemapEntry, 'url' | 'alternates'>;
|
|
|
915
1190
|
/**
|
|
916
1191
|
* Generate fully cross-referenced SitemapEntries for multiple locales.
|
|
917
1192
|
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
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
|
|
922
1205
|
*/
|
|
923
1206
|
declare function generateI18nEntries(path: string, locales: string[], baseUrl?: string, options?: I18nSitemapEntryOptions): SitemapEntry[];
|
|
924
1207
|
|
|
@@ -964,154 +1247,882 @@ declare class GenerateSitemapJob extends Job {
|
|
|
964
1247
|
private totalEntries;
|
|
965
1248
|
private processedEntries;
|
|
966
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
|
+
*/
|
|
967
1256
|
handle(): Promise<void>;
|
|
968
1257
|
/**
|
|
969
|
-
*
|
|
1258
|
+
* Calculates the total number of URL entries from all providers.
|
|
1259
|
+
*
|
|
1260
|
+
* @returns A promise resolving to the total entry count.
|
|
970
1261
|
*/
|
|
971
1262
|
private calculateTotal;
|
|
972
1263
|
/**
|
|
973
|
-
*
|
|
1264
|
+
* Performs sitemap generation while reporting progress to the tracker and callback.
|
|
974
1265
|
*/
|
|
975
1266
|
private generateWithProgress;
|
|
976
1267
|
}
|
|
977
1268
|
|
|
978
1269
|
/**
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
* Useful for small to medium sites where fresh data is critical. Dynamic sitemaps
|
|
982
|
-
* are generated on-the-fly and can be cached at the HTTP level.
|
|
983
|
-
*
|
|
984
|
-
* @public
|
|
985
|
-
* @since 3.0.0
|
|
986
|
-
*/
|
|
987
|
-
interface DynamicSitemapOptions extends SitemapStreamOptions {
|
|
988
|
-
/** The URL path where the sitemap will be exposed. @default '/sitemap.xml' */
|
|
989
|
-
path?: string | undefined;
|
|
990
|
-
/** List of sitemap entry providers to scan for content. */
|
|
991
|
-
providers: SitemapProvider[];
|
|
992
|
-
/** Cache duration in seconds for the HTTP response. @default undefined (no-cache) */
|
|
993
|
-
cacheSeconds?: number | undefined;
|
|
994
|
-
/** Persistence backend for cached XML files. Defaults to MemorySitemapStorage. */
|
|
995
|
-
storage?: SitemapStorage | undefined;
|
|
996
|
-
/** Optional distributed lock to prevent "cache stampede" during heavy generation. */
|
|
997
|
-
lock?: SitemapLock | undefined;
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Configuration for a statically pre-generated sitemap.
|
|
1270
|
+
* In-memory lock implementation for single-instance sitemap generation.
|
|
1001
1271
|
*
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
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.
|
|
1004
1275
|
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
/** Local directory where generated XML files will be saved. */
|
|
1010
|
-
outDir: string;
|
|
1011
|
-
/** The name of the root sitemap file. @default 'sitemap.xml' */
|
|
1012
|
-
filename?: string | undefined;
|
|
1013
|
-
/** List of sitemap entry providers to scan for content. */
|
|
1014
|
-
providers: SitemapProvider[];
|
|
1015
|
-
/** Custom storage backend. Defaults to DiskSitemapStorage using `outDir`. */
|
|
1016
|
-
storage?: SitemapStorage | undefined;
|
|
1017
|
-
/** Optional incremental generation settings to only update changed URLs. */
|
|
1018
|
-
incremental?: {
|
|
1019
|
-
/** Whether to enable incremental builds. */
|
|
1020
|
-
enabled: boolean;
|
|
1021
|
-
/** Backend for tracking structural site changes. */
|
|
1022
|
-
changeTracker: ChangeTracker;
|
|
1023
|
-
/** Whether to automatically record new changes during scan. @default false */
|
|
1024
|
-
autoTrack?: boolean;
|
|
1025
|
-
};
|
|
1026
|
-
/** Optional SEO redirect orchestration settings. */
|
|
1027
|
-
redirect?: {
|
|
1028
|
-
/** Whether to automatically handle 301/302 redirects found in sitemap. */
|
|
1029
|
-
enabled: boolean;
|
|
1030
|
-
/** Backend for managing and resolving redirect rules. */
|
|
1031
|
-
manager: RedirectManager;
|
|
1032
|
-
/** Strategy for resolving conflicting or chained redirects. @default 'remove_old_add_new' */
|
|
1033
|
-
strategy?: 'remove_old_add_new' | 'keep_relation' | 'update_url' | 'dual_mark';
|
|
1034
|
-
/** Whether to resolve redirect chains into a single jump. @default false */
|
|
1035
|
-
followChains?: boolean;
|
|
1036
|
-
/** Maximum number of redirect jumps to follow. @default 5 */
|
|
1037
|
-
maxChainLength?: number;
|
|
1038
|
-
};
|
|
1039
|
-
/** Deployment settings for atomic sitemap updates via shadow staging. */
|
|
1040
|
-
shadow?: {
|
|
1041
|
-
/** Whether to enable "shadow" (atomic staging) mode. */
|
|
1042
|
-
enabled: boolean;
|
|
1043
|
-
/** The update strategy: 'atomic' (full swap) or 'versioned' (archived). @default 'atomic' */
|
|
1044
|
-
mode: 'atomic' | 'versioned';
|
|
1045
|
-
};
|
|
1046
|
-
/** Persistence backend for long-running job progress tracking. */
|
|
1047
|
-
progressStorage?: SitemapProgressStorage;
|
|
1048
|
-
}
|
|
1049
|
-
/**
|
|
1050
|
-
* 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
|
|
1051
1280
|
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
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
|
|
1054
1285
|
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
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
|
|
1058
1291
|
*
|
|
1059
|
-
* @example
|
|
1292
|
+
* @example Basic usage with try-finally pattern
|
|
1060
1293
|
* ```typescript
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
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
|
+
* }
|
|
1066
1311
|
* ```
|
|
1067
1312
|
*
|
|
1068
|
-
* @example
|
|
1313
|
+
* @example Handling lock contention
|
|
1069
1314
|
* ```typescript
|
|
1070
|
-
* const
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
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
|
+
* }
|
|
1076
1323
|
* ```
|
|
1077
1324
|
*
|
|
1078
1325
|
* @public
|
|
1079
|
-
* @since 3.
|
|
1326
|
+
* @since 3.1.0
|
|
1080
1327
|
*/
|
|
1081
|
-
declare class
|
|
1082
|
-
private options;
|
|
1083
|
-
private mode;
|
|
1084
|
-
private constructor();
|
|
1328
|
+
declare class MemoryLock implements SitemapLock {
|
|
1085
1329
|
/**
|
|
1086
|
-
*
|
|
1330
|
+
* Internal map storing resource identifiers to their lock expiration timestamps.
|
|
1087
1331
|
*
|
|
1088
|
-
*
|
|
1089
|
-
*
|
|
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.
|
|
1090
1335
|
*/
|
|
1091
|
-
|
|
1336
|
+
private locks;
|
|
1092
1337
|
/**
|
|
1093
|
-
*
|
|
1338
|
+
* Attempts to acquire an exclusive lock on the specified resource.
|
|
1094
1339
|
*
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
*
|
|
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
|
+
* ```
|
|
1385
|
+
*/
|
|
1386
|
+
acquire(resource: string, ttl: number): Promise<boolean>;
|
|
1387
|
+
/**
|
|
1388
|
+
* Releases the lock on the specified resource, allowing others to acquire it.
|
|
1389
|
+
*
|
|
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
|
+
/** Maximum number of entries per individual sitemap file. @default 50000 */
|
|
2022
|
+
maxEntriesPerFile?: number | undefined;
|
|
2023
|
+
/** Custom storage backend. Defaults to DiskSitemapStorage using `outDir`. */
|
|
2024
|
+
storage?: SitemapStorage | undefined;
|
|
2025
|
+
/** Optional incremental generation settings to only update changed URLs. */
|
|
2026
|
+
incremental?: {
|
|
2027
|
+
/** Whether to enable incremental builds. */
|
|
2028
|
+
enabled: boolean;
|
|
2029
|
+
/** Backend for tracking structural site changes. */
|
|
2030
|
+
changeTracker: ChangeTracker;
|
|
2031
|
+
/** Whether to automatically record new changes during scan. @default false */
|
|
2032
|
+
autoTrack?: boolean;
|
|
2033
|
+
};
|
|
2034
|
+
/** Optional SEO redirect orchestration settings. */
|
|
2035
|
+
redirect?: {
|
|
2036
|
+
/** Whether to automatically handle 301/302 redirects found in sitemap. */
|
|
2037
|
+
enabled: boolean;
|
|
2038
|
+
/** Backend for managing and resolving redirect rules. */
|
|
2039
|
+
manager: RedirectManager;
|
|
2040
|
+
/** Strategy for resolving conflicting or chained redirects. @default 'remove_old_add_new' */
|
|
2041
|
+
strategy?: 'remove_old_add_new' | 'keep_relation' | 'update_url' | 'dual_mark';
|
|
2042
|
+
/** Whether to resolve redirect chains into a single jump. @default false */
|
|
2043
|
+
followChains?: boolean;
|
|
2044
|
+
/** Maximum number of redirect jumps to follow. @default 5 */
|
|
2045
|
+
maxChainLength?: number;
|
|
2046
|
+
};
|
|
2047
|
+
/** Deployment settings for atomic sitemap updates via shadow staging. */
|
|
2048
|
+
shadow?: {
|
|
2049
|
+
/** Whether to enable "shadow" (atomic staging) mode. */
|
|
2050
|
+
enabled: boolean;
|
|
2051
|
+
/** The update strategy: 'atomic' (full swap) or 'versioned' (archived). @default 'atomic' */
|
|
2052
|
+
mode: 'atomic' | 'versioned';
|
|
2053
|
+
};
|
|
2054
|
+
/** Persistence backend for long-running job progress tracking. */
|
|
2055
|
+
progressStorage?: SitemapProgressStorage;
|
|
2056
|
+
}
|
|
2057
|
+
/**
|
|
2058
|
+
* OrbitSitemap is the enterprise SEO orchestration module for Gravito.
|
|
2059
|
+
*
|
|
2060
|
+
* It provides advanced sitemap generation supporting large-scale indexing,
|
|
2061
|
+
* automatic 301/302 redirect handling, and atomic sitemap deployments.
|
|
2062
|
+
*
|
|
2063
|
+
* It can operate in two modes:
|
|
2064
|
+
* 1. **Dynamic**: Sitemaps are generated on-the-fly and cached.
|
|
2065
|
+
* 2. **Static**: Sitemaps are pre-built (e.g., during CI/CD) and served from disk or cloud storage.
|
|
2066
|
+
*
|
|
2067
|
+
* @example Dynamic Mode
|
|
2068
|
+
* ```typescript
|
|
2069
|
+
* const sitemap = OrbitSitemap.dynamic({
|
|
2070
|
+
* baseUrl: 'https://example.com',
|
|
2071
|
+
* providers: [new PostSitemapProvider()]
|
|
2072
|
+
* });
|
|
2073
|
+
* core.addOrbit(sitemap);
|
|
2074
|
+
* ```
|
|
2075
|
+
*
|
|
2076
|
+
* @example Static Mode
|
|
2077
|
+
* ```typescript
|
|
2078
|
+
* const sitemap = OrbitSitemap.static({
|
|
2079
|
+
* baseUrl: 'https://example.com',
|
|
2080
|
+
* outDir: './public',
|
|
2081
|
+
* shadow: { enabled: true, mode: 'atomic' }
|
|
2082
|
+
* });
|
|
2083
|
+
* await sitemap.generate();
|
|
2084
|
+
* ```
|
|
2085
|
+
*
|
|
2086
|
+
* @public
|
|
2087
|
+
* @since 3.0.0
|
|
2088
|
+
*/
|
|
2089
|
+
declare class OrbitSitemap implements GravitoOrbit {
|
|
2090
|
+
private options;
|
|
2091
|
+
private mode;
|
|
2092
|
+
private constructor();
|
|
2093
|
+
/**
|
|
2094
|
+
* Create a dynamic sitemap configuration.
|
|
2095
|
+
*
|
|
2096
|
+
* @param options - The dynamic sitemap options.
|
|
2097
|
+
* @returns An OrbitSitemap instance configured for dynamic generation.
|
|
2098
|
+
*/
|
|
2099
|
+
static dynamic(options: DynamicSitemapOptions): OrbitSitemap;
|
|
2100
|
+
/**
|
|
2101
|
+
* Create a static sitemap configuration.
|
|
2102
|
+
*
|
|
2103
|
+
* @param options - The static sitemap options.
|
|
2104
|
+
* @returns An OrbitSitemap instance configured for static generation.
|
|
2105
|
+
*/
|
|
2106
|
+
static static(options: StaticSitemapOptions): OrbitSitemap;
|
|
2107
|
+
/**
|
|
2108
|
+
* Installs the sitemap module into PlanetCore.
|
|
1101
2109
|
*
|
|
1102
2110
|
* @param core - The PlanetCore instance.
|
|
1103
2111
|
*/
|
|
1104
2112
|
install(core: PlanetCore): void;
|
|
2113
|
+
/**
|
|
2114
|
+
* Internal method to set up dynamic sitemap routes.
|
|
2115
|
+
*/
|
|
1105
2116
|
private installDynamic;
|
|
1106
2117
|
/**
|
|
1107
|
-
*
|
|
2118
|
+
* Generates the sitemap (static mode only).
|
|
1108
2119
|
*
|
|
1109
2120
|
* @returns A promise that resolves when generation is complete.
|
|
1110
2121
|
* @throws {Error} If called in dynamic mode.
|
|
1111
2122
|
*/
|
|
1112
2123
|
generate(): Promise<void>;
|
|
1113
2124
|
/**
|
|
1114
|
-
*
|
|
2125
|
+
* Generates incremental sitemap updates (static mode only).
|
|
1115
2126
|
*
|
|
1116
2127
|
* @param since - Only include items modified since this date.
|
|
1117
2128
|
* @returns A promise that resolves when incremental generation is complete.
|
|
@@ -1119,7 +2130,7 @@ declare class OrbitSitemap implements GravitoOrbit {
|
|
|
1119
2130
|
*/
|
|
1120
2131
|
generateIncremental(since?: Date): Promise<void>;
|
|
1121
2132
|
/**
|
|
1122
|
-
*
|
|
2133
|
+
* Generates sitemap asynchronously in the background (static mode only).
|
|
1123
2134
|
*
|
|
1124
2135
|
* @param options - Options for the async generation job.
|
|
1125
2136
|
* @returns A promise resolving to the job ID.
|
|
@@ -1137,7 +2148,7 @@ declare class OrbitSitemap implements GravitoOrbit {
|
|
|
1137
2148
|
onError?: (error: Error) => void;
|
|
1138
2149
|
}): Promise<string>;
|
|
1139
2150
|
/**
|
|
1140
|
-
*
|
|
2151
|
+
* Installs API endpoints for triggering and monitoring sitemap generation.
|
|
1141
2152
|
*
|
|
1142
2153
|
* @param core - The PlanetCore instance.
|
|
1143
2154
|
* @param basePath - The base path for the API endpoints (default: '/admin/sitemap').
|
|
@@ -1186,9 +2197,12 @@ declare class RouteScanner implements SitemapProvider {
|
|
|
1186
2197
|
private options;
|
|
1187
2198
|
constructor(router: any, options?: RouteScannerOptions);
|
|
1188
2199
|
/**
|
|
1189
|
-
*
|
|
2200
|
+
* Scans the router and returns discovered static GET routes as sitemap entries.
|
|
2201
|
+
*
|
|
2202
|
+
* This method iterates through all registered routes in the Gravito router,
|
|
2203
|
+
* applying inclusion/exclusion filters and defaulting metadata for matching routes.
|
|
1190
2204
|
*
|
|
1191
|
-
* @returns An array of
|
|
2205
|
+
* @returns An array of `SitemapEntry` objects.
|
|
1192
2206
|
*/
|
|
1193
2207
|
getEntries(): SitemapEntry[];
|
|
1194
2208
|
private extractRoutes;
|
|
@@ -1298,27 +2312,33 @@ declare class RedirectDetector {
|
|
|
1298
2312
|
private cache;
|
|
1299
2313
|
constructor(options: RedirectDetectorOptions);
|
|
1300
2314
|
/**
|
|
1301
|
-
*
|
|
2315
|
+
* Detects redirects for a single URL using multiple strategies.
|
|
2316
|
+
*
|
|
2317
|
+
* @param url - The URL path to probe for redirects.
|
|
2318
|
+
* @returns A promise resolving to a `RedirectRule` if a redirect is found, or null.
|
|
1302
2319
|
*/
|
|
1303
2320
|
detect(url: string): Promise<RedirectRule | null>;
|
|
1304
2321
|
/**
|
|
1305
|
-
*
|
|
2322
|
+
* Batch detects redirects for multiple URLs with concurrency control.
|
|
2323
|
+
*
|
|
2324
|
+
* @param urls - An array of URL paths to probe.
|
|
2325
|
+
* @returns A promise resolving to a Map of URLs to their respective `RedirectRule` or null.
|
|
1306
2326
|
*/
|
|
1307
2327
|
detectBatch(urls: string[]): Promise<Map<string, RedirectRule | null>>;
|
|
1308
2328
|
/**
|
|
1309
|
-
*
|
|
2329
|
+
* Detects a redirect from the configured database table.
|
|
1310
2330
|
*/
|
|
1311
2331
|
private detectFromDatabase;
|
|
1312
2332
|
/**
|
|
1313
|
-
*
|
|
2333
|
+
* Detects a redirect from a static JSON configuration file.
|
|
1314
2334
|
*/
|
|
1315
2335
|
private detectFromConfig;
|
|
1316
2336
|
/**
|
|
1317
|
-
*
|
|
2337
|
+
* Auto-detects a redirect by sending an HTTP HEAD request.
|
|
1318
2338
|
*/
|
|
1319
2339
|
private detectAuto;
|
|
1320
2340
|
/**
|
|
1321
|
-
*
|
|
2341
|
+
* Caches the detection result for a URL.
|
|
1322
2342
|
*/
|
|
1323
2343
|
private cacheResult;
|
|
1324
2344
|
}
|
|
@@ -1367,23 +2387,26 @@ declare class RedirectHandler {
|
|
|
1367
2387
|
private options;
|
|
1368
2388
|
constructor(options: RedirectHandlerOptions);
|
|
1369
2389
|
/**
|
|
1370
|
-
*
|
|
2390
|
+
* Processes a list of sitemap entries and handles redirects according to the configured strategy.
|
|
2391
|
+
*
|
|
2392
|
+
* @param entries - The original list of sitemap entries.
|
|
2393
|
+
* @returns A promise resolving to the processed list of entries.
|
|
1371
2394
|
*/
|
|
1372
2395
|
processEntries(entries: SitemapEntry[]): Promise<SitemapEntry[]>;
|
|
1373
2396
|
/**
|
|
1374
|
-
*
|
|
2397
|
+
* Strategy 1: Remove old URL and add the new destination URL.
|
|
1375
2398
|
*/
|
|
1376
2399
|
private handleRemoveOldAddNew;
|
|
1377
2400
|
/**
|
|
1378
|
-
*
|
|
2401
|
+
* Strategy 2: Keep the original URL but mark the destination as canonical.
|
|
1379
2402
|
*/
|
|
1380
2403
|
private handleKeepRelation;
|
|
1381
2404
|
/**
|
|
1382
|
-
*
|
|
2405
|
+
* Strategy 3: Silently update the URL to the destination.
|
|
1383
2406
|
*/
|
|
1384
2407
|
private handleUpdateUrl;
|
|
1385
2408
|
/**
|
|
1386
|
-
*
|
|
2409
|
+
* Strategy 4: Include both the original and destination URLs.
|
|
1387
2410
|
*/
|
|
1388
2411
|
private handleDualMark;
|
|
1389
2412
|
}
|
|
@@ -1411,10 +2434,39 @@ declare class MemoryRedirectManager implements RedirectManager {
|
|
|
1411
2434
|
private rules;
|
|
1412
2435
|
private maxRules;
|
|
1413
2436
|
constructor(options?: MemoryRedirectManagerOptions);
|
|
2437
|
+
/**
|
|
2438
|
+
* Registers a single redirect rule in memory.
|
|
2439
|
+
*
|
|
2440
|
+
* @param redirect - The redirect rule to add.
|
|
2441
|
+
*/
|
|
1414
2442
|
register(redirect: RedirectRule): Promise<void>;
|
|
2443
|
+
/**
|
|
2444
|
+
* Registers multiple redirect rules in memory.
|
|
2445
|
+
*
|
|
2446
|
+
* @param redirects - An array of redirect rules.
|
|
2447
|
+
*/
|
|
1415
2448
|
registerBatch(redirects: RedirectRule[]): Promise<void>;
|
|
2449
|
+
/**
|
|
2450
|
+
* Retrieves a specific redirect rule by its source path from memory.
|
|
2451
|
+
*
|
|
2452
|
+
* @param from - The source path.
|
|
2453
|
+
* @returns A promise resolving to the redirect rule, or null if not found.
|
|
2454
|
+
*/
|
|
1416
2455
|
get(from: string): Promise<RedirectRule | null>;
|
|
2456
|
+
/**
|
|
2457
|
+
* Retrieves all registered redirect rules from memory.
|
|
2458
|
+
*
|
|
2459
|
+
* @returns A promise resolving to an array of all redirect rules.
|
|
2460
|
+
*/
|
|
1417
2461
|
getAll(): Promise<RedirectRule[]>;
|
|
2462
|
+
/**
|
|
2463
|
+
* Resolves a URL to its final destination through the redirect table.
|
|
2464
|
+
*
|
|
2465
|
+
* @param url - The URL to resolve.
|
|
2466
|
+
* @param followChains - Whether to recursively resolve chained redirects.
|
|
2467
|
+
* @param maxChainLength - Maximum depth for chain resolution.
|
|
2468
|
+
* @returns A promise resolving to the final destination URL.
|
|
2469
|
+
*/
|
|
1418
2470
|
resolve(url: string, followChains?: boolean, maxChainLength?: number): Promise<string | null>;
|
|
1419
2471
|
}
|
|
1420
2472
|
/**
|
|
@@ -1448,10 +2500,39 @@ declare class RedisRedirectManager implements RedirectManager {
|
|
|
1448
2500
|
constructor(options: RedisRedirectManagerOptions);
|
|
1449
2501
|
private getKey;
|
|
1450
2502
|
private getListKey;
|
|
2503
|
+
/**
|
|
2504
|
+
* Registers a single redirect rule in Redis.
|
|
2505
|
+
*
|
|
2506
|
+
* @param redirect - The redirect rule to add.
|
|
2507
|
+
*/
|
|
1451
2508
|
register(redirect: RedirectRule): Promise<void>;
|
|
2509
|
+
/**
|
|
2510
|
+
* Registers multiple redirect rules in Redis.
|
|
2511
|
+
*
|
|
2512
|
+
* @param redirects - An array of redirect rules.
|
|
2513
|
+
*/
|
|
1452
2514
|
registerBatch(redirects: RedirectRule[]): Promise<void>;
|
|
2515
|
+
/**
|
|
2516
|
+
* Retrieves a specific redirect rule by its source path from Redis.
|
|
2517
|
+
*
|
|
2518
|
+
* @param from - The source path.
|
|
2519
|
+
* @returns A promise resolving to the redirect rule, or null if not found.
|
|
2520
|
+
*/
|
|
1453
2521
|
get(from: string): Promise<RedirectRule | null>;
|
|
2522
|
+
/**
|
|
2523
|
+
* Retrieves all registered redirect rules from Redis.
|
|
2524
|
+
*
|
|
2525
|
+
* @returns A promise resolving to an array of all redirect rules.
|
|
2526
|
+
*/
|
|
1454
2527
|
getAll(): Promise<RedirectRule[]>;
|
|
2528
|
+
/**
|
|
2529
|
+
* Resolves a URL to its final destination through the Redis redirect table.
|
|
2530
|
+
*
|
|
2531
|
+
* @param url - The URL to resolve.
|
|
2532
|
+
* @param followChains - Whether to recursively resolve chained redirects.
|
|
2533
|
+
* @param maxChainLength - Maximum depth for chain resolution.
|
|
2534
|
+
* @returns A promise resolving to the final destination URL.
|
|
2535
|
+
*/
|
|
1455
2536
|
resolve(url: string, followChains?: boolean, maxChainLength?: number): Promise<string | null>;
|
|
1456
2537
|
}
|
|
1457
2538
|
|
|
@@ -1475,10 +2556,51 @@ declare class DiskSitemapStorage implements SitemapStorage {
|
|
|
1475
2556
|
private outDir;
|
|
1476
2557
|
private baseUrl;
|
|
1477
2558
|
constructor(outDir: string, baseUrl: string);
|
|
2559
|
+
/**
|
|
2560
|
+
* Writes sitemap content to a file on the local disk.
|
|
2561
|
+
*
|
|
2562
|
+
* @param filename - The name of the file to write.
|
|
2563
|
+
* @param content - The XML or JSON content.
|
|
2564
|
+
*/
|
|
1478
2565
|
write(filename: string, content: string): Promise<void>;
|
|
2566
|
+
/**
|
|
2567
|
+
* 使用串流方式寫入 sitemap 檔案,可選擇性啟用 gzip 壓縮。
|
|
2568
|
+
* 此方法可大幅降低大型 sitemap 的記憶體峰值。
|
|
2569
|
+
*
|
|
2570
|
+
* @param filename - 檔案名稱
|
|
2571
|
+
* @param stream - XML 內容的 AsyncIterable
|
|
2572
|
+
* @param options - 寫入選項(如壓縮、content type)
|
|
2573
|
+
*
|
|
2574
|
+
* @since 3.1.0
|
|
2575
|
+
*/
|
|
2576
|
+
writeStream(filename: string, stream: AsyncIterable<string>, options?: WriteStreamOptions): Promise<void>;
|
|
2577
|
+
/**
|
|
2578
|
+
* Reads sitemap content from a file on the local disk.
|
|
2579
|
+
*
|
|
2580
|
+
* @param filename - The name of the file to read.
|
|
2581
|
+
* @returns A promise resolving to the file content as a string, or null if not found.
|
|
2582
|
+
*/
|
|
1479
2583
|
read(filename: string): Promise<string | null>;
|
|
2584
|
+
/**
|
|
2585
|
+
* Returns a readable stream for a sitemap file on the local disk.
|
|
2586
|
+
*
|
|
2587
|
+
* @param filename - The name of the file to stream.
|
|
2588
|
+
* @returns A promise resolving to an async iterable of file chunks, or null if not found.
|
|
2589
|
+
*/
|
|
1480
2590
|
readStream(filename: string): Promise<AsyncIterable<string> | null>;
|
|
2591
|
+
/**
|
|
2592
|
+
* Checks if a sitemap file exists on the local disk.
|
|
2593
|
+
*
|
|
2594
|
+
* @param filename - The name of the file to check.
|
|
2595
|
+
* @returns A promise resolving to true if the file exists, false otherwise.
|
|
2596
|
+
*/
|
|
1481
2597
|
exists(filename: string): Promise<boolean>;
|
|
2598
|
+
/**
|
|
2599
|
+
* Returns the full public URL for a sitemap file.
|
|
2600
|
+
*
|
|
2601
|
+
* @param filename - The name of the sitemap file.
|
|
2602
|
+
* @returns The public URL as a string.
|
|
2603
|
+
*/
|
|
1482
2604
|
getUrl(filename: string): string;
|
|
1483
2605
|
}
|
|
1484
2606
|
|
|
@@ -1536,27 +2658,125 @@ declare class GCPSitemapStorage implements SitemapStorage {
|
|
|
1536
2658
|
constructor(options: GCPSitemapStorageOptions);
|
|
1537
2659
|
private getStorageClient;
|
|
1538
2660
|
private getKey;
|
|
2661
|
+
/**
|
|
2662
|
+
* Writes sitemap content to a Google Cloud Storage object.
|
|
2663
|
+
*
|
|
2664
|
+
* @param filename - The name of the file to write.
|
|
2665
|
+
* @param content - The XML or JSON content.
|
|
2666
|
+
*/
|
|
1539
2667
|
write(filename: string, content: string): Promise<void>;
|
|
2668
|
+
/**
|
|
2669
|
+
* 使用串流方式寫入 sitemap 至 GCP Cloud Storage,可選擇性啟用 gzip 壓縮。
|
|
2670
|
+
*
|
|
2671
|
+
* @param filename - 檔案名稱
|
|
2672
|
+
* @param stream - XML 內容的 AsyncIterable
|
|
2673
|
+
* @param options - 寫入選項(如壓縮、content type)
|
|
2674
|
+
*
|
|
2675
|
+
* @since 3.1.0
|
|
2676
|
+
*/
|
|
2677
|
+
writeStream(filename: string, stream: AsyncIterable<string>, options?: WriteStreamOptions): Promise<void>;
|
|
2678
|
+
/**
|
|
2679
|
+
* Reads sitemap content from a Google Cloud Storage object.
|
|
2680
|
+
*
|
|
2681
|
+
* @param filename - The name of the file to read.
|
|
2682
|
+
* @returns A promise resolving to the file content as a string, or null if not found.
|
|
2683
|
+
*/
|
|
1540
2684
|
read(filename: string): Promise<string | null>;
|
|
2685
|
+
/**
|
|
2686
|
+
* Returns a readable stream for a Google Cloud Storage object.
|
|
2687
|
+
*
|
|
2688
|
+
* @param filename - The name of the file to stream.
|
|
2689
|
+
* @returns A promise resolving to an async iterable of file chunks, or null if not found.
|
|
2690
|
+
*/
|
|
1541
2691
|
readStream(filename: string): Promise<AsyncIterable<string> | null>;
|
|
2692
|
+
/**
|
|
2693
|
+
* Checks if a Google Cloud Storage object exists.
|
|
2694
|
+
*
|
|
2695
|
+
* @param filename - The name of the file to check.
|
|
2696
|
+
* @returns A promise resolving to true if the file exists, false otherwise.
|
|
2697
|
+
*/
|
|
1542
2698
|
exists(filename: string): Promise<boolean>;
|
|
2699
|
+
/**
|
|
2700
|
+
* Returns the full public URL for a Google Cloud Storage object.
|
|
2701
|
+
*
|
|
2702
|
+
* @param filename - The name of the sitemap file.
|
|
2703
|
+
* @returns The public URL as a string.
|
|
2704
|
+
*/
|
|
1543
2705
|
getUrl(filename: string): string;
|
|
2706
|
+
/**
|
|
2707
|
+
* Writes content to a shadow (staged) location in Google Cloud Storage.
|
|
2708
|
+
*
|
|
2709
|
+
* @param filename - The name of the file to write.
|
|
2710
|
+
* @param content - The XML or JSON content.
|
|
2711
|
+
* @param shadowId - Optional unique session identifier.
|
|
2712
|
+
*/
|
|
1544
2713
|
writeShadow(filename: string, content: string, shadowId?: string): Promise<void>;
|
|
2714
|
+
/**
|
|
2715
|
+
* Commits all staged shadow objects in a session to production in Google Cloud Storage.
|
|
2716
|
+
*
|
|
2717
|
+
* @param shadowId - The identifier of the session to commit.
|
|
2718
|
+
*/
|
|
1545
2719
|
commitShadow(shadowId: string): Promise<void>;
|
|
2720
|
+
/**
|
|
2721
|
+
* Lists all archived versions of a specific sitemap in Google Cloud Storage.
|
|
2722
|
+
*
|
|
2723
|
+
* @param filename - The sitemap filename.
|
|
2724
|
+
* @returns A promise resolving to an array of version identifiers.
|
|
2725
|
+
*/
|
|
1546
2726
|
listVersions(filename: string): Promise<string[]>;
|
|
2727
|
+
/**
|
|
2728
|
+
* Reverts a sitemap to a previously archived version in Google Cloud Storage.
|
|
2729
|
+
*
|
|
2730
|
+
* @param filename - The sitemap filename.
|
|
2731
|
+
* @param version - The version identifier to switch to.
|
|
2732
|
+
*/
|
|
1547
2733
|
switchVersion(filename: string, version: string): Promise<void>;
|
|
1548
2734
|
}
|
|
1549
2735
|
|
|
1550
2736
|
/**
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
2737
|
+
* MemoryProgressStorage is a non-persistent, in-memory implementation of the `SitemapProgressStorage`.
|
|
2738
|
+
*
|
|
2739
|
+
* It is suitable for single-process applications or development environments where
|
|
2740
|
+
* persistence of job progress across application restarts is not required.
|
|
2741
|
+
*
|
|
2742
|
+
* @public
|
|
2743
|
+
* @since 3.0.0
|
|
1553
2744
|
*/
|
|
1554
2745
|
declare class MemoryProgressStorage implements SitemapProgressStorage {
|
|
1555
2746
|
private storage;
|
|
2747
|
+
/**
|
|
2748
|
+
* Retrieves the progress of a specific generation job from memory.
|
|
2749
|
+
*
|
|
2750
|
+
* @param jobId - Unique identifier for the job.
|
|
2751
|
+
* @returns A promise resolving to the `SitemapProgress` object, or null if not found.
|
|
2752
|
+
*/
|
|
1556
2753
|
get(jobId: string): Promise<SitemapProgress | null>;
|
|
2754
|
+
/**
|
|
2755
|
+
* Initializes or overwrites a progress record in memory.
|
|
2756
|
+
*
|
|
2757
|
+
* @param jobId - Unique identifier for the job.
|
|
2758
|
+
* @param progress - The initial or current state of the job progress.
|
|
2759
|
+
*/
|
|
1557
2760
|
set(jobId: string, progress: SitemapProgress): Promise<void>;
|
|
2761
|
+
/**
|
|
2762
|
+
* Updates specific fields of an existing progress record in memory.
|
|
2763
|
+
*
|
|
2764
|
+
* @param jobId - Unique identifier for the job.
|
|
2765
|
+
* @param updates - Object containing the fields to update.
|
|
2766
|
+
*/
|
|
1558
2767
|
update(jobId: string, updates: Partial<SitemapProgress>): Promise<void>;
|
|
2768
|
+
/**
|
|
2769
|
+
* Deletes a progress record from memory.
|
|
2770
|
+
*
|
|
2771
|
+
* @param jobId - Unique identifier for the job to remove.
|
|
2772
|
+
*/
|
|
1559
2773
|
delete(jobId: string): Promise<void>;
|
|
2774
|
+
/**
|
|
2775
|
+
* Lists the most recent sitemap generation jobs from memory.
|
|
2776
|
+
*
|
|
2777
|
+
* @param limit - Maximum number of records to return.
|
|
2778
|
+
* @returns A promise resolving to an array of `SitemapProgress` objects, sorted by start time.
|
|
2779
|
+
*/
|
|
1560
2780
|
list(limit?: number): Promise<SitemapProgress[]>;
|
|
1561
2781
|
}
|
|
1562
2782
|
|
|
@@ -1579,10 +2799,51 @@ declare class MemorySitemapStorage implements SitemapStorage {
|
|
|
1579
2799
|
private baseUrl;
|
|
1580
2800
|
private files;
|
|
1581
2801
|
constructor(baseUrl: string);
|
|
2802
|
+
/**
|
|
2803
|
+
* Writes sitemap content to memory.
|
|
2804
|
+
*
|
|
2805
|
+
* @param filename - The name of the file to store.
|
|
2806
|
+
* @param content - The XML or JSON content.
|
|
2807
|
+
*/
|
|
1582
2808
|
write(filename: string, content: string): Promise<void>;
|
|
2809
|
+
/**
|
|
2810
|
+
* 使用串流方式寫入 sitemap 至記憶體,可選擇性啟用 gzip 壓縮。
|
|
2811
|
+
* 記憶體儲存會收集串流為完整字串。
|
|
2812
|
+
*
|
|
2813
|
+
* @param filename - 檔案名稱
|
|
2814
|
+
* @param stream - XML 內容的 AsyncIterable
|
|
2815
|
+
* @param options - 寫入選項(如壓縮)
|
|
2816
|
+
*
|
|
2817
|
+
* @since 3.1.0
|
|
2818
|
+
*/
|
|
2819
|
+
writeStream(filename: string, stream: AsyncIterable<string>, options?: WriteStreamOptions): Promise<void>;
|
|
2820
|
+
/**
|
|
2821
|
+
* Reads sitemap content from memory.
|
|
2822
|
+
*
|
|
2823
|
+
* @param filename - The name of the file to read.
|
|
2824
|
+
* @returns A promise resolving to the file content as a string, or null if not found.
|
|
2825
|
+
*/
|
|
1583
2826
|
read(filename: string): Promise<string | null>;
|
|
2827
|
+
/**
|
|
2828
|
+
* Returns a readable stream for a sitemap file in memory.
|
|
2829
|
+
*
|
|
2830
|
+
* @param filename - The name of the file to stream.
|
|
2831
|
+
* @returns A promise resolving to an async iterable of file chunks, or null if not found.
|
|
2832
|
+
*/
|
|
1584
2833
|
readStream(filename: string): Promise<AsyncIterable<string> | null>;
|
|
2834
|
+
/**
|
|
2835
|
+
* Checks if a sitemap file exists in memory.
|
|
2836
|
+
*
|
|
2837
|
+
* @param filename - The name of the file to check.
|
|
2838
|
+
* @returns A promise resolving to true if the file exists, false otherwise.
|
|
2839
|
+
*/
|
|
1585
2840
|
exists(filename: string): Promise<boolean>;
|
|
2841
|
+
/**
|
|
2842
|
+
* Returns the full public URL for a sitemap file.
|
|
2843
|
+
*
|
|
2844
|
+
* @param filename - The name of the sitemap file.
|
|
2845
|
+
* @returns The public URL as a string.
|
|
2846
|
+
*/
|
|
1586
2847
|
getUrl(filename: string): string;
|
|
1587
2848
|
}
|
|
1588
2849
|
|
|
@@ -1616,10 +2877,39 @@ declare class RedisProgressStorage implements SitemapProgressStorage {
|
|
|
1616
2877
|
constructor(options: RedisProgressStorageOptions);
|
|
1617
2878
|
private getKey;
|
|
1618
2879
|
private getListKey;
|
|
2880
|
+
/**
|
|
2881
|
+
* Retrieves the progress of a specific generation job from Redis.
|
|
2882
|
+
*
|
|
2883
|
+
* @param jobId - Unique identifier for the job.
|
|
2884
|
+
* @returns A promise resolving to the `SitemapProgress` object, or null if not found.
|
|
2885
|
+
*/
|
|
1619
2886
|
get(jobId: string): Promise<SitemapProgress | null>;
|
|
2887
|
+
/**
|
|
2888
|
+
* Initializes or overwrites a progress record in Redis.
|
|
2889
|
+
*
|
|
2890
|
+
* @param jobId - Unique identifier for the job.
|
|
2891
|
+
* @param progress - The initial or current state of the job progress.
|
|
2892
|
+
*/
|
|
1620
2893
|
set(jobId: string, progress: SitemapProgress): Promise<void>;
|
|
2894
|
+
/**
|
|
2895
|
+
* Updates specific fields of an existing progress record in Redis.
|
|
2896
|
+
*
|
|
2897
|
+
* @param jobId - Unique identifier for the job.
|
|
2898
|
+
* @param updates - Object containing the fields to update.
|
|
2899
|
+
*/
|
|
1621
2900
|
update(jobId: string, updates: Partial<SitemapProgress>): Promise<void>;
|
|
2901
|
+
/**
|
|
2902
|
+
* Deletes a progress record from Redis.
|
|
2903
|
+
*
|
|
2904
|
+
* @param jobId - Unique identifier for the job to remove.
|
|
2905
|
+
*/
|
|
1622
2906
|
delete(jobId: string): Promise<void>;
|
|
2907
|
+
/**
|
|
2908
|
+
* Lists the most recent sitemap generation jobs from Redis.
|
|
2909
|
+
*
|
|
2910
|
+
* @param limit - Maximum number of records to return.
|
|
2911
|
+
* @returns A promise resolving to an array of `SitemapProgress` objects, sorted by start time.
|
|
2912
|
+
*/
|
|
1623
2913
|
list(limit?: number): Promise<SitemapProgress[]>;
|
|
1624
2914
|
}
|
|
1625
2915
|
|
|
@@ -1680,15 +2970,156 @@ declare class S3SitemapStorage implements SitemapStorage {
|
|
|
1680
2970
|
constructor(options: S3SitemapStorageOptions);
|
|
1681
2971
|
private getS3Client;
|
|
1682
2972
|
private getKey;
|
|
2973
|
+
/**
|
|
2974
|
+
* Writes sitemap content to an S3 object.
|
|
2975
|
+
*
|
|
2976
|
+
* @param filename - The name of the file to write.
|
|
2977
|
+
* @param content - The XML or JSON content.
|
|
2978
|
+
*/
|
|
1683
2979
|
write(filename: string, content: string): Promise<void>;
|
|
2980
|
+
/**
|
|
2981
|
+
* 使用串流方式寫入 sitemap 至 S3,可選擇性啟用 gzip 壓縮。
|
|
2982
|
+
* S3 需要知道 Content-Length,因此會先收集串流為 Buffer。
|
|
2983
|
+
*
|
|
2984
|
+
* @param filename - 檔案名稱
|
|
2985
|
+
* @param stream - XML 內容的 AsyncIterable
|
|
2986
|
+
* @param options - 寫入選項(如壓縮、content type)
|
|
2987
|
+
*
|
|
2988
|
+
* @since 3.1.0
|
|
2989
|
+
*/
|
|
2990
|
+
writeStream(filename: string, stream: AsyncIterable<string>, options?: WriteStreamOptions): Promise<void>;
|
|
2991
|
+
/**
|
|
2992
|
+
* Reads sitemap content from an S3 object.
|
|
2993
|
+
*
|
|
2994
|
+
* @param filename - The name of the file to read.
|
|
2995
|
+
* @returns A promise resolving to the file content as a string, or null if not found.
|
|
2996
|
+
*/
|
|
1684
2997
|
read(filename: string): Promise<string | null>;
|
|
2998
|
+
/**
|
|
2999
|
+
* Returns a readable stream for an S3 object.
|
|
3000
|
+
*
|
|
3001
|
+
* @param filename - The name of the file to stream.
|
|
3002
|
+
* @returns A promise resolving to an async iterable of file chunks, or null if not found.
|
|
3003
|
+
*/
|
|
1685
3004
|
readStream(filename: string): Promise<AsyncIterable<string> | null>;
|
|
3005
|
+
/**
|
|
3006
|
+
* Checks if an S3 object exists.
|
|
3007
|
+
*
|
|
3008
|
+
* @param filename - The name of the file to check.
|
|
3009
|
+
* @returns A promise resolving to true if the file exists, false otherwise.
|
|
3010
|
+
*/
|
|
1686
3011
|
exists(filename: string): Promise<boolean>;
|
|
3012
|
+
/**
|
|
3013
|
+
* Returns the full public URL for an S3 object.
|
|
3014
|
+
*
|
|
3015
|
+
* @param filename - The name of the sitemap file.
|
|
3016
|
+
* @returns The public URL as a string.
|
|
3017
|
+
*/
|
|
1687
3018
|
getUrl(filename: string): string;
|
|
3019
|
+
/**
|
|
3020
|
+
* Writes content to a shadow (staged) location in S3.
|
|
3021
|
+
*
|
|
3022
|
+
* @param filename - The name of the file to write.
|
|
3023
|
+
* @param content - The XML or JSON content.
|
|
3024
|
+
* @param shadowId - Optional unique session identifier.
|
|
3025
|
+
*/
|
|
1688
3026
|
writeShadow(filename: string, content: string, shadowId?: string): Promise<void>;
|
|
3027
|
+
/**
|
|
3028
|
+
* Commits all staged shadow objects in a session to production.
|
|
3029
|
+
*
|
|
3030
|
+
* @param shadowId - The identifier of the session to commit.
|
|
3031
|
+
*/
|
|
1689
3032
|
commitShadow(shadowId: string): Promise<void>;
|
|
3033
|
+
/**
|
|
3034
|
+
* Lists all archived versions of a specific sitemap in S3.
|
|
3035
|
+
*
|
|
3036
|
+
* @param filename - The sitemap filename.
|
|
3037
|
+
* @returns A promise resolving to an array of version identifiers.
|
|
3038
|
+
*/
|
|
1690
3039
|
listVersions(filename: string): Promise<string[]>;
|
|
3040
|
+
/**
|
|
3041
|
+
* Reverts a sitemap to a previously archived version in S3.
|
|
3042
|
+
*
|
|
3043
|
+
* @param filename - The sitemap filename.
|
|
3044
|
+
* @param version - The version identifier to switch to.
|
|
3045
|
+
*/
|
|
1691
3046
|
switchVersion(filename: string, version: string): Promise<void>;
|
|
1692
3047
|
}
|
|
1693
3048
|
|
|
1694
|
-
|
|
3049
|
+
/**
|
|
3050
|
+
* @gravito/constellation - Compression utilities
|
|
3051
|
+
* @module utils/Compression
|
|
3052
|
+
* @since 3.1.0
|
|
3053
|
+
*/
|
|
3054
|
+
|
|
3055
|
+
/**
|
|
3056
|
+
* Compression configuration.
|
|
3057
|
+
*/
|
|
3058
|
+
interface CompressionConfig {
|
|
3059
|
+
/** Compression format. @default 'gzip' */
|
|
3060
|
+
format?: 'gzip' | undefined;
|
|
3061
|
+
/** Compression level (1-9). @default 6 */
|
|
3062
|
+
level?: number | undefined;
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* 將 AsyncIterable<string> 壓縮為 Buffer。
|
|
3066
|
+
* 適用於需要完整壓縮結果的場景(如 S3 Upload)。
|
|
3067
|
+
*
|
|
3068
|
+
* @param source - 輸入的字串串流
|
|
3069
|
+
* @param config - 壓縮設定
|
|
3070
|
+
* @returns 壓縮後的 Buffer
|
|
3071
|
+
*
|
|
3072
|
+
* @example
|
|
3073
|
+
* ```typescript
|
|
3074
|
+
* const source = (async function*() {
|
|
3075
|
+
* yield '<?xml version="1.0"?>'
|
|
3076
|
+
* yield '<urlset>...</urlset>'
|
|
3077
|
+
* })()
|
|
3078
|
+
* const compressed = await compressToBuffer(source)
|
|
3079
|
+
* ```
|
|
3080
|
+
*/
|
|
3081
|
+
declare function compressToBuffer(source: AsyncIterable<string>, config?: CompressionConfig): Promise<Buffer>;
|
|
3082
|
+
/**
|
|
3083
|
+
* 回傳壓縮串流 Transform。
|
|
3084
|
+
* 適用於可直接 pipe 的場景(如 Disk write)。
|
|
3085
|
+
*
|
|
3086
|
+
* @param config - 壓縮設定
|
|
3087
|
+
* @returns Transform stream
|
|
3088
|
+
*
|
|
3089
|
+
* @example
|
|
3090
|
+
* ```typescript
|
|
3091
|
+
* const readable = Readable.from(source)
|
|
3092
|
+
* const gzip = createCompressionStream({ level: 9 })
|
|
3093
|
+
* const writeStream = createWriteStream('output.xml.gz')
|
|
3094
|
+
* await pipeline(readable, gzip, writeStream)
|
|
3095
|
+
* ```
|
|
3096
|
+
*/
|
|
3097
|
+
declare function createCompressionStream(config?: CompressionConfig): Transform;
|
|
3098
|
+
/**
|
|
3099
|
+
* 將檔名轉換為 gzip 格式(加上 .gz 副檔名)。
|
|
3100
|
+
*
|
|
3101
|
+
* @param filename - 原始檔名
|
|
3102
|
+
* @returns 帶有 .gz 副檔名的檔名
|
|
3103
|
+
*
|
|
3104
|
+
* @example
|
|
3105
|
+
* ```typescript
|
|
3106
|
+
* toGzipFilename('sitemap.xml') // 'sitemap.xml.gz'
|
|
3107
|
+
* toGzipFilename('sitemap.xml.gz') // 'sitemap.xml.gz' (不重複添加)
|
|
3108
|
+
* ```
|
|
3109
|
+
*/
|
|
3110
|
+
declare function toGzipFilename(filename: string): string;
|
|
3111
|
+
/**
|
|
3112
|
+
* 移除檔名的 .gz 副檔名。
|
|
3113
|
+
*
|
|
3114
|
+
* @param filename - gzip 檔名
|
|
3115
|
+
* @returns 移除 .gz 後的檔名
|
|
3116
|
+
*
|
|
3117
|
+
* @example
|
|
3118
|
+
* ```typescript
|
|
3119
|
+
* fromGzipFilename('sitemap.xml.gz') // 'sitemap.xml'
|
|
3120
|
+
* fromGzipFilename('sitemap.xml') // 'sitemap.xml'
|
|
3121
|
+
* ```
|
|
3122
|
+
*/
|
|
3123
|
+
declare function fromGzipFilename(filename: string): string;
|
|
3124
|
+
|
|
3125
|
+
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 };
|