feat(service-worker): standalone service worker runtime#6022
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a host-agnostic controller runtime, refactors Egg controller registration, and introduces standalone service-worker runtime and HTTP/MCP transports. It also adds ChangesStandalone service-worker controller
Estimated code review effort: 5 (Critical) | ~120 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ 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: |
6fd52da
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5d83a7ba.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.
Deploying egg-v3 with
|
| Latest commit: |
6fd52da
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://55d33ef0.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
79d6bc3 to
27cb9ef
Compare
85a0476 to
ad9acf9
Compare
9434e0b to
078a2d9
Compare
Add a service-worker-format demo (worker-sw.ts + bundle-sw.mjs + run-sw.mjs) for
StandaloneWorkerBundler's `format: 'service-worker'` path: the entry uses
`addEventListener('fetch', ...)` and the bundler emits the CJS bundle as a classic
(non-module) script — no ESM wrapper, since a module wrapper would move the listener
to module scope where fetch events are not dispatched.
Verified under Node by shimming globalThis.addEventListener around the bundled
classic script (GET /hello + POST /mcp/calc/stream both 200). This format targets
Web Service Worker / edge runtimes, NOT Cloudflare workerd: workerd's nodejs_compat
(required for tegg's AsyncLocalStorage) only supports the module-worker format, and
wrangler rejects a service-worker-format script importing Node builtins. Documented
in the example README and egg-bundler wiki.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inline #createModuleLoader so the manifest descriptor is found once per module (was found twice — once for precomputedFiles, once for the module name), and take the bundle-mode unitName from moduleConfig.name (ModuleReference already carries it) instead of a second manifest.moduleDescriptors.find. Matches the egg plugin's EggModuleLoader, which passes moduleConfig.name gated on precomputed files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…er.load Pre-build a Map<unitPath, decoratedFiles> once before the load loop instead of a per-module manifest.moduleDescriptors.find (O(n) -> O(1) per module), mirroring the egg plugin's decoratedFilesMap. Behavior unchanged: an empty decoratedFiles array is still truthy, so a module present in the manifest still takes the bundle branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Condense the injection-seam comments to the non-obvious rationale only — drop comments that restate the code and merge duplicated bundle-mode explanations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A loader created on the dynamic path (ModuleLoader.createModuleLoader, e.g. DAL's
multiInstance getObjects) globbed and loaded non-decorated files (an egg plugin's
app.ts), which aren't in the bundle map — the fallback dynamic require then failed
("Cannot find module as expression is too dynamic"). Now createModuleLoader reuses
the manifest's decorated files from globalThis.__EGG_BUNDLE_MANIFEST__ in bundle
mode, mirroring how the egg app bundle stays loadable; non-bundle runs are unchanged
(no global -> globbing as before). This lets a standalone service-worker bundle
include teggDal without excluding it.
Also: DataSource.getObjects returns early when a module has no dataSource config
(was loading every scanned module first); egg-bundler importMetaPatch exported
consts get explicit types and ServiceWorkerApp.loadMetadata an explicit return type
(isolatedDeclarations build); drop the unused @eggjs/service-worker-runtime dep from
the controller and ignore the scan-only @eggjs/service-worker-controller dep on the
host (unplugin-unused).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`egg-bin bundle` now bundles a standalone service worker in addition to an egg app. A standalone bundle is selected by --entry (or --target standalone); --framework names the app package, which must export loadMetadata (the standalone counterpart of the egg app's framework specifier — no separate app-module/app-export). The command runs loadMetadata + StandaloneWorkerBundler in one shot, mirroring how `egg-bin bundle` hides the egg app's manifest-load-then-bundle. @eggjs/service-worker gains a top-level loadMetadata export. The example drops the hand-written bundle-cf.mjs/bundle-sw.mjs for `egg-bin bundle` scripts and no longer excludes teggDal (the bundle-mode dynamic-loader fix makes it work). Verified: both module and service-worker formats build via the CLI and run on Node + workerd. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the bundle-mode dynamic-module-loading fix (createModuleLoader reuses the manifest in bundle mode, aligning standalone with the egg app bundle), teggDal no longer needing excludeModules, and the egg-bin bundle standalone target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- js/stack-trace-exposure (FetchEventHandler): a 500 replied the raw thrown error message to the client. Reply a generic 'Internal Server Error' instead; the real error is still logged server-side and a host can surface details via errorResponseMapper. - js/polynomial-redos (importMetaPatch): the Turbopack import.meta object regex used `[\s\S]*?` for the getter body, which backtracks polynomially over a large emitted bundle. The body is a single brace-free `return …F(…)`, so `[^}]*` keeps it linear. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The polynomial backtracking was in the identifier match, not the getter body: the embedded `import$2e$meta__` literal is all word chars, so the surrounding `[\w$]*` backtracked over it on a large bundle. Match the identifier as a plain `[\w$]*` and move the import.meta marker check into the replace callback — the regex is now linear. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Adds a standalone service worker runtime — an egg-free host that serves HTTP controllers and MCP tools from tegg modules, built on the module-plugin mechanism (#6021). Getting there refactored the controller stack into a host-agnostic core shared by two hosts (egg + fetch/service-worker), so the same declarative
@HTTPController/@MCPControllermodules run in both.Architecture
Four-package controller layering (mirrors the egg/standalone host boundary):
@eggjs/controller-runtime— host-agnostic library, never scanned (noeggdep): base register classes,RootProtoManager,ControllerRegisterFactory, the controller lifecycle protos,McpRouter/Routerabstractions, collect-onlyMCPControllerRegister,MCPServerHelper.@eggjs/controller-plugin(teggController) — the egg host transport.@eggjs/service-worker-controller(serviceWorkermodule) — the fetch host transport.runtimeProtos.ts).Standalone service worker (three packages over
@eggjs/standalone):@eggjs/service-worker-runtime— protocol-agnostic@Runner()entry + event dispatch,BackgroundTaskHelper, the logger/config inner-object contract.@eggjs/service-worker-controller— fetch transport:FetchEventHandler, HTTP/MCP register providers,FetchRouter, streaming/SSE lifecycle, unified{ code, message }errors.@eggjs/service-worker— theServiceWorkerAppfacade:serve()(node:http bridge), embeddedhandleEvent(), and the raw Web Service Worker interface (self.addEventListener('fetch', e => e.respondWith(app.handleEvent(e)))). Framework modules are auto-discovered from the package's own deps — no hand-listed/ordered frameworkDeps.Controllers as container citizens
The dual-host work moved the controller registers into the DI graph so egg and the service worker share one path:
EggHTTPControllerRegistrarplugs the HTTP creator into the factory and mounts controllers ontoapp.routervia a@LoadUnitInstanceLifecycleProtohook when theCONTROLLER_LOAD_UNITinstance is created — no manual boot-timedoRegister.router/logger/ …) through the same() => app[name]compat protos business modules use (one PUBLIC copy, deduped); the imperativeControllerRegisterDefaultsbypass is deleted. egg and the service worker now differ only in theirRouter/McpRouterimplementations.McpRouterboundary (EggMcpRouter/ServiceWorkerMcpRouter); the sharedMCPControllerRegisteronly collects records. MCP stateless streamable HTTP uses the SDK's web-standard transport (SDK ≥ 1.29 forbids reuse — fresh server + transport per request) with anmcpAuthHandlerextension point.Notable fixes
StandaloneEggObjectFactoryname pinned toeggObjectFactory, soServiceWorkerRunner's inject resolves locally instead of the module-scan-order-sensitive global proto — this is what lets the service worker auto-discover its framework modules from its own package root.EggContainerFactorydirectly instead of reading it off the app.Testing
test/MultiApp.test.ts), the full HTTP/MCP suites for both hosts, and a runnableexamples/helloworld-service-worker(serve / embedded / fetch-event forms).Docs
Wiki updated:
wiki/packages/service-worker.md,wiki/concepts/tegg-module-plugin.md,wiki/workflows/local-ci.md, plus package READMEs.🤖 Generated with Claude Code
Summary by CodeRabbit
/hello+ calculatoraddtool).0.