Skip to content

feat: implement dynamic sitemap generation and remove KV dependencies - #10

Open
3scava1i3r wants to merge 12 commits into
ubiquity:mainfrom
3scava1i3r:sitemap-dynamic-2
Open

3scava1i3r wants to merge 12 commits into
ubiquity:mainfrom
3scava1i3r:sitemap-dynamic-2

Conversation

@3scava1i3r

@3scava1i3r 3scava1i3r commented Feb 6, 2026

Copy link
Copy Markdown

This PR implements the Dynamic Sitemap (Apps & Plugins) feature requested in issue #2 fix for it

🚀 New Features

  • Dynamic Sitemap Generation: Real-time discovery of all ubq.fi services and plugins
  • XML & JSON Formats: Both /sitemap.xml and /sitemap.json endpoints available
  • Plugin-Map System: Comprehensive plugin deployment tracking at /plugin-map.xml and /plugin-map.json
  • GitHub Integration: Automatic discovery from ubiquity and ubiquity-os-marketplace repositories

🛠 Architecture Improvements

  • KV-Free Design: Removed all KV namespace dependencies and analytics tracking
  • In-Memory Caching: Efficient caching with 6-hour sitemap and 2-hour plugin-map cache
  • Timeout Protection: 8-second timeout protection for sitemap generation
  • Error Handling: Graceful fallbacks and comprehensive error logging

📊 Rich Metadata

  • Service deployment types (Deno, Pages, both, none)
  • Plugin deployment status (main, development, both, none)
  • GitHub repository links and metadata
  • Real-time infrastructure status reporting

Endpoints Added

  • GET /sitemap.xml - XML sitemap of all services
  • GET /sitemap.json - JSON sitemap with rich metadata
  • GET /plugin-map.xml - XML plugin deployment map
  • GET /plugin-map.json - JSON plugin deployment map

Discovery System

  • Parallel batched discovery for performance
  • GitHub API integration with static fallback
  • Service type detection (Deno Deploy, Cloudflare Pages, both, none)
  • Plugin variant detection (main and development deployments)

Performance Optimizations

  • In-memory caching eliminates KV read/write costs
  • Batched GitHub API requests
  • Timeout protection prevents worker timeouts
  • Efficient filtering of non-existent services

- Add dynamic sitemap endpoints (/sitemap.xml, /sitemap.json)
- Add plugin-map endpoints (/plugin-map.xml, /plugin-map.json)
- Remove KV namespace dependencies and analytics tracking
- Implement in-memory caching with 6hr sitemap, 2hr plugin-map cache
- Add comprehensive service discovery from GitHub repositories
- Remove write-tracker.ts and analytics directory (KV-free architecture)
- Add timeout protection (8s) for sitemap generation
- Include rich metadata: service types, deployment status, GitHub links
@coderabbitai

coderabbitai Bot commented Feb 6, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a core discovery module to probe Deno and Pages deployments, fetch plugin variant manifests with timeouts, and batch-resolve services and plugins. Introduces plugin-map-discovery and site-map-discovery with in-memory TTL caching and forced-refresh. Adds plugin-map-generator and sitemap-generator to produce XML/JSON maps and response helpers. New utilities: memory-cache, static-config, build-pages-url, get-known-services, get-known-plugins, and updated utils re-exports. Worker routes for /sitemap.xml, /sitemap.json, /plugin-map.xml, /plugin-map.json; Env gains GITHUB_TOKEN and REFRESH_SECRET.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main objective: dynamic sitemap generation and removal of KV dependencies, which aligns with all major changes across multiple files.
Description check ✅ Passed The description comprehensively covers the changeset, detailing new features, architecture improvements, endpoints, discovery system, and performance optimizations introduced in the PR.
Docstring Coverage ✅ Passed Docstring coverage is 92.73% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (6)
src/utils/get-known-plugins.ts (1)

73-75: Static fallback result is never cached.

When the GitHub API fails (or no token is provided), the static list is returned but not written to the cache. This means subsequent calls will repeat the failed API attempt each time instead of serving the fallback from cache. Consider caching the static result too, perhaps with a shorter TTL.

src/worker.ts (1)

264-281: any return types and parameter types lose type safety.

safeSitemapGeneration returns Promise<any[]>, and all handlers use request?: any. Use the proper types (SitemapEntry[], PluginMapEntry[], Request) to get compile-time safety.

Proposed fix for safeSitemapGeneration (apply same pattern to safePluginMapGeneration)
+import type { SitemapEntry } from './sitemap-generator'
+import type { PluginMapEntry } from './plugin-map-generator'
 
 async function safeSitemapGeneration(
   forceRefresh: boolean,
   githubToken: string,
-  request?: any
-  ): Promise<any[]> {
+  request?: Request
+  ): Promise<SitemapEntry[]> {
   ...
-  const entries = await Promise.race([sitemapPromise, timeoutPromise]) as any[]
+  const entries = await Promise.race([sitemapPromise, timeoutPromise]) as SitemapEntry[]

Also applies to: 324-340

src/utils/static-config.ts (1)

43-83: Repetitive triple-entry pattern for plugins could be generated.

Each plugin has 3 manually maintained entries (base, -main, -development). A helper generating variants from a base list would halve the line count and eliminate copy-paste drift.

Example
const BASE_PLUGINS = [
  'daemon-xp',
  'text-conversation-rewards',
  'daemon-task-matcher',
  // ...
]

export const STATIC_PLUGINS: PluginConfig[] = BASE_PLUGINS.flatMap(name => [
  { name, github: `ubiquity-os-marketplace/${name}` },
  { name: `${name}-main`, github: `ubiquity-os-marketplace/${name}/tree/main` },
  { name: `${name}-development`, github: `ubiquity-os-marketplace/${name}/tree/development` },
])
src/plugin-map-discovery.ts (1)

46-55: forceRefresh and request params are unused — function name getCachedPluginMapEntries is misleading.

There's no caching logic here; it always regenerates. The params forceRefresh and request are never read. Either implement caching or rename to getPluginMapEntries and drop the unused params.

src/site-map-discovery.ts (1)

48-57: getCachedSitemapEntries — dead parameters and misleading name.

forceRefresh and request are unused. The function name says "cached" but performs no caching. Either wire up the in-memory cache from memory-cache.ts (as the module docstring on line 3 promises) or simplify this to a plain pass-through with an honest name.

src/sitemap-generator.ts (1)

144-155: generated timestamp is computed independently of the caller's timestamp.

generateJsonSitemap calls new Date().toISOString() internally, while generateJsonPluginMap in plugin-map-generator.ts accepts generationTimestamp as a parameter. Consider accepting the timestamp as a parameter here too for consistency and testability.

Comment thread src/core/discovery.ts
Comment thread src/core/discovery.ts Outdated
Comment thread src/core/discovery.ts
Comment thread src/plugin-map-discovery.ts Outdated
Comment thread src/plugin-map-generator.ts
Comment thread src/site-map-discovery.ts Outdated
Comment thread src/sitemap-generator.ts
Comment thread src/utils/memory-cache.ts
Comment thread src/worker.ts Outdated
Comment thread src/worker.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/worker.ts (1)

264-268: Promise<any[]> loses type safety across both safe-generation wrappers.

Use Promise<SitemapEntry[]> and Promise<PluginMapEntry[]> respectively. Also, request?: any should be request?: Request.

Also applies to: 324-328

src/site-map-discovery.ts (1)

38-47: ServiceType derivation logic is duplicated with plugin-map-discovery.ts (lines 27-35).

Consider extracting a shared helper like resolvePluginServiceType(variants) to keep both modules in sync.

Comment thread src/sitemap-generator.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (10)
src/utils/memory-cache.ts (3)

7-8: Unbounded cache — no eviction policy.

The Map grows without limit. In a long-lived isolate serving many unique cache keys, memory can grow indefinitely. Consider adding a max-size cap with LRU eviction, or at minimum document the assumption that key cardinality is small.


15-28: Async wrappers over synchronous Map operations.

All functions are async but perform no async work. This adds microtask overhead per call. Fine if you anticipate swapping to an async backend later, but worth a brief inline comment documenting that intent.


86-100: getCacheStats doesn't evict expired entries it discovers.

It iterates and finds expired keys but leaves them in the map. Opportunistic cleanup here would be cheap.

Proposed fix
   for (const [key, item] of GLOBAL_CACHE) {
     if (now < item.expiresAt) {
       validKeys.push(key)
+    } else {
+      GLOBAL_CACHE.delete(key)
     }
   }
src/core/discovery.ts (2)

30-50: url parameter is a dummy — confusing API surface.

discoverServiceType accepts a URL only so it can be passed to buildDenoUrl/buildPagesUrl, but the caller at line 136 constructs a throwaway URL with no meaningful path or query. The builders only use url.pathname and url.search, which are always / and "" here.

Consider accepting just the subdomain and constructing the base URLs internally, or document why the full URL is needed.


55-78: response.json() can throw on invalid JSON — unhandled inside the try.

If the response body is not valid JSON (e.g., an HTML error page returned with 200 OK), response.json() throws, which is caught by the outer catch. This works, but the error is silently swallowed. A brief console.warn in the catch would help debugging.

src/worker.ts (1)

63-63: Unsafe cast of header value to CacheControlValue.

Any arbitrary header string is cast to CacheControlValue. While it's only compared with === 'refresh', the as cast is misleading. A simple string | null would be more honest.

Proposed fix
-    const cacheControl = request.headers.get('X-Cache-Control') as CacheControlValue
+    const cacheControl = request.headers.get('X-Cache-Control')
src/sitemap-generator.ts (3)

121-128: escapeXml is duplicated in plugin-map-generator.ts (lines 11-18).

Extract to a shared utility (e.g., src/utils/escape-xml.ts).


57-62: Past review feedback addressed — -none check moved first.

The plugin-none dead code issue is fixed. However, note that after line 59, both remaining branches (lines 60–61) return 'weekly', making the startsWith('plugin-') check on line 60 redundant. The function could just be:

if (serviceType.endsWith('-none')) return 'monthly'
return 'weekly'

20-22: pluginManifest typed as any.

Use PluginManifest from ./types for type safety. It's already imported in the codebase.

Proposed fix
+import type { PluginManifest } from './types'
+
 // in SitemapEntry interface:
   metadata?: {
-    pluginManifest?: any
+    pluginManifest?: PluginManifest
     githubRepo?: string
   }

 // in createSitemapEntry signature:
-  pluginManifest: any,
+  pluginManifest: PluginManifest | undefined,
src/plugin-map-generator.ts (1)

112-115: GitHub org is hardcoded to ubiquity-os-marketplace.

If plugins can come from other orgs (e.g., ubiquity), this will produce incorrect repo links. Consider deriving the org from the discovery source or making it configurable.

Comment thread src/core/discovery.ts Outdated
Comment thread src/worker.ts
Comment thread src/worker.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/core/discovery.ts (3)

55-78: Uncaught JSON parse error on malformed response body.

If the server returns 200 OK with non-JSON body (e.g., HTML error page), response.json() throws a SyntaxError. The outer catch handles it, so this is functionally safe — but logging the error there would help debug manifest discovery issues in production.


102-111: Thin pass-through wrappers add indirection without value.

getAllServices and getAllPlugins just delegate to getKnownServices/getKnownPlugins. Consider calling the utils directly in discoverAllServices/discoverAllPlugins to reduce indirection.


116-140: discoverServiceType receives a synthetic URL — verify build helpers only use the host.

Line 128 constructs new URL(...) purely for discovery. If buildDenoUrl/buildPagesUrl append url.pathname + url.search, the HEAD check will hit / which is fine. But passing a full URL object when only the subdomain matters is misleading. Consider having discoverServiceType build the URL internally from just the subdomain.

Comment thread src/core/discovery.ts
Comment thread src/worker.ts Outdated
Comment thread src/worker.ts
@3scava1i3r

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Feb 10, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@3scava1i3r

Copy link
Copy Markdown
Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Feb 10, 2026

Copy link
Copy Markdown
✅ Actions performed

Reviews resumed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/worker.ts (3)

15-17: Merge duplicate imports from './types'.

Two separate import statements from the same module.

Proposed fix
-import type { PluginMapEntry } from './types'
-import { generateXmlPluginMap, generateJsonPluginMap, createXmlPluginMapResponse, createJsonPluginMapResponse } from './plugin-map-generator'
-import type { CacheControlValue } from './types'
+import type { PluginMapEntry, CacheControlValue } from './types'
+import { generateXmlPluginMap, generateJsonPluginMap, createXmlPluginMapResponse, createJsonPluginMapResponse } from './plugin-map-generator'

305-318: Unused request parameter.

request?: any on line 308 is never used. Remove it to keep the signature honest.

Proposed fix
 async function handleSitemapXml(
   forceRefresh: boolean,
   githubToken: string,
-  request?: any
-  ): Promise<Response> {
+): Promise<Response> {

277-363: safeSitemapGeneration and safePluginMapGeneration are near-identical.

Consider extracting a generic withTimeout<T>(fn: () => Promise<T>, ms: number, label: string): Promise<T> helper to DRY this up.

Comment thread src/worker.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/worker.ts (1)

303-316: Unused request?: any parameter.

request is accepted but never read. Remove it to keep the signature honest.

 async function handleSitemapXml(
   forceRefresh: boolean,
   githubToken: string,
-  request?: any
   ): Promise<Response> {

Comment thread src/worker.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/worker.ts (1)

277-298: Consider extracting the timeout-race pattern into a shared helper.

safeSitemapGeneration and safePluginMapGeneration are structurally identical — only the inner call differs. A generic wrapper eliminates the duplication:

async function withTimeout<T>(fn: () => Promise<T>, label: string, ms = 8000): Promise<T> {
  let timer: ReturnType<typeof setTimeout> | undefined
  const timeout = new Promise<never>((_, reject) => {
    timer = setTimeout(() => reject(new Error(`${label} timeout`)), ms)
  })
  try {
    return await Promise.race([fn(), timeout])
  } finally {
    clearTimeout(timer)
  }
}

Also applies to: 338-359

Comment thread src/worker.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/worker.ts (2)

337-358: safePluginMapGeneration is a near-clone of safeSitemapGeneration.

Extract a generic helper to reduce duplication:

♻️ Suggested refactor
+async function withTimeout<T>(
+  label: string,
+  task: Promise<T>,
+  timeoutMs = 8000,
+): Promise<T> {
+  let timer: ReturnType<typeof setTimeout> | undefined
+  const timeoutPromise = new Promise<never>((_, reject) => {
+    timer = setTimeout(() => reject(new Error(`${label} timeout`)), timeoutMs)
+  })
+  try {
+    return await Promise.race([task, timeoutPromise])
+  } finally {
+    clearTimeout(timer)
+  }
+}

Then both call sites become one-liners:

const entries = await withTimeout('Sitemap generation', getCachedSitemapEntries(githubToken, forceRefresh))

303-306: Minor formatting: closing paren is misaligned.

 async function handleSitemapXml(
   forceRefresh: boolean,
   githubToken: string,
-  ): Promise<Response> {
+): Promise<Response> {

Same applies to handleSitemapJson on line 323.

Comment thread src/worker.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant