Skip to content

feat(restate): add services folder, simplify audioPipeline, remove userRateLimiter#1997

Merged
yujonglee merged 2 commits intomainfrom
devin/1764474030-restate-services
Nov 30, 2025
Merged

feat(restate): add services folder, simplify audioPipeline, remove userRateLimiter#1997
yujonglee merged 2 commits intomainfrom
devin/1764474030-restate-services

Conversation

@yujonglee
Copy link
Contributor

@yujonglee yujonglee commented Nov 30, 2025

Summary

Adds a new services/ folder structure under apps/restate/src/ and consolidates the codebase:

New services:

  • rate-limit.ts: A Virtual Object implementing per-key rate limiting with a sliding window pattern. Provides a limiter() helper for easy consumption from other services.
  • stt-file.ts: A Workflow for STT file processing using Deepgram. Handles the transcription lifecycle (queue → transcribe → done/error) with callback-based result handling and automatic file cleanup.

Simplifications:

  • audioPipeline.ts: Removed LLM processing step (now STT-only), uses new services/rate-limit.ts, simplified status states from 6 to 4 (QUEUED, TRANSCRIBING, DONE, ERROR)
  • userRateLimiter.ts: Deleted entirely, replaced by services/rate-limit.ts
  • index.ts: Now registers [audioPipeline, rateLimiter, sttFile]

Review & Testing Checklist for Human

  • Verify web app compatibility: The apps/web/src/functions/transcription.ts expects StatusStateType with llmResult and statuses TRANSCRIBED/LLM_RUNNING which are now removed. The UI only uses status and transcript, but the Zod schemas may need updating to match.
  • Test the Deepgram callback flow: Both AudioPipeline (/AudioPipeline/{key}/onDeepgramResult) and SttFile (/SttFile/{key}/onTranscript) have callback handlers - verify they work with actual Deepgram webhooks.
  • Verify env access pattern: The ctx.request().extraArgs[0] as Env type assertion could fail at runtime if env isn't properly passed through the endpoint handler.
  • Confirm no external callers of UserRateLimiter: The old UserRateLimiter service name is gone - any external systems calling it will break.

Recommended test plan: Deploy to a staging Restate environment and trigger both AudioPipeline/run and SttFile/run invocations with valid audio files. Verify the web file-transcription page still works end-to-end.

Notes

  • The sttFile workflow is designed to be composable for future multiple LLM output versions (as mentioned in requirements) - it only handles STT, leaving LLM processing to be added separately.
  • Rate limit config is hardcoded (60s window, 5 max) - may want to make this configurable.
  • Updated durable-meeting-summary.mdx article to reflect simplified flow (removed LLM step, renamed RateLimiter).

Link to Devin run: https://app.devin.ai/sessions/2dd61f559b824d26ae2c2f8129ff01e4
Requested by: yujonglee (@yujonglee)

- Create apps/restate/src/services folder for organized service structure
- Add rate-limit.ts: Virtual Object for per-key rate limiting with token bucket pattern
- Add stt-file.ts: Workflow for STT file processing with Deepgram integration
- Update index.ts to register new services alongside existing ones
- Design supports future multiple LLM output versions

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@devin-ai-integration
Copy link
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Nov 30, 2025

Deploy Preview for hyprnote ready!

Name Link
🔨 Latest commit c1f2ee4
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/692bc79fd7e6f80008054946
😎 Deploy Preview https://deploy-preview-1997--hyprnote.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Nov 30, 2025

Deploy Preview for hyprnote-storybook ready!

Name Link
🔨 Latest commit c1f2ee4
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/692bc79ff7405300086504cc
😎 Deploy Preview https://deploy-preview-1997--hyprnote-storybook.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 30, 2025

📝 Walkthrough

Walkthrough

Replaces the per-user limiter with a new generic rate limiter and adds a new speech-to-text workflow that orchestrates Deepgram transcription, callback handling, and file cleanup. The audio pipeline is simplified to transcription-only (LLM steps removed). Index service registration updated accordingly; docs updated to reflect flow changes.

Changes

Cohort / File(s) Summary
Service Registration
apps/restate/src/index.ts
Replaced userRateLimiter import/registration with rateLimiter and added sttFile to the services array alongside audioPipeline.
New Rate Limiter
apps/restate/src/services/rate-limit.ts
Added new restate object rateLimiter with checkAndConsume and reset handlers, sliding-window state, 429 TerminalError on limit exceed, RateLimiter type alias, and limiter(ctx, key) helper.
New Speech-to-Text Workflow
apps/restate/src/services/stt-file.ts
Added sttFile workflow: run (rate-limited transcription start, signed URL generation, stores requestId, awaits transcript, finalizes and deletes file), onTranscript (Deepgram callback handler resolving transcript), and getStatus. Includes Zod schemas, Deepgram client invocation, callback parsing, and exported types.
Audio pipeline simplification
apps/restate/src/audioPipeline.ts
Removed LLM steps and related states/types (llmResult, TRANSCRIBED, LLM_RUNNING). Renamed/streamlined Deepgram interaction to transcribe, updated Deepgram callback shape and types, centralized cleanup, and replaced per-user limiter usage with generic limiter(ctx, userId).
Removed per-user limiter
apps/restate/src/userRateLimiter.ts
Deleted the previous userRateLimiter module and its exports (object, types, and limiterForUser).
Documentation
apps/web/content/articles/durable-meeting-summary.mdx
Updated architecture diagram and narrative: renamed participant UserRateLimiterRateLimiter, removed LLM components and status steps, simplified final payload to transcript-only.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Restate as Restate App (workflow)
    participant RateLimit as RateLimiter
    participant Storage as File Storage
    participant Deepgram as Deepgram API

    Client->>Restate: Submit audio for transcription (run)
    Restate->>RateLimit: checkAndConsume(userId)
    alt Rate limit exceeded
        RateLimit-->>Restate: TerminalError 429
        Restate-->>Client: 429 Too Many Requests
    else Within quota
        RateLimit-->>Restate: OK
        Restate->>Storage: Generate signed URL for file
        Storage-->>Restate: Signed URL
        Restate->>Deepgram: Start transcription (audioUrl, callbackUrl)
        Deepgram-->>Restate: requestId
        Restate->>Restate: await transcript (ctx.promise "transcript")
        Deepgram->>Restate: Callback -> onTranscript(transcript)
        Restate->>Restate: resolve transcript, set DONE
        Restate->>Storage: Delete audio file (cleanup)
        Storage-->>Restate: Deleted
        Restate-->>Client: Success (transcript stored)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review areas needing extra attention:
    • apps/restate/src/services/stt-file.ts: async/callback promise flow, Deepgram error handling, signed URL and file deletion in finally.
    • apps/restate/src/services/rate-limit.ts: sliding-window boundary conditions, persistence/store semantics, TerminalError usage.
    • apps/restate/src/audioPipeline.ts: type adjustments (removed llmResult), updated Deepgram callback parsing, and integration points with new limiter API.
    • apps/restate/src/index.ts and docs: ensure service registration and exported names match runtime wiring.

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding a services folder, simplifying audioPipeline by removing LLM processing, and removing the userRateLimiter module in favor of the new rate-limit service.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, detailing the new services structure, simplifications made to audioPipeline, and providing a testing checklist relevant to the changes.
✨ 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 devin/1764474030-restate-services

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

Copy link
Contributor

@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: 0

🧹 Nitpick comments (3)
apps/restate/src/services/stt-file.ts (3)

81-81: Unsafe type assertion for environment access.

The as Env assertion assumes extraArgs[0] is always a valid Env object. If the endpoint is misconfigured or the workflow is invoked incorrectly, this will fail silently and cause cryptic runtime errors when accessing env properties.

Consider validating with envSchema.parse() for consistency with how it's done in index.ts, or at minimum add a runtime check.

-        const env = ctx.request().extraArgs[0] as Env;
+        const env = envSchema.parse(ctx.request().extraArgs[0]);

This requires importing envSchema from ../env.


112-116: Silently swallowing cleanup errors may hide issues.

The .catch(() => {}) pattern suppresses all cleanup failures. While the intent is reasonable (don't fail the workflow due to cleanup), completely ignoring errors makes debugging difficult if files aren't being deleted.

Consider logging the error for observability:

          await ctx.run("cleanup", () =>
-            deleteFile(env, input.fileId).catch(() => {}),
+            deleteFile(env, input.fileId).catch((e) => {
+              console.warn(`Failed to cleanup file ${input.fileId}:`, e);
+            }),
          );

145-149: Minor: Redundant nullish coalescing.

transcript ?? undefined and error ?? undefined are redundant since if the values are already undefined, this expression returns undefined anyway. The ctx.get<string>() returns string | null typically, so if it returns null, the ?? undefined converts it.

This is fine as-is for explicit null-to-undefined conversion, but could be simplified if ctx.get already returns undefined for missing keys.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2984e5 and 874f697.

📒 Files selected for processing (3)
  • apps/restate/src/index.ts (1 hunks)
  • apps/restate/src/services/rate-limit.ts (1 hunks)
  • apps/restate/src/services/stt-file.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Agent implementations should use TypeScript and follow the established architectural patterns defined in the agent framework
Agent communication should use defined message protocols and interfaces

Files:

  • apps/restate/src/services/rate-limit.ts
  • apps/restate/src/services/stt-file.ts
  • apps/restate/src/index.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/restate/src/services/rate-limit.ts
  • apps/restate/src/services/stt-file.ts
  • apps/restate/src/index.ts
🧬 Code graph analysis (1)
apps/restate/src/index.ts (3)
apps/restate/src/env.ts (2)
  • Env (12-12)
  • envSchema (3-10)
apps/restate/src/services/rate-limit.ts (1)
  • rateLimiter (13-43)
apps/restate/src/services/stt-file.ts (1)
  • sttFile (69-153)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: fmt
🔇 Additional comments (11)
apps/restate/src/index.ts (2)

5-6: LGTM!

The new imports follow the existing import pattern and are correctly placed alphabetically within the services group.


13-13: LGTM!

The services array correctly registers both new services alongside the existing ones. The endpoint will now expose all four services.

apps/restate/src/services/rate-limit.ts (4)

3-11: LGTM!

The internal interfaces are well-defined and appropriately scoped (not exported), keeping the public API minimal.


39-41: LGTM!

The reset handler correctly clears state, allowing for manual rate limit resets when needed.


45-49: LGTM!

The limiter helper provides a clean, typed API for other services to acquire rate limiter clients by key. This is well-utilized by stt-file.ts.


16-37: I'll verify the concern about potential naming collision between the RateLimiter service and the existing userRateLimiter. Let me investigate the Restate service names and how they're registered.
<function_calls>

#!/bin/bash

Check the service name in rate-limit.ts

echo "=== Checking rate-limit.ts for service name ==="
rg -n "RateLimiter" apps/restate/src/services/rate-limit.ts -B2 -A2

Check if there's a registration or export statement

echo -e "\n=== Checking for service registration in rate-limit.ts ==="
rg -n "export|restate." apps/restate/src/services/rate-limit.ts

Check the userRateLimiter file

echo -e "\n=== Checking userRateLimiter.ts ==="
cat apps/restate/src/userRateLimiter.ts

Check index.ts imports

echo -e "\n=== Checking index.ts ==="
cat apps/restate/src/index.ts


</function_calls>

apps/restate/src/services/stt-file.ts (5)

1-8: LGTM!

Imports are well-organized and all dependencies are appropriately sourced from their respective packages.


10-48: LGTM!

The Zod schemas are well-structured. The DeepgramCallback schema correctly handles the nested optional structure of Deepgram's response format. Type exports are appropriately derived using z.infer.


50-67: LGTM!

The transcribe helper cleanly encapsulates the Deepgram API call with proper error handling for both API errors and missing request IDs.


138-151: LGTM!

The getStatus handler correctly retrieves and returns the current workflow state with appropriate fallback defaults.


120-136: The promise resolution is inherently idempotent per Restate's durable-promise semantics—no issue exists.

Restate's durable promises are single-shot primitives that can be resolved at most once. The outcome is recorded in workflow history immediately, and subsequent resolution attempts are benign no-ops. The existing check if (existing !== undefined) return; provides an extra defensive layer, but even without it, concurrent callbacks cannot cause problems: Restate's design eliminates the race condition by guaranteeing a single canonical outcome.

- Simplify audioPipeline.ts: remove LLM processing, use new rate-limit service
- Remove userRateLimiter.ts (replaced by services/rate-limit.ts)
- Update index.ts to remove old userRateLimiter registration
- Update MDX article to reference new RateLimiter and simplified flow

Co-Authored-By: yujonglee <yujonglee.dev@gmail.com>
@devin-ai-integration devin-ai-integration bot changed the title feat(restate): add services folder with rate-limit and stt-file services feat(restate): add services folder, simplify audioPipeline, remove userRateLimiter Nov 30, 2025
Copy link
Contributor

@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: 0

🧹 Nitpick comments (2)
apps/restate/src/audioPipeline.ts (2)

83-83: Unsafe type assertion for environment access.

The ctx.request().extraArgs[0] as Env pattern assumes extraArgs is populated and the first element is an Env object. If the deployment misconfigures this, you'll get a runtime error with no clear indication of the cause.

Consider adding a validation check:

-        const env = ctx.request().extraArgs[0] as Env;
+        const env = ctx.request().extraArgs[0] as Env | undefined;
+        if (!env?.DEEPGRAM_API_KEY || !env?.RESTATE_INGRESS_URL) {
+          throw new Error("Missing required environment configuration");
+        }

115-117: Silent error swallowing in cleanup may hinder debugging.

Swallowing cleanup errors with .catch(() => {}) is acceptable for non-fatal operations, but completely hiding failures makes it difficult to diagnose issues like permission errors or misconfigured storage.

Consider logging the error:

          await ctx.run("cleanup", () =>
-            deleteFile(env, req.fileId).catch(() => {}),
+            deleteFile(env, req.fileId).catch((e) => {
+              console.warn(`Cleanup failed for fileId=${req.fileId}:`, e);
+            }),
          );
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 874f697 and c1f2ee4.

📒 Files selected for processing (4)
  • apps/restate/src/audioPipeline.ts (4 hunks)
  • apps/restate/src/index.ts (1 hunks)
  • apps/restate/src/userRateLimiter.ts (0 hunks)
  • apps/web/content/articles/durable-meeting-summary.mdx (2 hunks)
💤 Files with no reviewable changes (1)
  • apps/restate/src/userRateLimiter.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/restate/src/index.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: Agent implementations should use TypeScript and follow the established architectural patterns defined in the agent framework
Agent communication should use defined message protocols and interfaces

Files:

  • apps/restate/src/audioPipeline.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Avoid creating a bunch of types/interfaces if they are not shared. Especially for function props, just inline them instead.
Never do manual state management for form/mutation. Use useForm (from tanstack-form) and useQuery/useMutation (from tanstack-query) instead for 99% of cases. Avoid patterns like setError.
If there are many classNames with conditional logic, use cn (import from @hypr/utils). It is similar to clsx. Always pass an array and split by logical grouping.
Use motion/react instead of framer-motion.

Files:

  • apps/restate/src/audioPipeline.ts
🧬 Code graph analysis (1)
apps/restate/src/audioPipeline.ts (2)
apps/restate/src/services/rate-limit.ts (1)
  • limiter (47-49)
apps/restate/src/supabase.ts (2)
  • createSignedUrl (23-58)
  • deleteFile (60-81)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Redirect rules - hyprnote
  • GitHub Check: Header rules - hyprnote
  • GitHub Check: Pages changed - hyprnote
  • GitHub Check: fmt
  • GitHub Check: ci (macos, macos-14)
🔇 Additional comments (7)
apps/web/content/articles/durable-meeting-summary.mdx (2)

15-52: Architecture diagram accurately reflects new service changes.

The mermaid diagram successfully captures the updated flow:

  • Line 20: RateLimiter participant correctly represents the new generic rate-limiting service
  • Lines 29-36: Rate-limit check flow with error/success paths is clear
  • Line 50: Response payload correctly shows only { status: DONE, transcript } (LLM steps removed)

This aligns well with the new services introduced in this PR (rate-limit.ts and stt-file.ts).


1-8: Document is unpublished with incomplete sections—clarify publication timeline.

The article is marked published: false and contains multiple empty section headers (TL;DR, The Problem, Pipeline States, Deepgram Callback Pattern, Error Handling). Clarify whether this is a placeholder for a follow-up PR or if content should be added before merging.

If this article is meant to ship with this PR, consider either:

  1. Adding placeholder text to each section to signal WIP, or
  2. Merging this as unpublished and opening a follow-up issue to complete the sections.
apps/restate/src/audioPipeline.ts (5)

1-50: Schema definitions look well-structured.

The DeepgramCallback schema correctly handles both the batch (results.channels[].alternatives[]) and streaming (channel.alternatives[]) callback formats from Deepgram. The optional chaining in the schema aligns with the extraction logic in onDeepgramResult.


52-69: Clean transcription helper function.

The function correctly uses Deepgram's callback-based transcription API and provides clear error messages prefixed with "Deepgram:" for easier debugging.


86-89: Rate limiting integration looks correct.

The limiter(ctx, req.userId) pattern correctly uses the new rate-limit service to enforce per-user throttling. The hardcoded values (60s window, 5 max) align with the PR's stated design.


122-138: Good idempotency handling in callback.

The early return when existing !== undefined correctly prevents duplicate processing if the callback is delivered multiple times. The transcript extraction handles both Deepgram response formats with appropriate fallback to empty string.


140-154: Status handler implementation is correct.

The handler properly retrieves all relevant state and returns a consistent StatusStateType object with appropriate defaults.

@argos-ci
Copy link

argos-ci bot commented Nov 30, 2025

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
web (Inspect) ⚠️ Changes detected (Review) 3 changed Nov 30, 2025, 4:31 AM

@yujonglee yujonglee merged commit fedec7f into main Nov 30, 2025
13 of 14 checks passed
@yujonglee yujonglee deleted the devin/1764474030-restate-services branch November 30, 2025 04:51
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