Skip to content

feat(sdk): add executor.cache, a host-pluggable key-value cache primitive#1248

Open
RhysSullivan wants to merge 1 commit into
mainfrom
executor-cache-primitive
Open

feat(sdk): add executor.cache, a host-pluggable key-value cache primitive#1248
RhysSullivan wants to merge 1 commit into
mainfrom
executor-cache-primitive

Conversation

@RhysSullivan

@RhysSullivan RhysSullivan commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

Adds the SDK cache seam future derived-data caching builds on. Supersedes #1118 (open, conflicting with main), rebuilt on current main with the same shape plus the pieces it deferred.

  • ExecutorConfig.cache?: KeyValueStore + executor.cache on the executor surface, with a bounded in-memory TTL/LRU fallback. The fallback is Clock-based, so tests pin TTL with the virtual TestClock instead of patching Date.now.
  • @executor-js/cloudflare/key-value-store: KV-backed adapter + boot layer, mirroring the R2 blob-store binding.
  • makeScopedExecutor reads the store optionally (Effect.serviceOption) and threads it into createExecutor — hosts without a KV tier are unaffected.
  • Cloud seams layer wires an optional CACHE KV binding (guarded like BLOBS); the wrangler binding ships commented out until the namespace is created, and everything falls back cleanly without it.

No consumers in this PR — the first one (OpenAPI spec-fetch caching) is stacked on top.

Tests

  • executor-cache.test.ts: fallback round-trip, host-store precedence, TestClock TTL expiry, LRU eviction.
  • key-value-store.test.ts: KV adapter round-trip, paginated size, bounded-parallel clear.
  • core/sdk, core/api, hosts/cloudflare, apps/cloud typecheck clean; suites green.

Stack

  1. feat(sdk): add executor.cache, a host-pluggable key-value cache primitive #1248 👈 current
  2. feat(openapi): cache spec fetches (URL index + conditional GET) #1247

ExecutorConfig.cache takes an Effect KeyValueStore; absent one the executor
gets a bounded in-memory store with TTL expiry and LRU eviction (Clock-based,
so tests pin TTL with the virtual TestClock). makeScopedExecutor reads an
ambient KeyValueStore service optionally and threads it into createExecutor,
so a host boot layer that provides one makes it durable. Ships the Cloudflare
KV adapter (@executor-js/cloudflare/key-value-store) and wires the optional
CACHE binding through the cloud seams layer; the wrangler binding stays
commented out until the namespace is created, and everything falls back
cleanly without it.
@RhysSullivan RhysSullivan force-pushed the executor-cache-primitive branch from 4049627 to 651f44a Compare July 2, 2026 01:39
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 651f44a Commit Preview URL

Branch Preview URL
Jul 02 2026, 01:41 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 2, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 651f44a Jul 02 2026, 01:42 AM

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Console https://executor-preview-pr-1248.executor-e2e.workers.dev
MCP https://executor-preview-pr-1248.executor-e2e.workers.dev/mcp
Deployed commit 651f44a

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.

@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds executor.cache, a host-pluggable KeyValueStore seam on the SDK surface, with a bounded in-memory TTL/LRU fallback when no durable backend is configured and a Cloudflare KV adapter for hosts that have one.

  • ExecutorConfig.cache?: KeyValueStore plumbed through createExecutor and makeScopedExecutor (via Effect.serviceOption to keep it out of the requirements channel); the in-memory fallback uses Map insertion-order for LRU and Clock.currentTimeMillis so TTL is testable with TestClock.
  • @executor-js/cloudflare/key-value-store wraps a KVNamespace as an Effect KeyValueStore, with paginated size/clear (documented as not for production use at scale) and bounded-parallel batch deletes.
  • CloudCacheLayer in the cloud execution stack is typed Layer<never> and guarded on env.CACHE, so the whole path falls back cleanly on workers without a provisioned KV namespace; the wrangler binding ships commented out pending wrangler kv namespace create.

Confidence Score: 4/5

Safe to merge. The cache seam is additive — existing hosts without a KV namespace are completely unaffected, and the new path degrades gracefully to an in-memory store.

The implementation is well-structured and the fallback/optional-service pattern is consistent with how RequestWebOrigin and RequestOrgSlug are handled elsewhere in makeScopedExecutor. The one minor rough edge is the void signal in clear, which means the paginated KV delete cannot be cooperatively cancelled on fiber interruption — not a correctness problem given clear is documented as off-limits for large namespaces in production, but tidier to drop the parameter.

packages/hosts/cloudflare/src/key-value-store.ts — specifically the clear implementation's signal handling is worth a second look before clear ever gets a production caller.

Important Files Changed

Filename Overview
packages/hosts/cloudflare/src/key-value-store.ts New Cloudflare KV adapter mirroring blob-store.ts. Pagination, batched-parallel deletes, and error mapping are correct; clear discards its AbortSignal (see comment).
packages/core/sdk/src/executor.ts Adds makeMemoryCacheStore (Clock-based TTL, LRU via Map insertion order) and wires executor.cache onto the executor surface. Logic is sound: eviction ordering, capacity, and Clock dependency are all correct.
packages/core/api/src/server/scoped-executor.ts Reads KeyValueStore optionally via Effect.serviceOption, keeping it out of the requirements channel; spreads into createExecutor config only when present. Pattern is consistent with RequestWebOrigin / RequestOrgSlug handling already in the file.
apps/cloud/src/engine/execution-stack.ts Adds CloudCacheLayer (typed Layer to keep the optional service out of the public seam type) and merges it into CloudExecutionSeamsLayer. The guard on env.CACHE and Layer.empty fallback are correct.
packages/core/sdk/src/executor-cache.test.ts Covers fallback round-trip, host-store precedence, TestClock TTL expiry (boundary at exactly 10 min), and LRU eviction at capacity. Cases are well-chosen and sufficient.
packages/hosts/cloudflare/src/key-value-store.test.ts Tests round-trip, multi-page size, and bounded-parallel clear with a fake KV that tracks concurrency. Well-structured and covers the pagination logic.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Host as Cloud Host Boot
    participant CSL as CloudExecutionSeamsLayer
    participant SE as makeScopedExecutor
    participant CE as createExecutor
    participant Cache as executor.cache

    Host->>CSL: env.CACHE present?
    alt KV namespace provisioned
        CSL->>CSL: layerCloudflareKeyValueStore(env.CACHE)
        note over CSL: Layer<KeyValueStore> typed as Layer<never>
    else not provisioned
        CSL->>CSL: Layer.empty
    end

    CSL->>SE: merge into CloudExecutionSeamsLayer

    SE->>SE: Effect.serviceOption(KeyValueStore)
    alt service present
        SE->>CE: "createExecutor({ cache: kvStore })"
        CE->>Cache: use Cloudflare KV adapter
    else service absent
        SE->>CE: "createExecutor({})"
        CE->>Cache: makeMemoryCacheStore() — bounded Map, 2048 cap, 10min TTL, LRU
    end

    CE-->>Host: executor.cache exposed on Executor surface
Loading
%%{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"}}}%%
sequenceDiagram
    participant Host as Cloud Host Boot
    participant CSL as CloudExecutionSeamsLayer
    participant SE as makeScopedExecutor
    participant CE as createExecutor
    participant Cache as executor.cache

    Host->>CSL: env.CACHE present?
    alt KV namespace provisioned
        CSL->>CSL: layerCloudflareKeyValueStore(env.CACHE)
        note over CSL: Layer<KeyValueStore> typed as Layer<never>
    else not provisioned
        CSL->>CSL: Layer.empty
    end

    CSL->>SE: merge into CloudExecutionSeamsLayer

    SE->>SE: Effect.serviceOption(KeyValueStore)
    alt service present
        SE->>CE: "createExecutor({ cache: kvStore })"
        CE->>Cache: use Cloudflare KV adapter
    else service absent
        SE->>CE: "createExecutor({})"
        CE->>Cache: makeMemoryCacheStore() — bounded Map, 2048 cap, 10min TTL, LRU
    end

    CE-->>Host: executor.cache exposed on Executor surface
Loading

Reviews (1): Last reviewed commit: "feat(sdk): add executor.cache, a host-pl..." | Re-trigger Greptile

Comment on lines +64 to +70
clear: Effect.tryPromise({
try: async (signal) => {
void signal;
await deleteAllKeys(kv, await listAllKeys(kv));
},
catch: storeError("clear", undefined),
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The signal parameter from Effect.tryPromise is acknowledged via void signal but never propagated to listAllKeys or deleteAllKeys. If the calling fiber is interrupted, the KV pagination loop and all batch deletes will still run to completion, holding open I/O in an Edge worker with strict CPU budgets. Since clear is an async function, it can simply drop the parameter — or better, pass the signal into the KV calls if Cloudflare's types permit it — to make the intent clearer and avoid the lint-suppress pattern.

Suggested change
clear: Effect.tryPromise({
try: async (signal) => {
void signal;
await deleteAllKeys(kv, await listAllKeys(kv));
},
catch: storeError("clear", undefined),
}),
clear: Effect.tryPromise({
try: async () => {
await deleteAllKeys(kv, await listAllKeys(kv));
},
catch: storeError("clear", undefined),
}),

@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1248

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1248

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1248

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1248

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1248

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1248

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1248

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1248

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1248

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1248

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1248

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1248

executor

npm i https://pkg.pr.new/executor@1248

commit: 651f44a

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.

1 participant