[WIP] feat(service-worker): standalone service worker runtime#6022
[WIP] feat(service-worker): standalone service worker runtime#6022gxkl wants to merge 5 commits into
Conversation
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Deploying egg with
|
| 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 |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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.
| 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
- 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.
| if (Buffer.isBuffer(body) || typeof body === 'string' || body instanceof ReadableStream) { | ||
| return new Response(body as BodyInit, { status: 200 }); | ||
| } |
There was a problem hiding this comment.
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.
| 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 }); | |
| } |
Deploying egg-v3 with
|
| 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 |
|
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. |
5001fb6 to
79594a6
Compare
bcb170d to
79d6bc3
Compare
b774e07 to
5c42a6d
Compare
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>
79d6bc3 to
27cb9ef
Compare
Summary
Open-sources the standalone service worker runtime on top of the module-plugin mechanism (#6021):
@eggjs/controller-pluginbecomes a dual-host module (the aop/dal/config pattern): it declareseggModuleand ships the declarative controller hook set for BOTH hosts; the host-agnostic controller runtime lives inside it underlib/runtime/(the interim@eggjs/controller-runtimepackage was absorbed). Egg keeps only imperative boot infra + transport creators (via the per-appControllerRegisterDefaultsqueue);app.rootProtoManager/controllerRegisterFactoryare 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 pointexamples/helloworld-service-worker, package READMEs and wiki pageAlso 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
EggContainerFactorydirectly instead of reading it off the app.Status
Rebased onto the current
feat/module-plugin-core;ServiceWorkerAppuses the tegg#325-shaped StandaloneApp API. Marked WIP pending review.🤖 Generated with Claude Code