Skip to content

perf(vue): publish writeItems once - #64

Merged
Akryum merged 3 commits into
mainfrom
fix/vue-batch-writeitems-publication
Sep 10, 2026
Merged

Akryum merged 3 commits into
mainfrom
fix/vue-batch-writeitems-publication

Conversation

@hanneskuettner

@hanneskuettner hanneskuettner commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Description

Bulk cache writes now publish one reactive update per affected collection instead of notifying Vue dependents after every item. This removes repeated synchronous query recomputation while preserving immediate writeItem behavior, nested relation consistency, hook ordering, and retryable partial failures.

The PR now includes an RStore-native benchmark that exercises the real Vue cache runtime, public peekMany query API, a Vue computed, and a synchronous watcher:

pnpm --filter @rstore/vue benchmark:write-items

The optimized case calls the public cache.writeItems API. The control reproduces the parent commit's flat, active, non-staggered writeItems path with the real writeItemNow, including per-item reactive publication and one outer hook. Each mode gets two warmups and 20 measured samples in alternating order, with a new store and cache for every sample.

For 1,000 items, a clean-process run on Node v26.5.0 and an Apple M4 Pro produced:

Metric Legacy per-item control Atomic writeItems
Median 758.051 ms 1.624 ms
p95 864.371 ms 2.389 ms
MAD 32.439 ms 0.073 ms
Visible query transitions 1,000 1
Watcher notifications 3,000 1
Query recomputations 3,001 2
Outer hooks 1 1

Both paths produced the same final cache and query digest. This run measured a 466.864× median speedup. Timing is descriptive and machine-dependent; the benchmark fails only when the final semantics, hook payload, or publication/recomputation counts differ.

Recorded 1,000-item timing samples and source provenance
  • Source commit: e6633e291f0a1212f75d6d839b52b622e7cd4a90
  • Clean worktree: true
  • Benchmark source SHA-256: 41ebc39a12e7d48d526eff79b758e22ff884baf67fb8f905a8195bffafa4de10
  • Legacy raw milliseconds: [712.311, 707.645, 708.969, 752.247, 709.683, 846.453, 772.726, 732.693, 720.207, 733.25, 731.016, 770.027, 762.509, 781.253, 871.926, 838.172, 798.52, 753.594, 764.693, 864.371]
  • Atomic raw milliseconds: [1.557, 1.419, 1.508, 1.491, 1.582, 1.478, 1.628, 1.68, 1.843, 1.543, 1.74, 2.536, 1.619, 1.561, 1.673, 2.208, 2.389, 1.597, 1.642, 1.631]

The exact control is limited to the benchmarked flat, non-staggered case. It does not claim to reproduce legacy staggering, nested-relation settlement, or error handling; focused tests cover those production contracts.

This is an atomic reactive-publication boundary, not a rollback transaction. Imperative cache reads can still observe already-written raw state during a staggered batch. If a later item fails, successful earlier writes are published before the error is returned, and the failed item remains retryable.

The settlement boundary also guarantees that:

  • all affected parent, child, index, and layered collection readers publish before nested afterCacheWrite hooks run;
  • nested hooks keep insertion order and run before the successful outer batch hook;
  • the original item-write error remains the primary error when publication also fails;
  • a subscriber or hook error after completion cannot strand work already queued behind the batch.

Additional context

The public writeItems API and multi-result applyMutation path share one batch initializer. CacheWriteBatch owns the affected collection set and deferred nested-hook payloads, so batching policy stays in the cache repository instead of leaking into consumers.

Mutation testing removed batch propagation and produced 11 focused failures while immediate single-item publication stayed green. The final checks are:

  • pnpm --filter @rstore/vue benchmark:write-items — semantic and structural assertions passed
  • pnpm vitest run packages/vue/test — 17 files, 261 tests passed
  • pnpm --filter @rstore/vue test:types — passed, including the benchmark source
  • scoped ESLint over all changed Vue cache and benchmark files — passed
  • pnpm --filter @rstore/vue build — passed
  • pnpm install --lockfile-only --offline --frozen-lockfile — passed
  • git diff --check — passed

The local monorepo-wide build and test commands could not complete because this checkout is missing generated Nuxt configs and package-local binaries such as nuxt-module-build. The affected package checks above pass; CI should run the full matrix in a prepared checkout.

Reviewer focus: queue completion after subscriber/hook exceptions, partial-failure retry semantics, the benchmark control's explicit scope, and the distinction between reactive atomicity and rollback.

New concepts

Atomic reactive publication

The cache still performs each raw item mutation in order, but it delays Vue notifications until the batch settles. One batch object records which collections changed and which nested hooks must run later.

This is preferable to disabling each consumer watcher because the cache owns the publication contract. It also covers public writes, mutation results, relations, indexes, and layers through one seam.

Do not use this pattern when intermediate states are part of the public contract. It is also not a substitute for transactional rollback because successful raw writes remain applied after a later failure.

Post-Deploy Monitoring & Validation

  • Healthy signal: bulk consumer ingestion no longer creates a multi-second synchronous task, and collection readers observe one final publication per batch.
  • Failure signals: stale relation/index reads, missing or duplicate afterCacheWrite hooks, or queued writes that stop progressing after a subscriber error.
  • Validation window: the first package preview/release and its first Directus Studio consumer run.
  • Owner: PR author with RStore maintainers.
  • Rollback: revert this PR or pin consumers to the previous @rstore/vue release.

What is the purpose of this pull request?

  • Bug fix
  • New Feature
  • Documentation update
  • Other

Before submitting the PR, please make sure you do the following

  • If it's a new feature, provide a convincing reason to add it. Ideally, you should open a suggestion issue first and have it approved before working on it.
  • Read the Contributing Guidelines.
  • Read the Pull Request Guidelines and follow the Commit Convention.
  • Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
  • Provide a description in this PR that addresses what the PR is solving, or reference the issue that it solves (e.g. fixes #123).
  • Ideally, include relevant tests that fail without this PR but pass with it.

Compound Engineering

@pkg-pr-new

pkg-pr-new Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@rstore/connector-toolkit

npm i https://pkg.pr.new/directus/rstore/@rstore/connector-toolkit@64

@rstore/core

npm i https://pkg.pr.new/directus/rstore/@rstore/core@64

@rstore/devtools

npm i https://pkg.pr.new/directus/rstore/@rstore/devtools@64

@rstore/directus

npm i https://pkg.pr.new/directus/rstore/@rstore/directus@64

@rstore/monospace

npm i https://pkg.pr.new/directus/rstore/@rstore/monospace@64

@rstore/nuxt

npm i https://pkg.pr.new/directus/rstore/@rstore/nuxt@64

@rstore/nuxt-directus

npm i https://pkg.pr.new/directus/rstore/@rstore/nuxt-directus@64

@rstore/nuxt-drizzle

npm i https://pkg.pr.new/directus/rstore/@rstore/nuxt-drizzle@64

@rstore/nuxt-monospace

npm i https://pkg.pr.new/directus/rstore/@rstore/nuxt-monospace@64

@rstore/nuxt-multiplayer

npm i https://pkg.pr.new/directus/rstore/@rstore/nuxt-multiplayer@64

@rstore/nuxt-multiplayer-server

npm i https://pkg.pr.new/directus/rstore/@rstore/nuxt-multiplayer-server@64

@rstore/offline

npm i https://pkg.pr.new/directus/rstore/@rstore/offline@64

@rstore/shared

npm i https://pkg.pr.new/directus/rstore/@rstore/shared@64

@rstore/vite-directus

npm i https://pkg.pr.new/directus/rstore/@rstore/vite-directus@64

@rstore/vite-monospace

npm i https://pkg.pr.new/directus/rstore/@rstore/vite-monospace@64

@rstore/vue

npm i https://pkg.pr.new/directus/rstore/@rstore/vue@64

commit: 72881e4

@hanneskuettner
hanneskuettner requested a review from Akryum August 28, 2026 11:17
@Akryum
Akryum force-pushed the fix/vue-batch-writeitems-publication branch from e6633e2 to 9ef9985 Compare September 9, 2026 12:01
@Akryum
Akryum force-pushed the fix/vue-batch-writeitems-publication branch from 9ef9985 to 06d9791 Compare September 9, 2026 16:27
Avoid notifying Vue dependents after every item in a bulk cache write. Publish affected collections atomically, defer nested hooks until settlement, and preserve queue progress and error context across partial failures.
@Akryum
Akryum force-pushed the fix/vue-batch-writeitems-publication branch from 06d9791 to b7eb76f Compare September 9, 2026 17:20
@Akryum
Akryum force-pushed the fix/vue-batch-writeitems-publication branch from b7eb76f to eed258a Compare September 9, 2026 17:25
@Akryum
Akryum merged commit 35a7061 into main Sep 10, 2026
22 checks passed
@Akryum
Akryum deleted the fix/vue-batch-writeitems-publication branch September 10, 2026 08:54
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.

2 participants