Skip to content

Add world.snapshots storage interface (local, postgres, vercel) - #3250

Open
TooTallNate wants to merge 2 commits into
quickjs-vm-perffrom
quickjs-vm-snapshots
Open

Add world.snapshots storage interface (local, postgres, vercel)#3250
TooTallNate wants to merge 2 commits into
quickjs-vm-perffrom
quickjs-vm-snapshots

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Note

Supersedes #3050, which GitHub auto-marked as merged (and auto-deleted the head branch) when a restacking mistake briefly force-pushed the head branch to the same commit as its base. Same content, freshly rebased on the stack.

Stacked PR — based on #3049 (quickjs-vm-perf), which is based on #3048 (quickjs-vm). Review only the top commit here until the bases merge.

Summary

PR 3 of the QuickJS VM roadmap: the world.snapshots storage interface and its implementations, ported from the original snapshot-runtime branch (#1300). Inert until PR 4 — nothing in the runtime calls these APIs yet.

What's included

  • @workflow/world: Storage.snapshots interface — save(runId, data, metadata) / load(runId) / delete(runId) — plus SnapshotMetadata (eventsCursor, createdAt). The cursor is storage-layer metadata stored alongside the opaque snapshot bytes so a restore only fetches events recorded after the snapshot.
  • @workflow/world-local: filesystem storage under {basedir}/snapshots/ ({runId}.bin + {runId}.json sidecar), with round-trip unit tests.
  • @workflow/world-postgres: workflow_snapshots table (migration 0016, renumbered from the original branch to follow main's 00120015).
  • @workflow/world-vercel: client for the workflow-server snapshot endpoints (PUT/GET/DELETE /v2/runs/:runId/snapshot, binary body + metadata headers — endpoints are live in workflow-server since vercel/workflow-server#339). Includes:
    • the undici.request() (not fetch()) PUT path so Buffer bodies survive RetryAgent retries (Retry Handler Fails When Request Has a Body nodejs/undici#3288),
    • explicit W3C trace-context injection on all three request paths (they route around makeRequest, per the repo's trace-propagation rule), covered by new trace-propagation.test.ts cases.

Notes

  • Bytes are transported opaquely: compression/encryption is @workflow/core's responsibility (PR 4's save pipeline); worlds must not add their own compression (ciphertext doesn't compress).
  • Adding a required member to Storage is a breaking change for community World implementations — appropriate for the v5 beta line; called out in the changeset.
  • The always-on WORLD_SNAPSHOT_DIAG warn logging from the original branch was demoted to console.debug.

Copilot AI review requested due to automatic review settings July 31, 2026 03:21
@TooTallNate
TooTallNate requested review from a team and ijjk as code owners July 31, 2026 03:21
@changeset-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 968cfa1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@workflow/world Minor
@workflow/world-local Minor
@workflow/world-postgres Minor
@workflow/world-vercel Minor
@workflow/cli Patch
@workflow/core Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/world-testing Patch
workflow Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

⚠️ Results below are stale and not from the latest commit. This comment will be updated when CI completes on the latest run.

Tests are running...


_Started at: _


Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (3 failed)

hono-quickjs (1 failed):

nuxt-quickjs (1 failed):

vite-quickjs (1 failed):

📦 Local Production (1 failed)

nextjs-webpack-stable-node (1 failed):

  • webhookWorkflow | wrun_41KYV383090GZ8K7BS48QK6596

📋 Other (1 failed)

e2e-vercel-prod-tanstack-start-quickjs (1 failed):

  • sleepWinsRaceWorkflow | wrun_41KYV39JDS0GV8S6C91VSNVH4W

E2E Test Summary

Summary
Passed Failed Skipped Total
❌ ▲ Vercel Production 2907 3 478 3388
✅ 💻 Local Development 3088 0 454 3542
❌ 📦 Local Production 3241 1 454 3696
✅ 🐘 Local Postgres 3088 0 454 3542
✅ 🪟 Windows 308 0 0 308
❌ 📋 Other 2039 1 424 2464
✅ vercel-multi-region 27 0 0 27
Total 14698 5 2264 16967
Details by Category

❌ ▲ Vercel Production

App Passed Failed Skipped
✅ astro-node 126 0 28
✅ astro-quickjs 126 0 28
✅ example-node 126 0 28
✅ example-quickjs 126 0 28
✅ express-node 126 0 28
✅ express-quickjs 126 0 28
✅ fastify-node 126 0 28
✅ fastify-quickjs 126 0 28
✅ hono-node 126 0 28
❌ hono-quickjs 125 1 28
✅ nextjs-turbopack-node 151 0 3
✅ nextjs-turbopack-quickjs 151 0 3
✅ nextjs-webpack-node 151 0 3
✅ nextjs-webpack-quickjs 151 0 3
✅ nitro-node 126 0 28
✅ nitro-quickjs 126 0 28
✅ nuxt-node 126 0 28
❌ nuxt-quickjs 125 1 28
✅ sveltekit-node 145 0 9
✅ sveltekit-quickjs 145 0 9
✅ vite-node 126 0 28
❌ vite-quickjs 125 1 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable-node 128 0 26
✅ astro-stable-quickjs 128 0 26
✅ express-stable-node 128 0 26
✅ express-stable-quickjs 128 0 26
✅ fastify-stable-node 128 0 26
✅ fastify-stable-quickjs 128 0 26
✅ hono-stable-node 128 0 26
✅ hono-stable-quickjs 128 0 26
✅ nextjs-turbopack-canary-node 135 0 19
✅ nextjs-turbopack-canary-quickjs 135 0 19
✅ nextjs-turbopack-stable-node 154 0 0
✅ nextjs-turbopack-stable-quickjs 154 0 0
✅ nextjs-webpack-canary-node 135 0 19
✅ nextjs-webpack-canary-quickjs 135 0 19
✅ nextjs-webpack-stable-quickjs 154 0 0
✅ nitro-stable-node 128 0 26
✅ nitro-stable-quickjs 128 0 26
✅ nuxt-stable-node 128 0 26
✅ nuxt-stable-quickjs 128 0 26
✅ sveltekit-stable-node 147 0 7
✅ sveltekit-stable-quickjs 147 0 7
✅ vite-stable-node 128 0 26
✅ vite-stable-quickjs 128 0 26

❌ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable-node 128 0 26
✅ astro-stable-quickjs 128 0 26
✅ express-stable-node 128 0 26
✅ express-stable-quickjs 128 0 26
✅ fastify-stable-node 128 0 26
✅ fastify-stable-quickjs 128 0 26
✅ hono-stable-node 128 0 26
✅ hono-stable-quickjs 128 0 26
✅ nextjs-turbopack-canary-node 135 0 19
✅ nextjs-turbopack-canary-quickjs 135 0 19
✅ nextjs-turbopack-stable-node 154 0 0
✅ nextjs-turbopack-stable-quickjs 154 0 0
✅ nextjs-webpack-canary-node 135 0 19
✅ nextjs-webpack-canary-quickjs 135 0 19
❌ nextjs-webpack-stable-node 153 1 0
✅ nextjs-webpack-stable-quickjs 154 0 0
✅ nitro-stable-node 128 0 26
✅ nitro-stable-quickjs 128 0 26
✅ nuxt-stable-node 128 0 26
✅ nuxt-stable-quickjs 128 0 26
✅ sveltekit-stable-node 147 0 7
✅ sveltekit-stable-quickjs 147 0 7
✅ vite-stable-node 128 0 26
✅ vite-stable-quickjs 128 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable-node 128 0 26
✅ astro-stable-quickjs 128 0 26
✅ express-stable-node 128 0 26
✅ express-stable-quickjs 128 0 26
✅ fastify-stable-node 128 0 26
✅ fastify-stable-quickjs 128 0 26
✅ hono-stable-node 128 0 26
✅ hono-stable-quickjs 128 0 26
✅ nextjs-turbopack-canary-node 135 0 19
✅ nextjs-turbopack-canary-quickjs 135 0 19
✅ nextjs-turbopack-stable-node 154 0 0
✅ nextjs-webpack-canary-node 135 0 19
✅ nextjs-webpack-canary-quickjs 135 0 19
✅ nextjs-webpack-stable-node 154 0 0
✅ nextjs-webpack-stable-quickjs 154 0 0
✅ nitro-stable-node 128 0 26
✅ nitro-stable-quickjs 128 0 26
✅ nuxt-stable-node 128 0 26
✅ nuxt-stable-quickjs 128 0 26
✅ sveltekit-stable-node 147 0 7
✅ sveltekit-stable-quickjs 147 0 7
✅ vite-stable-node 128 0 26
✅ vite-stable-quickjs 128 0 26

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack-node 154 0 0
✅ nextjs-turbopack-quickjs 154 0 0

❌ 📋 Other

App Passed Failed Skipped
✅ e2e-local-dev-nest-stable-node 128 0 26
✅ e2e-local-dev-nest-stable-quickjs 128 0 26
✅ e2e-local-dev-tanstack-start-node 128 0 26
✅ e2e-local-dev-tanstack-start-quickjs 128 0 26
✅ e2e-local-postgres-nest-stable-node 128 0 26
✅ e2e-local-postgres-nest-stable-quickjs 128 0 26
✅ e2e-local-postgres-tanstack-start-node 128 0 26
✅ e2e-local-postgres-tanstack-start-quickjs 128 0 26
✅ e2e-local-prod-nest-stable-node 128 0 26
✅ e2e-local-prod-nest-stable-quickjs 128 0 26
✅ e2e-local-prod-tanstack-start-node 128 0 26
✅ e2e-local-prod-tanstack-start-quickjs 128 0 26
✅ e2e-vercel-prod-nest-node 126 0 28
✅ e2e-vercel-prod-nest-quickjs 126 0 28
✅ e2e-vercel-prod-tanstack-start-node 126 0 28
❌ e2e-vercel-prod-tanstack-start-quickjs 125 1 28

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview Jul 31, 2026 11:13pm
example-nextjs-workflow-webpack Ready Ready Preview Jul 31, 2026 11:13pm
example-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-astro-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-express-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-fastify-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-hono-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-nestjs-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-nitro-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-nuxt-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-sveltekit-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-tanstack-start-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workbench-vite-workflow Ready Ready Preview Jul 31, 2026 11:13pm
workflow-docs Ready Ready Preview, v0 Jul 31, 2026 11:13pm
workflow-swc-playground Ready Ready Preview Jul 31, 2026 11:13pm
workflow-tarballs Ready Ready Preview Jul 31, 2026 11:13pm
workflow-web Ready Ready Preview Jul 31, 2026 11:13pm

Comment thread packages/world-local/src/storage/snapshots-storage.ts
…torage

The security bot flagged path traversal: an unvalidated runId was
interpolated directly into {basedir}/snapshots paths, letting ../ (or /,
\, NUL, absolute fragments) escape the snapshots dir on save/load/delete.
Apply the same two-layer scheme as the other world-local storages:
assertSafeEntityId as the primary defense plus resolveWithinBase
containment at the join. Hostile-runId test covers all three operations.
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 968cfa1 failed. See the run logs for details.

Partial results from the failed run:

commit 968cfa1 · Fri, 31 Jul 2026 23:24:59 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
STSO 1020 steps (inline) 166 (+29%) 🔻 452 (±0%) 503 (-1.0%) 693 (-3.3%) 1016
STSO 1020 steps (queue-hop) 2271 (+52%) 🔻 3077 (-2.5%) 3077 (-2.5%) 3077 (-2.5%) 3
WO 1020 steps 380806 (-2.4%) 380806 (-2.4%) 380806 (-2.4%) 380806 (-2.4%) 1
SO stream overhead (structured) 103 (+1.0%) 157 (-17%) 💚 174 (-26%) 💚 249 (-62%) 💚 30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 382767ms → this run 371563ms (Δ -11204ms, -3%)

  100-150 ms  ┃                         main   3  this   0    -3
  150-200 ms  █████┃                    main  34  this  48   +14
  200-250 ms  ███████████████┃          main 122  this 121    -1
  250-300 ms  █████████████████████┃    main 153  this 162    +9
  300-350 ms  ██████████████████┃       main 134  this 140    +6
  350-400 ms  █████████████████░░░░░░┃  main 124  this 178   +54
  400-450 ms  █████████████┃█████████   main 172  this 106   -66
  450-500 ms  ███████████████████┃█     main 153  this 151    -2
  500-550 ms  ████████┃                 main  57  this  65    +8
  550-600 ms  ██┃██                     main  37  this  22   -15
  600-650 ms  ┃                         main   7  this   6    -1
  650-700 ms  ┃                         main   7  this   7    +0
  700-750 ms  ┃                         main   5  this   5    +0
  750-800 ms  ┃                         main   1  this   3    +2
  800-850 ms  ┃                         main   2  this   2    +0
  900-950 ms  ┃                         main   1  this   0    -1
 950-1000 ms  ┃                         main   1  this   0    -1
1000-1050 ms  ┃                         main   1  this   0    -1
1100-1150 ms  ┃                         main   1  this   0    -1
1250-1300 ms  ┃                         main   1  this   0    -1

1020 steps (queue-hop)

Cumulative STSO time: main 6857ms → this run 7908ms (Δ +1051ms, +15%)

1000-1500 ms  ┃███████████████████████  main 1  this 0  -1
2000-2500 ms  ███████████████████████┃  main 1  this 1  +0
2500-3000 ms  ░░░░░░░░░░░░░░░░░░░░░░░┃  main 0  this 1  +1
3000-3500 ms  ███████████████████████┃  main 1  this 1  +0
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — SO 250/500/1000

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the Best column shows the fastest (warm-start) sample for comparison.

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