Skip to content

fix(console): stop retrying /api/v1/dev/metadata-events after the first 404 - #7266

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7257-dev-metadata-events-polling
Sep 2, 2026
Merged

hotlong merged 2 commits into
mainfrom
claude/issue-7257-dev-metadata-events-polling

Conversation

@hotlong

@hotlong hotlong commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #7257

File-surface note

The PM's claim comment scoped this to packages/app-shell/src/**. Verified against origin/main: nothing under packages/app-shell references metadata-events at all. The actual subscription is MetadataHmrReloader at apps/console/src/components/MetadataHmrReloader.tsx, mounted once from apps/console/src/App.tsx. This PR's diff is confined to that one component + its new test + a changeset — it does not touch packages/app-shell, nor #7253's marketplace files or #7254's studio/i18n files.

Root cause

MetadataHmrReloader is dev-only HMR tooling: an EventSource subscribed to /api/v1/dev/metadata-events, gated on enabled = import.meta.env.DEV. Its own error handler unconditionally scheduled a reconnect on a fixed reconnectDelayMs (2s) whenever readyState === CLOSED — with no distinction between "the stream dropped after being open" (worth retrying) and "the very first connection attempt failed" (a route that isn't mounted, i.e. production posture — retrying just recreates the same 404 forever, ~30/min while a record/list page stays open, drowning out the legitimate sys_inbox_message / sys_notification_receipt polling).

Separately, and worth recording even though it isn't fixed in this PR: enabled's import.meta.env.DEV gate is not airtight against every rig. Traced through Vite 8's resolveConfig (node_modules/vite/dist/node/chunks/node.js): for vite build, mode defaults to 'production' regardless of NODE_ENV, but import.meta.env.DEV/PROD are derived from a separate isProduction = process.env.NODE_ENV === 'production' check. scripts/dev-local/run-stack.sh --with-ui in the cloud repo builds the console with NODE_ENV=development forced ("vite/tsc misbehave under NODE_ENV=production" per its own comment) while the server itself runs --prod-like (NODE_ENV=production). That combination bakes import.meta.env.DEV === true into the shipped console bundle even though the server is in production posture — which is one concrete way enabled ends up true where it shouldn't. That's a cloud-repo rig concern, out of scope here; this PR's fix is deliberately layered underneath the enabled gate so the component is safe regardless of why it ended up enabled.

Fix

The first connect() attempt now doubles as the real capability probe (apps/console/src/components/MetadataHmrReloader.tsx): a hasOpenedOnce flag is set on the stream's open event. If the connection closes (or throws) before ever reaching open, the component gives up permanently — no retry, and specifically not a longer backoff interval either, per the fix requirement. A stream that opened at least once and later drops (dev server restart, network blip) keeps reconnecting exactly as before, since that proves the route genuinely exists.

Alternative channel for production metadata refresh — none needed

Per the ruling, no replacement channel is implemented in this card. Explaining why none is needed: MetadataHmrReloader's only job is dev-time "a metadata file changed on disk" → full reload. Production deployments never have metadata files changing on the server's disk after a publish — there is no equivalent event for a production channel to watch. The referenced "Studio left nav doesn't refresh" issue is a separate, unrelated caching bug (Studio has its own useMetadataHmr + subscribe(...) invalidation layer per this file's docstring) and does not go through this SSE stream either way — it needs its own fix, tracked separately.

Tests

apps/console/src/components/MetadataHmrReloader.test.tsx (new), against a hand-rolled FakeEventSource (this repo's happy-dom test env has no real EventSource):

  • THE FIX: a connection that closes before open (404-shaped) produces exactly one EventSource instance even after advancing fake timers 15x past reconnectDelayMs.
  • A second case pins "not a longer interval either" — 100x the interval, still zero further attempts.
  • CONTROL: a stream that opens once and then drops keeps reconnecting (2 more instances across 2 drops) — proves the fix doesn't regress genuine dev-time reconnect.
  • CONTROL: enabled={false} subscribes to nothing (unaffected by this change).

Reverse-verified: reverted MetadataHmrReloader.tsx to origin/main (git diff confirmed byte-identical), re-ran the new test file — the two "give up" tests went red (2 EventSource instances instead of 1) while both controls stayed green; restored the fix (git checkout HEAD --, git diff HEAD empty) and confirmed green again.

pnpm exec vitest run apps/console/src/components/MetadataHmrReloader.test.tsx
 Test Files  1 passed (1)
      Tests  4 passed (4)

pnpm exec vitest run apps/console/                 # full package, unaffected suites
 Test Files  86 passed (86)
      Tests  952 passed (952)

pnpm --filter @object-ui/console type-check        # clean, no errors
pnpm --filter @object-ui/console build              # exit 0
node scripts/check-changeset-presence.mjs           # ✅ 1 changeset declared
node scripts/check-changeset-no-major.mjs           # ✅
node scripts/check-control-bytes.mjs                # ✅

All at a32a727bd.

Changeset

.changeset/7257-metadata-hmr-give-up-after-404.md — @object-ui/console patch.

Generated by Claude Code

Companion to the previous commit — declares the @object-ui/console patch
release note for objectui#7257.
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 48 chunks) 3160.2 KB 3191.4 KB
Main entry chunk (gzip) 142.6 KB 350 KB
Entry file index-BSr_FEpG.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.33KB 5.59KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 514.46KB 117.29KB
core (index.js) 5.55KB 2.23KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 178.20KB 49.60KB
fields (index.js) 244.25KB 61.73KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.98KB 10.98KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.92KB 12.93KB
plugin-charts (index.js) 70.02KB 19.44KB
plugin-chatbot (index.js) 190.53KB 45.18KB
plugin-dashboard (index.js) 132.63KB 34.56KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 250.65KB 63.91KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.78KB 32.58KB
plugin-gantt (index.js) 166.77KB 40.76KB
plugin-grid (index.js) 208.88KB 56.59KB
plugin-kanban (index.js) 53.21KB 14.66KB
plugin-list (index.js) 113.55KB 27.68KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 29.34KB 8.47KB
plugin-tree (index.js) 8.98KB 3.08KB
plugin-view (index.js) 85.90KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 3.11KB 1.48KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 09:28
@hotlong
hotlong added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit ef1ffc2 Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7257-dev-metadata-events-polling branch September 2, 2026 09:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

生产姿态下 console 持续轮询 GET /api/v1/dev/metadata-events → 404,浏览器控制台被刷红

1 participant