feat(openapi): cache spec fetches (URL index + conditional GET)#1247
feat(openapi): cache spec fetches (URL index + conditional GET)#1247RhysSullivan wants to merge 3 commits into
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | f81d13f | Commit Preview URL Branch Preview URL |
Jul 02 2026, 02:58 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | f81d13f | Jul 02 2026, 02:59 AM |
Cloudflare preview
Sign-in is Cloudflare Access (one-time PIN to an allowed email). The preview has its own database and encryption key; it is destroyed when this PR closes. |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
Greptile SummaryThis PR introduces a tenant-scoped
Confidence Score: 5/5Safe to merge; the caching logic is well-reasoned, all call sites are correctly wired, and the test suite covers the key paths at both unit and e2e level. The spec-cache implementation handles the TTL, conditional GET, and broken-index recovery paths correctly with appropriate error propagation. The persisted flag correctly avoids redundant blob re-puts. No correctness or data-integrity issues were found in the changed files. e2e/scenarios/openapi-spec-fetch-cache-ui.test.ts should be moved to e2e/cloud/ per the project convention for browser-capability tests. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["URL input\n(previewSpec / addSpec / updateSpec)"] --> B{freshness?}
B -->|prefer-cache| C{entry in spec_source\nAND within TTL?}
B -->|revalidate| E
C -->|yes| D{blob in store?}
D -->|yes| HIT["Return cached blob\n(zero network)"]
D -->|no - pruned| E["Conditional GET\n(If-None-Match / If-Modified-Since)"]
C -->|no / stale| E
E --> F{response}
F -->|304 Not Modified| G{blob still in store?}
G -->|yes| REVAL["Return cached blob\nUpdate fetchedAt"]
G -->|no - broken index| RECOVERY["Unconditional GET\n(fetchSpecText)\nWrite blob + index"]
F -->|200 OK| FULL["Write blob + Write spec_source entry\nReturn new text"]
A2["Inline blob input"] --> BYPASS["Bypass cache entirely\n(persisted=false)"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["URL input\n(previewSpec / addSpec / updateSpec)"] --> B{freshness?}
B -->|prefer-cache| C{entry in spec_source\nAND within TTL?}
B -->|revalidate| E
C -->|yes| D{blob in store?}
D -->|yes| HIT["Return cached blob\n(zero network)"]
D -->|no - pruned| E["Conditional GET\n(If-None-Match / If-Modified-Since)"]
C -->|no / stale| E
E --> F{response}
F -->|304 Not Modified| G{blob still in store?}
G -->|yes| REVAL["Return cached blob\nUpdate fetchedAt"]
G -->|no - broken index| RECOVERY["Unconditional GET\n(fetchSpecText)\nWrite blob + index"]
F -->|200 OK| FULL["Write blob + Write spec_source entry\nReturn new text"]
A2["Inline blob input"] --> BYPASS["Bypass cache entirely\n(persisted=false)"]
Reviews (4): Last reviewed commit: "test(e2e): browser journey for the spec-..." | Re-trigger Greptile |
c9ff4bd to
943c411
Compare
4049627 to
651f44a
Compare
Spec URLs resolve through a tenant-shared spec_source index over the existing
content-addressed blob store (url -> {specHash, etag, lastModified,
fetchedAt}): within-TTL repeats — the add flow's detect -> preview -> addSpec
sequence, debounced previews — are served from the store without touching the
network, and updateSpec revalidates with the stored ETag/Last-Modified so an
unchanged upstream costs a bodyless 304 instead of a multi-MB download.
Spec fetches remain structurally unauthenticated (fetchSpecText's dead
credentials param is removed); any future authed-spec feature must bypass
this cache rather than extend it.
943c411 to
65cef87
Compare
A real 127.0.0.1 spec host with a strong ETag counts downloads vs 304s; the scenario drives only the public API and asserts the ledger: preview -> preview -> addSpec is one download (across separate HTTP requests, so on cloud this also proves the URL index is durable, not per-executor memory), updateSpec on an unchanged upstream revalidates to a bodyless 304, and a changed upstream busts the cache through the validators. Non-spec paths (OAuth discovery probes) 404 and stay out of the count.
The API-surface twin pins the contract headlessly; this one makes it watchable: the session video shows the add form analyzing a pasted spec URL, the integration landing, and Edit -> re-fetch-on-save, while the counting ETag spec server proves the network did what the UI implies - one download across the whole add journey, and a bodyless 304 for the refresh.
Summary
OpenAPI spec documents were re-downloaded on every step of every flow: the add form's debounced
previewSpecre-fetched per keystroke-burst,addSpecre-fetched the URL the preview had just downloaded,detectfetched it again, andupdateSpecalways pulled the full document even when nothing changed upstream.Spec-fetch cache (
spec_sourceindex over the blob store)The content-addressed blob store already dedupes spec storage, but its key is the SHA-256 of the body, so it could never skip a download. The new org-owned
spec_sourceplugin-storage collection remembers what each URL last resolved to (url -> { specHash, etag, lastModified, fetchedAt }):spec/<hash>from the store, zero network. This collapses detect → preview → preview → addSpec into one download.If-None-Match/If-Modified-Since); a 304 revalidates the stored blob without a body transfer.updateSpecalways revalidates (never trusts the TTL) — an explicit refresh must see a changed upstream immediately; an unchanged one now costs a bodyless 304 instead of a multi-MB download.Inline blob specs bypass the cache entirely. The index guards against key-hash collisions by storing the full URL in the row, and concurrent first-fetch races degrade to a harmless duplicate-insert-ignored.
Auth invariant: spec fetches were already structurally unauthenticated (the
credentialsparam onfetchSpecTextwas dead code — never passed by any call site; integration headers only apply at invoke time). That dead param is removed and the invariant is now documented at the fetch site: the fetched text is cached tenant-wide, so a future authed-spec feature must bypass this cache, not extend the fetch.Why not
executor.cache? The URL index must be durable and tenant-scoped — plugin storage gives both for free, while KV on per-request cloud executors would add an eventual-consistency question for no win. The primitive (#1248) stays for derived-data consumers where best-effort is fine.Tests
e2e/scenarios/openapi-spec-fetch-cache.test.tspins the contract black-box on both targets (cloud + selfhost): a real ETag-serving spec host counts downloads vs 304s while the scenario drives only the public API — preview → preview → addSpec is one download (separate HTTP requests, so on cloud this also proves the URL index is durable, not per-executor memory), an unchanged-upstream refresh revalidates to a bodyless 304, and a changed upstream busts the cache.spec-cache.test.tspins the same contract at the plugin's HTTP boundary against a real local ETag-serving server: request/304 counts for preview→add single-download, refresh-revalidates-on-304, changed-upstream cache-bust, and inline-blob no-network. Full plugin-openapi suite green (165), google + microsoft plugins green, repo typecheck and lint clean.Follow-ups (deliberately out of scope)
CACHEbinding (feat(sdk): add executor.cache, a host-pluggable key-value cache primitive #1248's follow-up).Stack