Skip to content

Feat/optimize meta sync#1049

Open
guitavano wants to merge 10 commits intomainfrom
feat/optimize-meta-sync
Open

Feat/optimize meta sync#1049
guitavano wants to merge 10 commits intomainfrom
feat/optimize-meta-sync

Conversation

@guitavano
Copy link
Copy Markdown
Contributor

@guitavano guitavano commented Feb 3, 2026

Summary by cubic

Cuts meta sync time from 15s+ to ~25–65ms by gzipping SSE, caching schemas, and pre-warming meta. Also adds folder metadata to /deco/meta and a readiness endpoint.

  • Performance

    • Gzip large SSE messages when the client supports it; client-side decompression added.
    • Cache generated schemas on disk (schemas.gen.json) keyed by manifest hash + deco version.
    • Faster FS snapshot: skip heavy dirs (.git, node_modules, etc.), only read .json, and skip large generated files.
    • Eager-start worker and prefetch /deco/meta on boot to reduce first-request latency.
    • Skip writing manifest.gen.ts when unchanged to avoid needless HMR.
  • New Features

    • /_ready endpoint: long-poll readiness with bounded timeouts (1–120s), returns readiness and elapsed time.
    • Folder metadata: reads folder/_folder.json (description, icon) and exposes it as folderMeta in /deco/meta.

Written for commit 97e7a65. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added health check and readiness endpoints for improved daemon monitoring and startup verification.
    • Introduced folder-level metadata support with custom descriptions and icons for better organization.
  • Performance Improvements

    • Optimized file processing with selective scanning and metadata caching.
    • Enabled gzip compression for large payloads to reduce data transfer.
  • Chores

    • Bumped version to 1.136.0-beta.

vibegui and others added 9 commits February 2, 2026 15:16
Read _folder.json files from block folders and include their
metadata (description, icon) in the MetaInfo response, keyed
by blockType and folder name.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add gzip compression for large SSE payloads (>50KB) to reduce tunnel latency
- Add schema disk caching to avoid regenerating schema on unchanged manifests
- Skip manifest.gen.ts write when content unchanged to prevent HMR restarts
- Pre-warm meta on daemon startup to avoid waiting for watchMeta() loop
- Add decompression support in client for gzipped SSE events

This reduces meta-info delivery from 15+ seconds to ~25-65ms by compressing
the ~816KB payload to ~123KB before sending through the tunnel.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two optimizations:
1. Skip walking node_modules, .git, _fresh, etc. using walk's skip option
2. Only read .json files for metadata - .ts/.tsx files can't be blocks

Also skip large generated files: schemas.gen.json, manifest.gen.ts, fresh.gen.ts

Results: 56ms (down from 470ms) - 8.4x faster

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add X-SSE-Encoding header check for backward compatibility.
Old clients without the header will receive uncompressed data.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. SKIP_DIRS entries like .git, .deno, .cache were not escaped,
   causing the regex to match any character (e.g., /xdeno/ would match).
   Now properly escaped as \.git, \.deno, \.cache.

2. Removed followSymlinks:false from walk() options. This was new
   and could break k8s environments that use symlinks for volumes
   or mounted files. The default (follow symlinks) is restored.

Also added .claude/ to .gitignore.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SSE improvements:
- Wrap enqueue() internals in try/catch to prevent stream crashes
- Wrap file sync loop in try/catch to handle startFS errors gracefully
- Log errors instead of crashing silently

eagerStart improvements:
- Add 60s timeout to prevent hanging forever if worker never becomes ready
- Log success/failure/timeout for debugging
- Errors are now logged instead of silently swallowed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. SSE: Add X-SSE-Encoding response header when client supports gzip
   - Admin-cx checks for this header to decide whether to use readFromStream
   - Without it, gzip detection was broken

2. /_ready: Fix fake timeout that didn't actually abort anything
   - Replaced unused AbortController with proper Promise.race
   - Now correctly returns 503 on timeout

3. Schema cache: Add deco version to invalidation key
   - Cache now invalidates when deco is upgraded
   - Added timestamp for debugging

4. Cleanup: Remove unused variables
   - Removed daemonStartTime from main.ts
   - Removed iteration from meta.ts watchMeta

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1. runtime/features/meta.ts: Only ignore NotFound for _folder.json
   - Other errors (invalid JSON) now logged for debugging

2. daemon/fs/api.ts: Escape SEPARATOR for regex on Windows
   - Backslash is special in regex, now properly escaped

3. scripts/apps/bundle.lib.ts: Only ignore NotFound when reading manifest
   - Permission/I/O errors now surface properly

4. daemon/main.ts /_ready endpoint:
   - Add timeout validation with min/max bounds (1s-120s)
   - Fix timer leak by clearing setTimeout when worker finishes first

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 3, 2026

Tagging Options

Should a new tag be published when this PR is merged?

  • 👍 for Patch 1.137.1 update
  • 🎉 for Minor 1.138.0 update
  • 🚀 for Major 2.0.0 update

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

This PR implements gzip compression for streaming data, optimizes filesystem traversal with skip mechanisms, introduces daemon readiness checks with eager worker startup, adds folder-level metadata support, implements schema caching, and includes conditional manifest writing to avoid unnecessary disk writes.

Changes

Cohort / File(s) Summary
Version Bumps
deno.json, dev/deno.json, scripts/deno.json
Updated version from 1.135.0 to 1.136.0-beta across all deno.json manifest files.
Gitignore Configuration
.gitignore
Added .claude/ directory to ignore rules.
Streaming Gzip Decompression
clients/withManifest.ts
Added decompress() and parseChunk() utilities to handle base64-encoded gzip-compressed chunks in streaming data; replaced direct JSON.parse calls with parseChunk() calls for decompressed-then-parsed data.
Daemon Endpoints & Worker Management
daemon/main.ts
Added /_healthcheck endpoint returning daemon version; introduced /_ready endpoint implementing timeout-bounded worker startup polling with error handling; implemented eager startup mechanism in onListen hook with configurable timeouts to pre-start worker and trigger meta generation.
Metadata Pre-Resolution
daemon/meta.ts
Added public setMetaIfPending() function to externally pre-resolve pending meta; modified start() signature to always return meta-info when detail exists (removed prior timestamp comparison check).
Filesystem Optimization
daemon/fs/api.ts
Introduced SKIP_DIRS and SKIP_FILES constants with skip regex filtering; added early exit for non-JSON files in inferMetadata(); enhanced start() with performance accounting (totalStart, fileCount, yieldedCount), parallel metadata/mtime retrieval, and precomputed git status yield instead of runtime invocation.
SSE Gzip Compression
daemon/sse/api.ts
Added compress() helper for gzip-compression and base64-encoding; detects client gzip support via X-SSE-Encoding header; conditionally compresses payloads >50k and uses gzip prefix for compressed data; wrapped enqueue in try/catch for resilience; added X-SSE-Encoding response header when compression enabled.
Schema Caching
engine/schema/reader.ts
Introduced schema cache file with SchemaCache interface and helper functions (hashManifest, loadFromCache, saveToCache); modified genSchemas to check cache before generation and save results asynchronously; tracks Deco version in cache validity; removed previous in-process cleanup logic.
Folder-Level Metadata
runtime/features/meta.ts
Added public FolderMeta interface and FolderMetaMap type; extended MetaInfo with optional folderMeta field; implemented readFolderMeta() to discover and extract folder metadata from _folder.json files and assemble folder metadata map.
Manifest Generation Optimization
scripts/apps/bundle.lib.ts
Changed manifest.gen.ts write behavior from unconditional to conditional: compares newContent with existing file, skips write if identical (avoiding HMR triggers), handles NotFound errors gracefully, logs skip/completion status.
Dependency Import Management
scripts/dev.ts
Added logic to copy required dependency ("@deco/deno-ast-wasm") into project imports after exports processing: deletes in reset mode, copies from imports to entries if present in imports but not in entries.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Daemon
    participant Worker
    participant Timeout

    Client->>Daemon: GET /_ready
    activate Daemon
    
    Daemon->>Daemon: Start timeout race
    par Worker Startup
        Daemon->>Worker: Spawn/initialize worker
        activate Worker
        Worker->>Worker: Execute startup logic
    and Timeout Race
        Timeout->>Timeout: Wait (MIN to MAX bounds)
    end
    
    alt Worker completes first
        Worker->>Daemon: Worker ready
        deactivate Worker
        Daemon->>Daemon: Trigger /deco/meta (optional)
        Daemon->>Client: 200 OK {ready: true}
    else Timeout expires first
        Timeout->>Daemon: Timeout triggered
        Daemon->>Client: 503 Service Unavailable {ready: false, elapsed, error}
    end
    
    deactivate Daemon
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through gzipped streams so light,
Skipping folders, caching schemas tight,
Ready endpoints race against the clock,
Metadata flows while daemon stands stock,
Version bumped to beta, oh what a sight! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title "Feat/optimize meta sync" directly corresponds to the main objective of the PR: performance improvements to meta sync operations reducing sync time from 15s+ to ~25–65ms.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/optimize-meta-sync

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.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 10 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="daemon/main.ts">

<violation number="1" location="daemon/main.ts:447">
P2: Timer leak: The timeout is not cleared when the worker finishes first. Unlike the `/_ready` endpoint which correctly uses `clearTimeout(timeoutId!)`, this Promise.race pattern leaves the 60-second timer running unnecessarily. Apply the same pattern used in `/_ready`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread daemon/main.ts
}
return "success";
})(),
new Promise<"timeout">((resolve) =>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Feb 3, 2026

Choose a reason for hiding this comment

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

P2: Timer leak: The timeout is not cleared when the worker finishes first. Unlike the /_ready endpoint which correctly uses clearTimeout(timeoutId!), this Promise.race pattern leaves the 60-second timer running unnecessarily. Apply the same pattern used in /_ready.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At daemon/main.ts, line 447:

<comment>Timer leak: The timeout is not cleared when the worker finishes first. Unlike the `/_ready` endpoint which correctly uses `clearTimeout(timeoutId!)`, this Promise.race pattern leaves the 60-second timer running unnecessarily. Apply the same pattern used in `/_ready`.</comment>

<file context>
@@ -351,9 +420,55 @@ const stableEnvironmentName = () => {
+        }
+        return "success";
+      })(),
+      new Promise<"timeout">((resolve) =>
+        setTimeout(() => resolve("timeout"), EAGER_START_TIMEOUT_MS)
+      ),
</file context>
Fix with Cubic

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@scripts/dev.ts`:
- Around line 25-33: The TypeScript error TS7053 occurs because the JSON-typed
imports object lacks an index signature, so using the string variable dep to
index it is invalid; cast imports to a string-keyed map before indexing (e.g.,
const importMap = imports as Record<string, unknown> or as any) and then replace
imports[dep] with importMap[dep] inside the loop that iterates requiredDeps
(referencing requiredDeps, dep, imports, entries, isReset) so the assignment
entries[dep] = importMap[dep] compiles without error.
🧹 Nitpick comments (3)
engine/schema/reader.ts (2)

17-28: Consider: hashing only keys may cause stale cache when block content changes.

The hashManifest function only hashes block keys, not values. If a block's implementation changes (e.g., different props/types) but the key remains the same, the cache won't invalidate.

The decoVersion check provides some protection, but local development changes to blocks won't trigger cache invalidation until a version bump.

Is this intentional, relying on version bumps to handle content changes? If local development scenarios need fresh schemas after block modifications, you may want to include file modification times or a content hash.


80-81: Minor: fire-and-forget cache save won't surface write errors.

The saveSchemaCache call on line 81 is intentionally not awaited, which is fine for a non-critical cache. However, if debugging cache issues, consider adding a debug log inside the catch block of saveSchemaCache.

runtime/features/meta.ts (1)

88-103: Consider parallelizing file reads for folders.

The loop reads _folder.json files sequentially. For projects with many folders, parallelizing these reads could reduce latency:

♻️ Suggested parallel reads
   // Read _folder.json for each folder
-  for (const folder of folders) {
-    try {
-      const filePath = `./${blockType}/${folder}/_folder.json`;
-      const content = await Deno.readTextFile(filePath);
-      const meta = JSON.parse(content) as FolderMeta;
-      if (meta.description || meta.icon) {
-        folderMeta[blockType] ??= {};
-        folderMeta[blockType][folder] = meta;
-      }
-    } catch (err) {
-      // Only ignore missing files, surface other errors (e.g., invalid JSON)
-      if (!(err instanceof Deno.errors.NotFound)) {
-        console.error(`[meta] Error reading ${blockType}/${folder}/_folder.json:`, err);
-      }
-    }
-  }
+  await Promise.all([...folders].map(async (folder) => {
+    try {
+      const filePath = `./${blockType}/${folder}/_folder.json`;
+      const content = await Deno.readTextFile(filePath);
+      const meta = JSON.parse(content) as FolderMeta;
+      if (meta.description || meta.icon) {
+        folderMeta[blockType] ??= {};
+        folderMeta[blockType][folder] = meta;
+      }
+    } catch (err) {
+      if (!(err instanceof Deno.errors.NotFound)) {
+        console.error(`[meta] Error reading ${blockType}/${folder}/_folder.json:`, err);
+      }
+    }
+  }));

Comment thread scripts/dev.ts
Comment on lines +25 to +33
// Copy dependencies that the deco package needs but projects might not have
const requiredDeps = ["@deco/deno-ast-wasm"];
for (const dep of requiredDeps) {
if (isReset) {
delete entries[dep];
} else if (imports[dep] && !entries[dep]) {
entries[dep] = imports[dep];
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix TypeScript error: indexing typed object with string variable.

The pipeline is failing with TS7053 because imports has a specific object type (from the JSON import) without an index signature. Using dep (a string) to access imports[dep] is not allowed.

🐛 Proposed fix: cast imports to allow string indexing
 // Copy dependencies that the deco package needs but projects might not have
 const requiredDeps = ["@deco/deno-ast-wasm"];
+const importsRecord = imports as Record<string, string>;
 for (const dep of requiredDeps) {
   if (isReset) {
     delete entries[dep];
-  } else if (imports[dep] && !entries[dep]) {
-    entries[dep] = imports[dep];
+  } else if (importsRecord[dep] && !entries[dep]) {
+    entries[dep] = importsRecord[dep];
   }
 }
🧰 Tools
🪛 GitHub Actions: Publish

[error] 30-30: TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ "$fresh/": string; "@cliffy/prompt": string; "@core/asyncutil": string; "@deco/codemod-toolkit": string; "@deco/deno-ast-wasm": string; "@deco/durable": string; "@deco/warp": string; "@hono/hono": string; ... 23 more ...; "std/": string; }'. No index signature with a parameter of type 'string' was found on type '{ "$fresh/": string; "@cliffy/prompt": string; "@core/asyncutil": string; "@deco/codemod-toolkit": string; "@deco/deno-ast-wasm": string; "@deco/durable": string; "@deco/warp": string; "@hono/hono": string; ... 23 more ...; "std/": string; }'.


[error] 31-31: TS7053: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ "$fresh/": string; "@cliffy/prompt": string; "@core/asyncutil": string; "@deco/codemod-toolkit": string; "@deco/deno-ast-wasm": string; "@deco/durable": string; "@deco/warp": string; "@hono/hono": string; ... 23 more ...; "std/": string; }'. No index signature with a parameter of type 'string' was found on type '{ "$fresh/": string; "@cliffy/prompt": string; "@core/asyncutil": string; "@deco/codemod-toolkit": string; "@deco/deno-ast-wasm": string; "@deco/durable": string; "@deco/warp": string; "@hono/hono": string; ... 23 more ...; "std/": string; }'.

🤖 Prompt for AI Agents
In `@scripts/dev.ts` around lines 25 - 33, The TypeScript error TS7053 occurs
because the JSON-typed imports object lacks an index signature, so using the
string variable dep to index it is invalid; cast imports to a string-keyed map
before indexing (e.g., const importMap = imports as Record<string, unknown> or
as any) and then replace imports[dep] with importMap[dep] inside the loop that
iterates requiredDeps (referencing requiredDeps, dep, imports, entries, isReset)
so the assignment entries[dep] = importMap[dep] compiles without error.

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.

2 participants