Skip to content

[WIP] feat(service-worker): standalone service worker runtime#6022

Draft
gxkl wants to merge 5 commits into
feat/module-plugin-corefrom
feat/service-worker
Draft

[WIP] feat(service-worker): standalone service worker runtime#6022
gxkl wants to merge 5 commits into
feat/module-plugin-corefrom
feat/service-worker

Conversation

@gxkl

@gxkl gxkl commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Open-sources the standalone service worker runtime on top of the module-plugin mechanism (#6021):

  • @eggjs/controller-plugin becomes a dual-host module (the aop/dal/config pattern): it declares eggModule and ships the declarative controller hook set for BOTH hosts; the host-agnostic controller runtime lives inside it under lib/runtime/ (the interim @eggjs/controller-runtime package was absorbed). Egg keeps only imperative boot infra + transport creators (via the per-app ControllerRegisterDefaults queue); app.rootProtoManager/controllerRegisterFactory are backfilled from the container at didLoad.
  • @eggjs/service-worker-runtime — protocol-agnostic runner: @Runner() entry, event dispatch, BackgroundTaskHelper, logger/config inner-object contract
  • @eggjs/service-worker — fetch adapter + HTTP server facade (ServiceWorkerApp): fetch transport providers, streaming/SSE lifecycle, unified errors, MCP stateless streamable HTTP transport (SDK >= 1.29) with an auth extension point
  • multi-app isolation regression coverage, runnable example under examples/helloworld-service-worker, package READMEs and wiki page

Also fixes a real injection-proxy bug exposed by the dual-host work: the inject proxy binds function-valued properties and a bound class loses its statics — egg HTTP/MCP registers now import EggContainerFactory directly instead of reading it off the app.

Status

Rebased onto the current feat/module-plugin-core; ServiceWorkerApp uses the tegg#325-shaped StandaloneApp API. Marked WIP pending review.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b486102-c53e-4006-b520-4b7ef70aa775

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/service-worker

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 6, 2026

Copy link
Copy Markdown

Deploying egg with  Cloudflare Pages  Cloudflare Pages

Latest commit: 27cb9ef
Status: ✅  Deploy successful!
Preview URL: https://b1f3af06.egg-cci.pages.dev
Branch Preview URL: https://feat-service-worker.egg-cci.pages.dev

View logs

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces standalone service worker support for @eggjs/tegg modules, allowing HTTP controllers and MCP tools to be served over a fetch event loop without requiring a full Egg application. It extracts the host-agnostic controller registration runtime into @eggjs/controller-runtime and adds the @eggjs/service-worker-runtime and @eggjs/service-worker packages, along with a runnable example. The review feedback suggests improving exception handling in FetchEventHandler by checking if caught errors are instances of Error before accessing their message, and expanding ResponseUtils to correctly handle and pass through standard web-standard BodyInit types (such as Uint8Array and Blob) instead of incorrectly serializing them as JSON.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

return await this.#guardResponseStream(response);
} catch (e: any) {
console.error('[service-worker] handle fetch event failed:', e);
return ResponseUtils.createErrorResponse(500, 'INTERNAL_SERVER_ERROR', e?.message ?? 'internal error');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

When handling a caught exception, check if the caught value is an Error instance before accessing its message property. Use String(e) as a fallback for non-Error values to ensure a meaningful error message is returned in the response.

Suggested change
return ResponseUtils.createErrorResponse(500, 'INTERNAL_SERVER_ERROR', e?.message ?? 'internal error');
return ResponseUtils.createErrorResponse(500, 'INTERNAL_SERVER_ERROR', e instanceof Error ? e.message : String(e));
References
  1. When creating a new Error from a caught exception, check if the caught value is an Error instance before accessing its message property. Use String(err) as a fallback for non-Error values to ensure a meaningful error message.

Comment on lines +20 to +22
if (Buffer.isBuffer(body) || typeof body === 'string' || body instanceof ReadableStream) {
return new Response(body as BodyInit, { status: 200 });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

In a fetch-based service worker runtime, controllers may return standard web-standard body types such as Uint8Array, ArrayBuffer, Blob, URLSearchParams, or FormData. Currently, these types are not checked and will fall through to JSON.stringify(body), which will incorrectly serialize them. We should explicitly check for these standard BodyInit types to pass them directly to the Response constructor.

Suggested change
if (Buffer.isBuffer(body) || typeof body === 'string' || body instanceof ReadableStream) {
return new Response(body as BodyInit, { status: 200 });
}
if (Buffer.isBuffer(body) || typeof body === 'string' || body instanceof ReadableStream || body instanceof Uint8Array || body instanceof ArrayBuffer || body instanceof Blob || body instanceof URLSearchParams || body instanceof FormData) {
return new Response(body as BodyInit, { status: 200 });
}

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 6, 2026

Copy link
Copy Markdown

Deploying egg-v3 with  Cloudflare Pages  Cloudflare Pages

Latest commit: 27cb9ef
Status: ✅  Deploy successful!
Preview URL: https://dbb0a054.egg-v3.pages.dev
Branch Preview URL: https://feat-service-worker.egg-v3.pages.dev

View logs

@gxkl gxkl force-pushed the feat/service-worker branch from 544b347 to 501a495 Compare July 6, 2026 00:25
@socket-security

socket-security Bot commented Jul 6, 2026

Copy link
Copy Markdown

Dependency limit exceeded — report not shown.

This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report.

Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard.

Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account.

@gxkl gxkl force-pushed the feat/module-plugin-core branch from 5001fb6 to 79594a6 Compare July 6, 2026 01:39
@gxkl gxkl force-pushed the feat/service-worker branch 4 times, most recently from bcb170d to 79d6bc3 Compare July 6, 2026 10:07
@gxkl gxkl force-pushed the feat/module-plugin-core branch 8 times, most recently from b774e07 to 5c42a6d Compare July 9, 2026 15:46
gxkl and others added 5 commits July 10, 2026 11:42
Following the aop/dal/config precedent, the controller plugin declares
eggModule and serves BOTH hosts from one package, three layers inside:

- the host-agnostic controller runtime stays at its original paths
  (hooks, register factory + per-app ControllerRegisterDefaults queue,
  root proto manager, HTTP register/method-register base skeletons,
  MCPServerHelper), re-exported from the package entry for the
  standalone host;
- lib/ControllerModule.ts: the declarative dual-host module surface —
  rootProtoManager/controllerRegisterFactory inner objects plus the
  load-unit and prototype lifecycle hooks (constructor injection; egg
  imports type-only so the scan stays host-safe);
- app.ts: egg-only imperative boot — loader/creator infra, transport
  creators enqueued through ControllerRegisterDefaults and drained by
  the module factory proto, app.rootProtoManager/controllerRegisterFactory
  backfilled from the container at didLoad.

ModuleHandler provides the app as a PRIVATE eggApp inner object (the
egg compat surface owns the plain app name) so the shared factory can
carry the host. Also fixes an injection-proxy bug this exposed: the
proxy binds function-valued properties and a bound class loses its
statics — egg HTTP/MCP registers import EggContainerFactory directly
instead of reading it off the app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Open-sources the standalone service worker on the module-plugin
mechanism:

- @eggjs/service-worker-runtime — protocol-agnostic runner: @runner()
  entry, @EventHandlerProto dispatch, BackgroundTaskHelper, the
  `event` context proto, logger/config inner-object contract;
- @eggjs/service-worker — fetch adapter + HTTP server facade
  (ServiceWorkerApp on the StandaloneAppInit/init(opts) split): fetch
  transport providers over the controller plugin's dual-host module,
  streaming/SSE lifecycle with unified errors, MCP stateless streamable
  HTTP transport (SDK >= 1.29, per-request web-standard transport) with
  an auth extension point, multi-app isolation regression coverage;
- a runnable example under examples/helloworld-service-worker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The MCP controller register mixed record collection with egg-specific
transport (routes on app.router, ctxStorage, middleware, SSE/stream
state), and the egg host threaded its Application into the module
inner-object DI graph as a PRIVATE `eggApp` provided object so the
register could reach it — a host-boundary leak that undermines the
host-agnostic module-plugin goal.

Extract a `McpRouter` transport boundary (controller-core): the shared
`MCPControllerRegister` now only COLLECTS tool/resource/prompt records
and delegates transport to an injected router via `registerServer()`.
The egg node-HTTP transport moves to `EggMcpRouter` (constructed
imperatively in app.ts, which already holds `app`), and the service
worker fetch transport to `ServiceWorkerMcpRouter`; both provide the
`mcpRouter` DI name (the two host plugins are never used together, so no
qualifier is needed). MCPServerHelper was already host-agnostic and is
unchanged.

With the register no longer needing `app`, the `eggApp` provided inner
object is removed from ModuleHandler and EggControllerRegisterFactory
drops its host generic/injection — Egg Application leaves the DI graph.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@gxkl gxkl force-pushed the feat/service-worker branch from 79d6bc3 to 27cb9ef Compare July 10, 2026 05:18
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