Skip to content

fix(createPlugin): defer factory call to install time to prevent resource leaks#343

Merged
johnleider merged 2 commits into
vuetifyjs:masterfrom
sridhar-3009:fix/create-plugin-lazy-factory-338
Jun 23, 2026
Merged

fix(createPlugin): defer factory call to install time to prevent resource leaks#343
johnleider merged 2 commits into
vuetifyjs:masterfrom
sridhar-3009:fix/create-plugin-lazy-factory-338

Conversation

@sridhar-3009

Copy link
Copy Markdown
Contributor

Fixes #338

Problem

createXPlugin() (via createPluginContext) called the context factory at plugin-creation time — before app.use() ever runs:

// Before: factory runs here, at createXPlugin() call time
const [, provide, context] = createXContext({ ...options, namespace } as O)

This means:

  • A plugin created but never installed allocates live resources (matchMedia listeners, watchers, unhead entries) that are never torn down
  • Two plugin instances with the same namespace on one app: the second install is silently skipped, but its factory already ran and its resources are stranded
  • useMediaQuery's hydration-deferral contract silently breaks on the plugin path — the factory runs outside any injection context

Solution

Move factory invocation inside the provide callback so it runs at install time, not factory time:

// After: factory deferred to install time
let context!: E

return createPlugin({
  namespace,
  provide: app => {
    const [, provide, ctx] = createXContext({ ...options, namespace } as O)
    context = ctx
    provide(context, app)
    // ...restore...
  },
  setup: app => {
    // context is set by provide, which always runs first
    config?.setup?.(context, app, options)
    // ...persist watch...
  },
})

provide always runs before setup (enforced by createPlugin), so the definite assignment is safe. The INSTALLED guard ensures setup never runs without a prior provide call.

createXContext (the standalone path) is unchanged — standalone contexts still create eagerly, which is correct.

Test plan

  • 2 new tests verify factory is not called at createXPlugin() time
  • All existing persist/restore ordering tests pass
  • 6110 tests pass, typecheck and lint clean

@pkg-pr-new

pkg-pr-new Bot commented Jun 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

commit: d7e6f3c

…t cross-talk

The lazy factory runs once per install, so the same plugin object installed on
multiple apps gets a distinct context each time. The persist watch closed over
the shared `context` binding, so a later install reassigned the source out from
under earlier apps' watchers. Capture the context locally in setup so each app's
persist watch tracks its own instance.

Adds regression tests for the duplicate-namespace install (second factory must
not run) and the multi-app persist isolation case, and trims the inline comments.
@johnleider

Copy link
Copy Markdown
Member

Pushed a small follow-up directly to this branch as a maintainer edit (d7e6f3c), on top of your fix.

What it addresses

The lazy-factory move is correct. One edge it left open is in setup, which still read the shared outer context binding. Now that the factory runs once per install, the same plugin object installed on more than one app reassigns context on each install — and the persist watch source closes over that mutable binding lazily, so a later install pulls the source out from under an earlier app's watcher.

Concretely: with a single createXPlugin({ persist: true }) object used on two apps, mutating app A's state ends up persisting app B's value into app A's storage key.

Fix is one capture — const ctx = context at the top of setup, used for both the config.setup call and the persist watch — so each app's watcher tracks the instance that provide created for it. The adapter path was already safe (it receives context as a parameter, a fresh per-call binding); only the in-createPluginContext persist watch needed it.

Tests added

  • should not run a second factory when a duplicate namespace is installed — covers the issue's second scenario (a second same-namespace plugin object on one app must not run its factory).
  • should persist each app independently when one plugin object is reused — fails against the shared-closure version ('b-initial' leaks into app A's storage) and passes after the capture.

Also

Trimmed the two inline comment blocks down to the why.

Verified in packages/0: vitest 23/23 on createPlugin, vue-tsc --noEmit clean, ESLint clean on both files.

@johnleider johnleider added T: bug Something isn't working E: createPlugin labels Jun 23, 2026
@johnleider johnleider added this to the v1.0.0 milestone Jun 23, 2026
@johnleider johnleider merged commit e77e7b4 into vuetifyjs:master Jun 23, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

E: createPlugin T: bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(createPlugin): plugin contexts are created eagerly at factory time, stranding live resources

2 participants