@gravito/constellation 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,18 +1,34 @@
1
1
  import {
2
2
  DiskSitemapStorage
3
- } from "./chunk-7WHLC3OJ.js";
3
+ } from "./chunk-IS2H7U6M.js";
4
4
 
5
5
  // src/core/ChangeTracker.ts
6
6
  var MemoryChangeTracker = class {
7
7
  changes = [];
8
+ urlIndex = /* @__PURE__ */ new Map();
8
9
  maxChanges;
9
10
  constructor(options = {}) {
10
11
  this.maxChanges = options.maxChanges || 1e5;
11
12
  }
12
13
  async track(change) {
13
14
  this.changes.push(change);
15
+ const urlChanges = this.urlIndex.get(change.url) || [];
16
+ urlChanges.push(change);
17
+ this.urlIndex.set(change.url, urlChanges);
14
18
  if (this.changes.length > this.maxChanges) {
15
- this.changes = this.changes.slice(-this.maxChanges);
19
+ const removed = this.changes.shift();
20
+ if (removed) {
21
+ const changes = this.urlIndex.get(removed.url);
22
+ if (changes) {
23
+ const index = changes.indexOf(removed);
24
+ if (index > -1) {
25
+ changes.splice(index, 1);
26
+ }
27
+ if (changes.length === 0) {
28
+ this.urlIndex.delete(removed.url);
29
+ }
30
+ }
31
+ }
16
32
  }
17
33
  }
18
34
  async getChanges(since) {
@@ -22,14 +38,21 @@ var MemoryChangeTracker = class {
22
38
  return this.changes.filter((change) => change.timestamp >= since);
23
39
  }
24
40
  async getChangesByUrl(url) {
25
- return this.changes.filter((change) => change.url === url);
41
+ return this.urlIndex.get(url) || [];
26
42
  }
27
43
  async clear(since) {
28
44
  if (!since) {
29
45
  this.changes = [];
46
+ this.urlIndex.clear();
30
47
  return;
31
48
  }
32
49
  this.changes = this.changes.filter((change) => change.timestamp < since);
50
+ this.urlIndex.clear();
51
+ for (const change of this.changes) {
52
+ const urlChanges = this.urlIndex.get(change.url) || [];
53
+ urlChanges.push(change);
54
+ this.urlIndex.set(change.url, urlChanges);
55
+ }
33
56
  }
34
57
  };
35
58
  var RedisChangeTracker = class {
@@ -204,52 +227,72 @@ var DiffCalculator = class {
204
227
  }
205
228
  };
206
229
 
230
+ // src/utils/Mutex.ts
231
+ var Mutex = class {
232
+ queue = Promise.resolve();
233
+ async runExclusive(fn) {
234
+ const next = this.queue.then(() => fn());
235
+ this.queue = next.then(
236
+ () => {
237
+ },
238
+ () => {
239
+ }
240
+ );
241
+ return next;
242
+ }
243
+ };
244
+
207
245
  // src/core/ShadowProcessor.ts
208
246
  var ShadowProcessor = class {
209
247
  options;
210
248
  shadowId;
211
249
  operations = [];
250
+ mutex = new Mutex();
212
251
  constructor(options) {
213
252
  this.options = options;
214
- this.shadowId = `shadow-${Date.now()}-${Math.random().toString(36).substring(7)}`;
253
+ this.shadowId = `shadow-${Date.now()}-${crypto.randomUUID()}`;
215
254
  }
216
255
  /**
217
256
  * 添加一個影子操作
218
257
  */
219
258
  async addOperation(operation) {
220
- if (!this.options.enabled) {
221
- await this.options.storage.write(operation.filename, operation.content);
222
- return;
223
- }
224
- this.operations.push({
225
- ...operation,
226
- shadowId: operation.shadowId || this.shadowId
259
+ return this.mutex.runExclusive(async () => {
260
+ if (!this.options.enabled) {
261
+ await this.options.storage.write(operation.filename, operation.content);
262
+ return;
263
+ }
264
+ this.operations.push({
265
+ ...operation,
266
+ shadowId: operation.shadowId || this.shadowId
267
+ });
268
+ if (this.options.storage.writeShadow) {
269
+ await this.options.storage.writeShadow(operation.filename, operation.content, this.shadowId);
270
+ } else {
271
+ await this.options.storage.write(operation.filename, operation.content);
272
+ }
227
273
  });
228
- if (this.options.storage.writeShadow) {
229
- await this.options.storage.writeShadow(operation.filename, operation.content, this.shadowId);
230
- } else {
231
- await this.options.storage.write(operation.filename, operation.content);
232
- }
233
274
  }
234
275
  /**
235
276
  * 提交所有影子操作
236
277
  */
237
278
  async commit() {
238
- if (!this.options.enabled) {
239
- return;
240
- }
241
- if (this.options.mode === "atomic") {
242
- if (this.options.storage.commitShadow) {
243
- await this.options.storage.commitShadow(this.shadowId);
279
+ return this.mutex.runExclusive(async () => {
280
+ if (!this.options.enabled) {
281
+ return;
244
282
  }
245
- } else {
246
- for (const operation of this.operations) {
283
+ if (this.options.mode === "atomic") {
247
284
  if (this.options.storage.commitShadow) {
248
- await this.options.storage.commitShadow(operation.shadowId || this.shadowId);
285
+ await this.options.storage.commitShadow(this.shadowId);
286
+ }
287
+ } else {
288
+ for (const operation of this.operations) {
289
+ if (this.options.storage.commitShadow) {
290
+ await this.options.storage.commitShadow(operation.shadowId || this.shadowId);
291
+ }
249
292
  }
250
293
  }
251
- }
252
- this.operations = [];
294
+ this.operations = [];
295
+ });
253
296
  }
254
297
  /**
255
298
  * 取消所有影子操作
@@ -335,6 +378,12 @@ var SitemapIndex = class {
335
378
  var SitemapStream = class {
336
379
  options;
337
380
  entries = [];
381
+ flags = {
382
+ hasImages: false,
383
+ hasVideos: false,
384
+ hasNews: false,
385
+ hasAlternates: false
386
+ };
338
387
  constructor(options) {
339
388
  this.options = { ...options };
340
389
  if (this.options.baseUrl.endsWith("/")) {
@@ -342,10 +391,19 @@ var SitemapStream = class {
342
391
  }
343
392
  }
344
393
  add(entry) {
345
- if (typeof entry === "string") {
346
- this.entries.push({ url: entry });
347
- } else {
348
- this.entries.push(entry);
394
+ const e = typeof entry === "string" ? { url: entry } : entry;
395
+ this.entries.push(e);
396
+ if (e.images && e.images.length > 0) {
397
+ this.flags.hasImages = true;
398
+ }
399
+ if (e.videos && e.videos.length > 0) {
400
+ this.flags.hasVideos = true;
401
+ }
402
+ if (e.news) {
403
+ this.flags.hasNews = true;
404
+ }
405
+ if (e.alternates && e.alternates.length > 0) {
406
+ this.flags.hasAlternates = true;
349
407
  }
350
408
  return this;
351
409
  }
@@ -357,33 +415,33 @@ var SitemapStream = class {
357
415
  }
358
416
  toXML() {
359
417
  const { baseUrl, pretty } = this.options;
360
- let xml = `<?xml version="1.0" encoding="UTF-8"?>
361
- `;
362
- xml += `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"`;
363
- if (this.hasImages()) {
364
- xml += ` xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"`;
418
+ const parts = [];
419
+ parts.push('<?xml version="1.0" encoding="UTF-8"?>\n');
420
+ parts.push('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"');
421
+ if (this.flags.hasImages) {
422
+ parts.push(' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"');
365
423
  }
366
- if (this.hasVideos()) {
367
- xml += ` xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"`;
424
+ if (this.flags.hasVideos) {
425
+ parts.push(' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"');
368
426
  }
369
- if (this.hasNews()) {
370
- xml += ` xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"`;
427
+ if (this.flags.hasNews) {
428
+ parts.push(' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"');
371
429
  }
372
- if (this.hasAlternates()) {
373
- xml += ` xmlns:xhtml="http://www.w3.org/1999/xhtml"`;
430
+ if (this.flags.hasAlternates) {
431
+ parts.push(' xmlns:xhtml="http://www.w3.org/1999/xhtml"');
374
432
  }
375
- xml += `>
376
- `;
433
+ parts.push(">\n");
377
434
  for (const entry of this.entries) {
378
- xml += this.renderUrl(entry, baseUrl, pretty);
435
+ parts.push(this.renderUrl(entry, baseUrl, pretty));
379
436
  }
380
- xml += `</urlset>`;
381
- return xml;
437
+ parts.push("</urlset>");
438
+ return parts.join("");
382
439
  }
383
440
  renderUrl(entry, baseUrl, pretty) {
384
441
  const indent = pretty ? " " : "";
385
442
  const subIndent = pretty ? " " : "";
386
443
  const nl = pretty ? "\n" : "";
444
+ const parts = [];
387
445
  let loc = entry.url;
388
446
  if (!loc.startsWith("http")) {
389
447
  if (!loc.startsWith("/")) {
@@ -391,17 +449,17 @@ var SitemapStream = class {
391
449
  }
392
450
  loc = baseUrl + loc;
393
451
  }
394
- let item = `${indent}<url>${nl}`;
395
- item += `${subIndent}<loc>${this.escape(loc)}</loc>${nl}`;
452
+ parts.push(`${indent}<url>${nl}`);
453
+ parts.push(`${subIndent}<loc>${this.escape(loc)}</loc>${nl}`);
396
454
  if (entry.lastmod) {
397
455
  const date = entry.lastmod instanceof Date ? entry.lastmod : new Date(entry.lastmod);
398
- item += `${subIndent}<lastmod>${date.toISOString().split("T")[0]}</lastmod>${nl}`;
456
+ parts.push(`${subIndent}<lastmod>${date.toISOString().split("T")[0]}</lastmod>${nl}`);
399
457
  }
400
458
  if (entry.changefreq) {
401
- item += `${subIndent}<changefreq>${entry.changefreq}</changefreq>${nl}`;
459
+ parts.push(`${subIndent}<changefreq>${entry.changefreq}</changefreq>${nl}`);
402
460
  }
403
461
  if (entry.priority !== void 0) {
404
- item += `${subIndent}<priority>${entry.priority.toFixed(1)}</priority>${nl}`;
462
+ parts.push(`${subIndent}<priority>${entry.priority.toFixed(1)}</priority>${nl}`);
405
463
  }
406
464
  if (entry.alternates) {
407
465
  for (const alt of entry.alternates) {
@@ -412,7 +470,9 @@ var SitemapStream = class {
412
470
  }
413
471
  altLoc = baseUrl + altLoc;
414
472
  }
415
- item += `${subIndent}<xhtml:link rel="alternate" hreflang="${alt.lang}" href="${this.escape(altLoc)}"/>${nl}`;
473
+ parts.push(
474
+ `${subIndent}<xhtml:link rel="alternate" hreflang="${alt.lang}" href="${this.escape(altLoc)}"/>${nl}`
475
+ );
416
476
  }
417
477
  }
418
478
  if (entry.redirect?.canonical) {
@@ -423,10 +483,14 @@ var SitemapStream = class {
423
483
  }
424
484
  canonicalUrl = baseUrl + canonicalUrl;
425
485
  }
426
- item += `${subIndent}<xhtml:link rel="canonical" href="${this.escape(canonicalUrl)}"/>${nl}`;
486
+ parts.push(
487
+ `${subIndent}<xhtml:link rel="canonical" href="${this.escape(canonicalUrl)}"/>${nl}`
488
+ );
427
489
  }
428
490
  if (entry.redirect && !entry.redirect.canonical) {
429
- item += `${subIndent}<!-- Redirect: ${entry.redirect.from} \u2192 ${entry.redirect.to} (${entry.redirect.type}) -->${nl}`;
491
+ parts.push(
492
+ `${subIndent}<!-- Redirect: ${entry.redirect.from} \u2192 ${entry.redirect.to} (${entry.redirect.type}) -->${nl}`
493
+ );
430
494
  }
431
495
  if (entry.images) {
432
496
  for (const img of entry.images) {
@@ -437,91 +501,110 @@ var SitemapStream = class {
437
501
  }
438
502
  loc2 = baseUrl + loc2;
439
503
  }
440
- item += `${subIndent}<image:image>${nl}`;
441
- item += `${subIndent} <image:loc>${this.escape(loc2)}</image:loc>${nl}`;
504
+ parts.push(`${subIndent}<image:image>${nl}`);
505
+ parts.push(`${subIndent} <image:loc>${this.escape(loc2)}</image:loc>${nl}`);
442
506
  if (img.title) {
443
- item += `${subIndent} <image:title>${this.escape(img.title)}</image:title>${nl}`;
507
+ parts.push(`${subIndent} <image:title>${this.escape(img.title)}</image:title>${nl}`);
444
508
  }
445
509
  if (img.caption) {
446
- item += `${subIndent} <image:caption>${this.escape(img.caption)}</image:caption>${nl}`;
510
+ parts.push(
511
+ `${subIndent} <image:caption>${this.escape(img.caption)}</image:caption>${nl}`
512
+ );
447
513
  }
448
514
  if (img.geo_location) {
449
- item += `${subIndent} <image:geo_location>${this.escape(img.geo_location)}</image:geo_location>${nl}`;
515
+ parts.push(
516
+ `${subIndent} <image:geo_location>${this.escape(img.geo_location)}</image:geo_location>${nl}`
517
+ );
450
518
  }
451
519
  if (img.license) {
452
- item += `${subIndent} <image:license>${this.escape(img.license)}</image:license>${nl}`;
520
+ parts.push(
521
+ `${subIndent} <image:license>${this.escape(img.license)}</image:license>${nl}`
522
+ );
453
523
  }
454
- item += `${subIndent}</image:image>${nl}`;
524
+ parts.push(`${subIndent}</image:image>${nl}`);
455
525
  }
456
526
  }
457
527
  if (entry.videos) {
458
528
  for (const video of entry.videos) {
459
- item += `${subIndent}<video:video>${nl}`;
460
- item += `${subIndent} <video:thumbnail_loc>${this.escape(video.thumbnail_loc)}</video:thumbnail_loc>${nl}`;
461
- item += `${subIndent} <video:title>${this.escape(video.title)}</video:title>${nl}`;
462
- item += `${subIndent} <video:description>${this.escape(video.description)}</video:description>${nl}`;
529
+ parts.push(`${subIndent}<video:video>${nl}`);
530
+ parts.push(
531
+ `${subIndent} <video:thumbnail_loc>${this.escape(video.thumbnail_loc)}</video:thumbnail_loc>${nl}`
532
+ );
533
+ parts.push(`${subIndent} <video:title>${this.escape(video.title)}</video:title>${nl}`);
534
+ parts.push(
535
+ `${subIndent} <video:description>${this.escape(video.description)}</video:description>${nl}`
536
+ );
463
537
  if (video.content_loc) {
464
- item += `${subIndent} <video:content_loc>${this.escape(video.content_loc)}</video:content_loc>${nl}`;
538
+ parts.push(
539
+ `${subIndent} <video:content_loc>${this.escape(video.content_loc)}</video:content_loc>${nl}`
540
+ );
465
541
  }
466
542
  if (video.player_loc) {
467
- item += `${subIndent} <video:player_loc>${this.escape(video.player_loc)}</video:player_loc>${nl}`;
543
+ parts.push(
544
+ `${subIndent} <video:player_loc>${this.escape(video.player_loc)}</video:player_loc>${nl}`
545
+ );
468
546
  }
469
547
  if (video.duration) {
470
- item += `${subIndent} <video:duration>${video.duration}</video:duration>${nl}`;
548
+ parts.push(`${subIndent} <video:duration>${video.duration}</video:duration>${nl}`);
471
549
  }
472
550
  if (video.view_count) {
473
- item += `${subIndent} <video:view_count>${video.view_count}</video:view_count>${nl}`;
551
+ parts.push(`${subIndent} <video:view_count>${video.view_count}</video:view_count>${nl}`);
474
552
  }
475
553
  if (video.publication_date) {
476
554
  const pubDate = video.publication_date instanceof Date ? video.publication_date : new Date(video.publication_date);
477
- item += `${subIndent} <video:publication_date>${pubDate.toISOString()}</video:publication_date>${nl}`;
555
+ parts.push(
556
+ `${subIndent} <video:publication_date>${pubDate.toISOString()}</video:publication_date>${nl}`
557
+ );
478
558
  }
479
559
  if (video.family_friendly) {
480
- item += `${subIndent} <video:family_friendly>${video.family_friendly}</video:family_friendly>${nl}`;
560
+ parts.push(
561
+ `${subIndent} <video:family_friendly>${video.family_friendly}</video:family_friendly>${nl}`
562
+ );
481
563
  }
482
564
  if (video.tag) {
483
565
  for (const tag of video.tag) {
484
- item += `${subIndent} <video:tag>${this.escape(tag)}</video:tag>${nl}`;
566
+ parts.push(`${subIndent} <video:tag>${this.escape(tag)}</video:tag>${nl}`);
485
567
  }
486
568
  }
487
- item += `${subIndent}</video:video>${nl}`;
569
+ parts.push(`${subIndent}</video:video>${nl}`);
488
570
  }
489
571
  }
490
572
  if (entry.news) {
491
- item += `${subIndent}<news:news>${nl}`;
492
- item += `${subIndent} <news:publication>${nl}`;
493
- item += `${subIndent} <news:name>${this.escape(entry.news.publication.name)}</news:name>${nl}`;
494
- item += `${subIndent} <news:language>${this.escape(entry.news.publication.language)}</news:language>${nl}`;
495
- item += `${subIndent} </news:publication>${nl}`;
573
+ parts.push(`${subIndent}<news:news>${nl}`);
574
+ parts.push(`${subIndent} <news:publication>${nl}`);
575
+ parts.push(
576
+ `${subIndent} <news:name>${this.escape(entry.news.publication.name)}</news:name>${nl}`
577
+ );
578
+ parts.push(
579
+ `${subIndent} <news:language>${this.escape(entry.news.publication.language)}</news:language>${nl}`
580
+ );
581
+ parts.push(`${subIndent} </news:publication>${nl}`);
496
582
  const pubDate = entry.news.publication_date instanceof Date ? entry.news.publication_date : new Date(entry.news.publication_date);
497
- item += `${subIndent} <news:publication_date>${pubDate.toISOString()}</news:publication_date>${nl}`;
498
- item += `${subIndent} <news:title>${this.escape(entry.news.title)}</news:title>${nl}`;
583
+ parts.push(
584
+ `${subIndent} <news:publication_date>${pubDate.toISOString()}</news:publication_date>${nl}`
585
+ );
586
+ parts.push(`${subIndent} <news:title>${this.escape(entry.news.title)}</news:title>${nl}`);
499
587
  if (entry.news.genres) {
500
- item += `${subIndent} <news:genres>${this.escape(entry.news.genres)}</news:genres>${nl}`;
588
+ parts.push(
589
+ `${subIndent} <news:genres>${this.escape(entry.news.genres)}</news:genres>${nl}`
590
+ );
501
591
  }
502
592
  if (entry.news.keywords) {
503
- item += `${subIndent} <news:keywords>${entry.news.keywords.map((k) => this.escape(k)).join(", ")}</news:keywords>${nl}`;
593
+ parts.push(
594
+ `${subIndent} <news:keywords>${entry.news.keywords.map((k) => this.escape(k)).join(", ")}</news:keywords>${nl}`
595
+ );
504
596
  }
505
- item += `${subIndent}</news:news>${nl}`;
597
+ parts.push(`${subIndent}</news:news>${nl}`);
506
598
  }
507
- item += `${indent}</url>${nl}`;
508
- return item;
509
- }
510
- hasImages() {
511
- return this.entries.some((e) => e.images && e.images.length > 0);
512
- }
513
- hasVideos() {
514
- return this.entries.some((e) => e.videos && e.videos.length > 0);
515
- }
516
- hasNews() {
517
- return this.entries.some((e) => !!e.news);
518
- }
519
- hasAlternates() {
520
- return this.entries.some((e) => e.alternates && e.alternates.length > 0);
599
+ parts.push(`${indent}</url>${nl}`);
600
+ return parts.join("");
521
601
  }
522
602
  escape(str) {
523
603
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
524
604
  }
605
+ getEntries() {
606
+ return this.entries;
607
+ }
525
608
  };
526
609
 
527
610
  // src/core/SitemapGenerator.ts
@@ -553,12 +636,21 @@ var SitemapGenerator = class {
553
636
  baseUrl: this.options.baseUrl,
554
637
  pretty: this.options.pretty
555
638
  });
639
+ const shards = [];
556
640
  let isMultiFile = false;
557
641
  const flushShard = async () => {
558
642
  isMultiFile = true;
559
643
  const baseName = this.options.filename?.replace(/\.xml$/, "");
560
644
  const filename = `${baseName}-${shardIndex}.xml`;
561
645
  const xml = currentStream.toXML();
646
+ const entries = currentStream.getEntries();
647
+ shards.push({
648
+ filename,
649
+ from: this.normalizeUrl(entries[0].url),
650
+ to: this.normalizeUrl(entries[entries.length - 1].url),
651
+ count: entries.length,
652
+ lastmod: /* @__PURE__ */ new Date()
653
+ });
562
654
  if (this.shadowProcessor) {
563
655
  await this.shadowProcessor.addOperation({ filename, content: xml });
564
656
  } else {
@@ -596,16 +688,44 @@ var SitemapGenerator = class {
596
688
  }
597
689
  }
598
690
  }
691
+ const writeManifest = async () => {
692
+ if (!this.options.generateManifest) return;
693
+ const manifest = {
694
+ version: 1,
695
+ generatedAt: /* @__PURE__ */ new Date(),
696
+ baseUrl: this.options.baseUrl,
697
+ maxEntriesPerShard: this.options.maxEntriesPerFile,
698
+ sort: "url-lex",
699
+ shards
700
+ };
701
+ const manifestFilename = this.options.filename?.replace(/\.xml$/, "-manifest.json") || "sitemap-manifest.json";
702
+ const content = JSON.stringify(manifest, null, this.options.pretty ? 2 : 0);
703
+ if (this.shadowProcessor) {
704
+ await this.shadowProcessor.addOperation({ filename: manifestFilename, content });
705
+ } else {
706
+ await this.options.storage.write(manifestFilename, content);
707
+ }
708
+ };
599
709
  if (!isMultiFile) {
600
710
  const xml = currentStream.toXML();
711
+ const entries = currentStream.getEntries();
712
+ shards.push({
713
+ filename: this.options.filename,
714
+ from: entries[0] ? this.normalizeUrl(entries[0].url) : "",
715
+ to: entries[entries.length - 1] ? this.normalizeUrl(entries[entries.length - 1].url) : "",
716
+ count: entries.length,
717
+ lastmod: /* @__PURE__ */ new Date()
718
+ });
601
719
  if (this.shadowProcessor) {
602
720
  await this.shadowProcessor.addOperation({
603
721
  filename: this.options.filename,
604
722
  content: xml
605
723
  });
724
+ await writeManifest();
606
725
  await this.shadowProcessor.commit();
607
726
  } else {
608
727
  await this.options.storage.write(this.options.filename, xml);
728
+ await writeManifest();
609
729
  }
610
730
  return;
611
731
  }
@@ -618,10 +738,21 @@ var SitemapGenerator = class {
618
738
  filename: this.options.filename,
619
739
  content: indexXml
620
740
  });
741
+ await writeManifest();
621
742
  await this.shadowProcessor.commit();
622
743
  } else {
623
744
  await this.options.storage.write(this.options.filename, indexXml);
745
+ await writeManifest();
746
+ }
747
+ }
748
+ normalizeUrl(url) {
749
+ if (url.startsWith("http")) {
750
+ return url;
624
751
  }
752
+ const { baseUrl } = this.options;
753
+ const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
754
+ const normalizedPath = url.startsWith("/") ? url : `/${url}`;
755
+ return normalizedBase + normalizedPath;
625
756
  }
626
757
  /**
627
758
  * 獲取影子處理器(如果啟用)
@@ -631,22 +762,104 @@ var SitemapGenerator = class {
631
762
  }
632
763
  };
633
764
 
765
+ // src/core/SitemapParser.ts
766
+ var SitemapParser = class {
767
+ static parse(xml) {
768
+ const entries = [];
769
+ const urlRegex = /<url>([\s\S]*?)<\/url>/g;
770
+ let match;
771
+ while ((match = urlRegex.exec(xml)) !== null) {
772
+ const entry = this.parseEntry(match[1]);
773
+ if (entry) {
774
+ entries.push(entry);
775
+ }
776
+ }
777
+ return entries;
778
+ }
779
+ static async *parseStream(stream) {
780
+ let buffer = "";
781
+ const urlRegex = /<url>([\s\S]*?)<\/url>/g;
782
+ for await (const chunk of stream) {
783
+ buffer += chunk;
784
+ let match;
785
+ while ((match = urlRegex.exec(buffer)) !== null) {
786
+ const entry = this.parseEntry(match[1]);
787
+ if (entry) {
788
+ yield entry;
789
+ }
790
+ buffer = buffer.slice(match.index + match[0].length);
791
+ urlRegex.lastIndex = 0;
792
+ }
793
+ if (buffer.length > 1024 * 1024) {
794
+ const lastUrlStart = buffer.lastIndexOf("<url>");
795
+ buffer = lastUrlStart !== -1 ? buffer.slice(lastUrlStart) : "";
796
+ }
797
+ }
798
+ }
799
+ static parseEntry(urlContent) {
800
+ const entry = { url: "" };
801
+ const locMatch = /<loc>(.*?)<\/loc>/.exec(urlContent);
802
+ if (locMatch) {
803
+ entry.url = this.unescape(locMatch[1]);
804
+ } else {
805
+ return null;
806
+ }
807
+ const lastmodMatch = /<lastmod>(.*?)<\/lastmod>/.exec(urlContent);
808
+ if (lastmodMatch) {
809
+ entry.lastmod = new Date(lastmodMatch[1]);
810
+ }
811
+ const priorityMatch = /<priority>(.*?)<\/priority>/.exec(urlContent);
812
+ if (priorityMatch) {
813
+ entry.priority = parseFloat(priorityMatch[1]);
814
+ }
815
+ const changefreqMatch = /<changefreq>(.*?)<\/changefreq>/.exec(urlContent);
816
+ if (changefreqMatch) {
817
+ entry.changefreq = changefreqMatch[1];
818
+ }
819
+ return entry;
820
+ }
821
+ static parseIndex(xml) {
822
+ const urls = [];
823
+ const sitemapRegex = /<sitemap>([\s\S]*?)<\/sitemap>/g;
824
+ let match;
825
+ while ((match = sitemapRegex.exec(xml)) !== null) {
826
+ const content = match[1];
827
+ const locMatch = /<loc>(.*?)<\/loc>/.exec(content);
828
+ if (locMatch) {
829
+ urls.push(this.unescape(locMatch[1]));
830
+ }
831
+ }
832
+ return urls;
833
+ }
834
+ static unescape(str) {
835
+ return str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'");
836
+ }
837
+ };
838
+
634
839
  // src/core/IncrementalGenerator.ts
635
840
  var IncrementalGenerator = class {
636
841
  options;
637
842
  changeTracker;
638
843
  diffCalculator;
639
844
  generator;
845
+ mutex = new Mutex();
640
846
  constructor(options) {
641
- this.options = options;
642
- this.changeTracker = options.changeTracker;
643
- this.diffCalculator = options.diffCalculator || new DiffCalculator();
644
- this.generator = new SitemapGenerator(options);
847
+ this.options = {
848
+ autoTrack: true,
849
+ generateManifest: true,
850
+ ...options
851
+ };
852
+ this.changeTracker = this.options.changeTracker;
853
+ this.diffCalculator = this.options.diffCalculator || new DiffCalculator();
854
+ this.generator = new SitemapGenerator(this.options);
645
855
  }
646
- /**
647
- * 生成完整的 sitemap(首次生成)
648
- */
649
856
  async generateFull() {
857
+ return this.mutex.runExclusive(() => this.performFullGeneration());
858
+ }
859
+ async generateIncremental(since) {
860
+ return this.mutex.runExclusive(() => this.performIncrementalGeneration(since));
861
+ }
862
+ async performFullGeneration() {
650
863
  await this.generator.run();
651
864
  if (this.options.autoTrack) {
652
865
  const { providers } = this.options;
@@ -664,52 +877,130 @@ var IncrementalGenerator = class {
664
877
  }
665
878
  }
666
879
  }
667
- /**
668
- * 增量生成(只更新變更的部分)
669
- */
670
- async generateIncremental(since) {
880
+ async performIncrementalGeneration(since) {
671
881
  const changes = await this.changeTracker.getChanges(since);
672
882
  if (changes.length === 0) {
673
883
  return;
674
884
  }
675
- const baseEntries = await this.loadBaseEntries();
676
- const diff = this.diffCalculator.calculateFromChanges(baseEntries, changes);
677
- await this.generateDiff(diff);
885
+ const manifest = await this.loadManifest();
886
+ if (!manifest) {
887
+ await this.performFullGeneration();
888
+ return;
889
+ }
890
+ const totalCount = manifest.shards.reduce((acc, s) => acc + s.count, 0);
891
+ const changeRatio = totalCount > 0 ? changes.length / totalCount : 1;
892
+ if (changeRatio > 0.3) {
893
+ await this.performFullGeneration();
894
+ return;
895
+ }
896
+ const affectedShards = this.getAffectedShards(manifest, changes);
897
+ if (affectedShards.size / manifest.shards.length > 0.5) {
898
+ await this.performFullGeneration();
899
+ return;
900
+ }
901
+ await this.updateShards(manifest, affectedShards);
678
902
  }
679
- /**
680
- * 手動追蹤變更
681
- */
682
- async trackChange(change) {
683
- await this.changeTracker.track(change);
903
+ normalizeUrl(url) {
904
+ if (url.startsWith("http")) {
905
+ return url;
906
+ }
907
+ const { baseUrl } = this.options;
908
+ const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
909
+ const normalizedPath = url.startsWith("/") ? url : `/${url}`;
910
+ return normalizedBase + normalizedPath;
684
911
  }
685
- /**
686
- * 獲取變更記錄
687
- */
688
- async getChanges(since) {
689
- return this.changeTracker.getChanges(since);
912
+ async loadManifest() {
913
+ const filename = this.options.filename?.replace(/\.xml$/, "-manifest.json") || "sitemap-manifest.json";
914
+ const content = await this.options.storage.read(filename);
915
+ if (!content) {
916
+ return null;
917
+ }
918
+ try {
919
+ return JSON.parse(content);
920
+ } catch {
921
+ return null;
922
+ }
690
923
  }
691
- /**
692
- * 載入基礎 entries(從現有 sitemap)
693
- */
694
- async loadBaseEntries() {
695
- const entries = [];
696
- const { providers } = this.options;
697
- for (const provider of providers) {
698
- const providerEntries = await provider.getEntries();
699
- const entriesArray = Array.isArray(providerEntries) ? providerEntries : await this.toArray(providerEntries);
700
- entries.push(...entriesArray);
924
+ getAffectedShards(manifest, changes) {
925
+ const affected = /* @__PURE__ */ new Map();
926
+ for (const change of changes) {
927
+ const normalizedUrl = this.normalizeUrl(change.url);
928
+ let shard = manifest.shards.find((s) => {
929
+ return normalizedUrl >= s.from && normalizedUrl <= s.to;
930
+ });
931
+ if (!shard) {
932
+ shard = manifest.shards.find((s) => normalizedUrl <= s.to);
933
+ if (!shard) {
934
+ shard = manifest.shards[manifest.shards.length - 1];
935
+ }
936
+ }
937
+ if (shard) {
938
+ const shardChanges = affected.get(shard.filename) || [];
939
+ shardChanges.push(change);
940
+ affected.set(shard.filename, shardChanges);
941
+ }
701
942
  }
702
- return entries;
943
+ return affected;
703
944
  }
704
- /**
705
- * 生成差異部分
706
- */
707
- async generateDiff(_diff) {
708
- await this.generator.run();
945
+ async updateShards(manifest, affectedShards) {
946
+ for (const [filename, shardChanges] of affectedShards) {
947
+ const entries = [];
948
+ const stream = await this.options.storage.readStream?.(filename);
949
+ if (stream) {
950
+ for await (const entry of SitemapParser.parseStream(stream)) {
951
+ entries.push(entry);
952
+ }
953
+ } else {
954
+ const xml = await this.options.storage.read(filename);
955
+ if (!xml) {
956
+ continue;
957
+ }
958
+ entries.push(...SitemapParser.parse(xml));
959
+ }
960
+ const updatedEntries = this.applyChanges(entries, shardChanges);
961
+ const outStream = new SitemapStream({
962
+ baseUrl: this.options.baseUrl,
963
+ pretty: this.options.pretty
964
+ });
965
+ outStream.addAll(updatedEntries);
966
+ const newXml = outStream.toXML();
967
+ await this.options.storage.write(filename, newXml);
968
+ const shardInfo = manifest.shards.find((s) => s.filename === filename);
969
+ if (shardInfo) {
970
+ shardInfo.count = updatedEntries.length;
971
+ shardInfo.lastmod = /* @__PURE__ */ new Date();
972
+ shardInfo.from = this.normalizeUrl(updatedEntries[0].url);
973
+ shardInfo.to = this.normalizeUrl(updatedEntries[updatedEntries.length - 1].url);
974
+ }
975
+ }
976
+ const manifestFilename = this.options.filename?.replace(/\.xml$/, "-manifest.json") || "sitemap-manifest.json";
977
+ await this.options.storage.write(
978
+ manifestFilename,
979
+ JSON.stringify(manifest, null, this.options.pretty ? 2 : 0)
980
+ );
981
+ }
982
+ applyChanges(entries, changes) {
983
+ const entryMap = /* @__PURE__ */ new Map();
984
+ for (const entry of entries) {
985
+ entryMap.set(this.normalizeUrl(entry.url), entry);
986
+ }
987
+ for (const change of changes) {
988
+ const normalizedUrl = this.normalizeUrl(change.url);
989
+ if (change.type === "add" || change.type === "update") {
990
+ if (change.entry) {
991
+ entryMap.set(normalizedUrl, {
992
+ ...change.entry,
993
+ url: normalizedUrl
994
+ });
995
+ }
996
+ } else if (change.type === "remove") {
997
+ entryMap.delete(normalizedUrl);
998
+ }
999
+ }
1000
+ return Array.from(entryMap.values()).sort(
1001
+ (a, b) => this.normalizeUrl(a.url).localeCompare(this.normalizeUrl(b.url))
1002
+ );
709
1003
  }
710
- /**
711
- * 將 AsyncIterable 轉換為陣列
712
- */
713
1004
  async toArray(iterable) {
714
1005
  const array = [];
715
1006
  for await (const item of iterable) {
@@ -1078,6 +1369,15 @@ var MemorySitemapStorage = class {
1078
1369
  async read(filename) {
1079
1370
  return this.files.get(filename) || null;
1080
1371
  }
1372
+ async readStream(filename) {
1373
+ const content = this.files.get(filename);
1374
+ if (content === void 0) {
1375
+ return null;
1376
+ }
1377
+ return (async function* () {
1378
+ yield content;
1379
+ })();
1380
+ }
1081
1381
  async exists(filename) {
1082
1382
  return this.files.has(filename);
1083
1383
  }
@@ -1216,7 +1516,7 @@ var OrbitSitemap = class _OrbitSitemap {
1216
1516
  const opts = this.options;
1217
1517
  let storage = opts.storage;
1218
1518
  if (!storage) {
1219
- const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-7ZZMGC4K.js");
1519
+ const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-WP6RITUN.js");
1220
1520
  storage = new DiskSitemapStorage2(opts.outDir, opts.baseUrl);
1221
1521
  }
1222
1522
  let providers = opts.providers;
@@ -1262,7 +1562,7 @@ var OrbitSitemap = class _OrbitSitemap {
1262
1562
  }
1263
1563
  let storage = opts.storage;
1264
1564
  if (!storage) {
1265
- const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-7ZZMGC4K.js");
1565
+ const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-WP6RITUN.js");
1266
1566
  storage = new DiskSitemapStorage2(opts.outDir, opts.baseUrl);
1267
1567
  }
1268
1568
  const incrementalGenerator = new IncrementalGenerator({
@@ -1291,7 +1591,7 @@ var OrbitSitemap = class _OrbitSitemap {
1291
1591
  const jobId = randomUUID();
1292
1592
  let storage = opts.storage;
1293
1593
  if (!storage) {
1294
- const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-7ZZMGC4K.js");
1594
+ const { DiskSitemapStorage: DiskSitemapStorage2 } = await import("./DiskSitemapStorage-WP6RITUN.js");
1295
1595
  storage = new DiskSitemapStorage2(opts.outDir, opts.baseUrl);
1296
1596
  }
1297
1597
  let providers = opts.providers;
@@ -1818,6 +2118,30 @@ var GCPSitemapStorage = class {
1818
2118
  throw error;
1819
2119
  }
1820
2120
  }
2121
+ async readStream(filename) {
2122
+ try {
2123
+ const { bucket } = await this.getStorageClient();
2124
+ const key = this.getKey(filename);
2125
+ const file = bucket.file(key);
2126
+ const [exists] = await file.exists();
2127
+ if (!exists) {
2128
+ return null;
2129
+ }
2130
+ const stream = file.createReadStream();
2131
+ return (async function* () {
2132
+ const decoder = new TextDecoder();
2133
+ for await (const chunk of stream) {
2134
+ yield decoder.decode(chunk, { stream: true });
2135
+ }
2136
+ yield decoder.decode();
2137
+ })();
2138
+ } catch (error) {
2139
+ if (error.code === 404) {
2140
+ return null;
2141
+ }
2142
+ throw error;
2143
+ }
2144
+ }
1821
2145
  async exists(filename) {
1822
2146
  try {
1823
2147
  const { bucket } = await this.getStorageClient();
@@ -1840,7 +2164,7 @@ var GCPSitemapStorage = class {
1840
2164
  return this.write(filename, content);
1841
2165
  }
1842
2166
  const { bucket } = await this.getStorageClient();
1843
- const id = shadowId || `shadow-${Date.now()}-${Math.random().toString(36).substring(7)}`;
2167
+ const id = shadowId || `shadow-${Date.now()}-${crypto.randomUUID()}`;
1844
2168
  const shadowKey = this.getKey(`${filename}.shadow.${id}`);
1845
2169
  const file = bucket.file(shadowKey);
1846
2170
  await file.save(content, {
@@ -2112,6 +2436,34 @@ var S3SitemapStorage = class {
2112
2436
  throw error;
2113
2437
  }
2114
2438
  }
2439
+ async readStream(filename) {
2440
+ try {
2441
+ const s3 = await this.getS3Client();
2442
+ const key = this.getKey(filename);
2443
+ const response = await s3.client.send(
2444
+ new s3.GetObjectCommand({
2445
+ Bucket: this.bucket,
2446
+ Key: key
2447
+ })
2448
+ );
2449
+ if (!response.Body) {
2450
+ return null;
2451
+ }
2452
+ const body = response.Body;
2453
+ return (async function* () {
2454
+ const decoder = new TextDecoder();
2455
+ for await (const chunk of body) {
2456
+ yield decoder.decode(chunk, { stream: true });
2457
+ }
2458
+ yield decoder.decode();
2459
+ })();
2460
+ } catch (error) {
2461
+ if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
2462
+ return null;
2463
+ }
2464
+ throw error;
2465
+ }
2466
+ }
2115
2467
  async exists(filename) {
2116
2468
  try {
2117
2469
  const s3 = await this.getS3Client();
@@ -2141,7 +2493,7 @@ var S3SitemapStorage = class {
2141
2493
  return this.write(filename, content);
2142
2494
  }
2143
2495
  const s3 = await this.getS3Client();
2144
- const id = shadowId || `shadow-${Date.now()}-${Math.random().toString(36).substring(7)}`;
2496
+ const id = shadowId || `shadow-${Date.now()}-${crypto.randomUUID()}`;
2145
2497
  const shadowKey = this.getKey(`${filename}.shadow.${id}`);
2146
2498
  await s3.client.send(
2147
2499
  new s3.PutObjectCommand({