From c9f0eddb7867ec0a8e675cbe0c22de878f3c1c6d Mon Sep 17 00:00:00 2001 From: talves Date: Fri, 21 Mar 2025 14:07:16 +0000 Subject: [PATCH 1/7] feat: add bulk gets for kv in miniflare --- .../src/workers/kv/namespace.worker.ts | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/packages/miniflare/src/workers/kv/namespace.worker.ts b/packages/miniflare/src/workers/kv/namespace.worker.ts index 1359cd37acf6..d8ca86881e93 100644 --- a/packages/miniflare/src/workers/kv/namespace.worker.ts +++ b/packages/miniflare/src/workers/kv/namespace.worker.ts @@ -3,12 +3,14 @@ import { DeferredPromise, DELETE, GET, + POST, HttpError, KeyValueStorage, maybeApply, MiniflareDurableObject, PUT, RouteHandler, + KeyValueEntry, } from "miniflare:shared"; import { KVHeaders, KVLimits, KVParams } from "./constants"; import { @@ -73,6 +75,31 @@ function secondsToMillis(seconds: number): number { return seconds * 1000; } +async function processKeyValue(obj: KeyValueEntry | null, type: string = "text", withMetadata: boolean = false) { + const decoder = new TextDecoder(); + let r = ""; + if(obj?.value) { + for await (const chunk of obj?.value) { + r += decoder.decode(chunk, { stream: true }); + } + r += decoder.decode(); + } + + let val = null; + try { + val = obj?.value == null ? null : type === "json" ? JSON.parse(r) : r; + } catch (err: any) { + throw new HttpError(400, "At least of of the requested keys corresponds to a non-JSON value"); + } + if (val == null) { + return null; + } + if (withMetadata) { + return { value: val, metadata: obj?.metadata ? JSON.stringify(obj?.metadata) : null }; + } + return val; + } + export class KVNamespaceObject extends MiniflareDurableObject { #storage?: KeyValueStorage; get storage() { @@ -81,12 +108,31 @@ export class KVNamespaceObject extends MiniflareDurableObject { } @GET("/:key") + @POST("/bulk/get") get: RouteHandler = async (req, params, url) => { // Decode URL parameters const key = decodeKey(params, url.searchParams); const cacheTtlParam = url.searchParams.get(KVParams.CACHE_TTL); const cacheTtl = cacheTtlParam === null ? undefined : parseInt(cacheTtlParam); + if(req.body != null) { // get bulk + // get bulk + let r = ""; + const decoder = new TextDecoder(); + for await (const chunk of req.body) { + r += decoder.decode(chunk, { stream: true }); + } + r += decoder.decode(); + const parsedBody = JSON.parse(r); + const keys: string[] = parsedBody.keys; + const obj: {[key: string]: any} = {}; + for(const key of keys) { + validateGetOptions(key, { cacheTtl: parsedBody?.cacheTtl }); + const entry = await this.storage.get(key); + obj[key] = await processKeyValue(entry, parsedBody?.type, parsedBody?.withMetadata); + } + return new Response(JSON.stringify(obj)); + } // Get value from storage validateGetOptions(key, { cacheTtl }); From 3cfb2d02d0a3c3e5fdea8cd01ab3f7f3fd8b5a74 Mon Sep 17 00:00:00 2001 From: talves Date: Mon, 24 Mar 2025 16:27:29 +0000 Subject: [PATCH 2/7] update workerd version --- .../src/workers/kv/namespace.worker.ts | 58 +++++++++++++------ .../miniflare/test/plugins/kv/index.spec.ts | 50 ++++++++++++++++ .../miniflare/test/test-shared/miniflare.ts | 22 ++++++- 3 files changed, 110 insertions(+), 20 deletions(-) diff --git a/packages/miniflare/src/workers/kv/namespace.worker.ts b/packages/miniflare/src/workers/kv/namespace.worker.ts index d8ca86881e93..33fcbacc093f 100644 --- a/packages/miniflare/src/workers/kv/namespace.worker.ts +++ b/packages/miniflare/src/workers/kv/namespace.worker.ts @@ -3,14 +3,14 @@ import { DeferredPromise, DELETE, GET, - POST, HttpError, + KeyValueEntry, KeyValueStorage, maybeApply, MiniflareDurableObject, + POST, PUT, RouteHandler, - KeyValueEntry, } from "miniflare:shared"; import { KVHeaders, KVLimits, KVParams } from "./constants"; import { @@ -22,6 +22,7 @@ import { validatePutOptions, } from "./validator.worker"; +const MAX_BULK_LENGTH = 100 * 1024 * 1024; interface KVParams { key: string; } @@ -75,10 +76,14 @@ function secondsToMillis(seconds: number): number { return seconds * 1000; } -async function processKeyValue(obj: KeyValueEntry | null, type: string = "text", withMetadata: boolean = false) { +async function processKeyValue( + obj: KeyValueEntry | null, + type: string = "text", + withMetadata: boolean = false +) { const decoder = new TextDecoder(); let r = ""; - if(obj?.value) { + if (obj?.value) { for await (const chunk of obj?.value) { r += decoder.decode(chunk, { stream: true }); } @@ -89,16 +94,22 @@ async function processKeyValue(obj: KeyValueEntry | null, type: string try { val = obj?.value == null ? null : type === "json" ? JSON.parse(r) : r; } catch (err: any) { - throw new HttpError(400, "At least of of the requested keys corresponds to a non-JSON value"); + throw new HttpError( + 400, + "At least of of the requested keys corresponds to a non-JSON value" + ); } if (val == null) { return null; } if (withMetadata) { - return { value: val, metadata: obj?.metadata ? JSON.stringify(obj?.metadata) : null }; + return { + value: val, + metadata: obj?.metadata ? JSON.stringify(obj?.metadata) : null, + }; } return val; - } +} export class KVNamespaceObject extends MiniflareDurableObject { #storage?: KeyValueStorage; @@ -110,13 +121,7 @@ export class KVNamespaceObject extends MiniflareDurableObject { @GET("/:key") @POST("/bulk/get") get: RouteHandler = async (req, params, url) => { - // Decode URL parameters - const key = decodeKey(params, url.searchParams); - const cacheTtlParam = url.searchParams.get(KVParams.CACHE_TTL); - const cacheTtl = - cacheTtlParam === null ? undefined : parseInt(cacheTtlParam); - if(req.body != null) { // get bulk - // get bulk + if (req.method === "POST" && req.body != null) { let r = ""; const decoder = new TextDecoder(); for await (const chunk of req.body) { @@ -125,15 +130,33 @@ export class KVNamespaceObject extends MiniflareDurableObject { r += decoder.decode(); const parsedBody = JSON.parse(r); const keys: string[] = parsedBody.keys; - const obj: {[key: string]: any} = {}; - for(const key of keys) { + const type = parsedBody?.type; + if (type && type !== "text" && type !== "json") { + return new Response("", { status: 400 }); + } + const obj: { [key: string]: any } = {}; + if (keys.length > 100) { + return new Response("", { status: 400 }); + } + for (const key of keys) { validateGetOptions(key, { cacheTtl: parsedBody?.cacheTtl }); const entry = await this.storage.get(key); - obj[key] = await processKeyValue(entry, parsedBody?.type, parsedBody?.withMetadata); + const value = await processKeyValue( + entry, + parsedBody?.type, + parsedBody?.withMetadata + ); + obj[key] = value; } + return new Response(JSON.stringify(obj)); } + // Decode URL parameters + const key = decodeKey(params, url.searchParams); + const cacheTtlParam = url.searchParams.get(KVParams.CACHE_TTL); + const cacheTtl = + cacheTtlParam === null ? undefined : parseInt(cacheTtlParam); // Get value from storage validateGetOptions(key, { cacheTtl }); const entry = await this.storage.get(key); @@ -160,7 +183,6 @@ export class KVNamespaceObject extends MiniflareDurableObject { const rawExpiration = url.searchParams.get(KVParams.EXPIRATION); const rawExpirationTtl = url.searchParams.get(KVParams.EXPIRATION_TTL); const rawMetadata = req.headers.get(KVHeaders.METADATA); - // Validate key, expiration and metadata const now = millisToSeconds(this.timers.now()); const { expiration, metadata } = validatePutOptions(key, { diff --git a/packages/miniflare/test/plugins/kv/index.spec.ts b/packages/miniflare/test/plugins/kv/index.spec.ts index 6c7efcbc1fea..a30b73f5de82 100644 --- a/packages/miniflare/test/plugins/kv/index.spec.ts +++ b/packages/miniflare/test/plugins/kv/index.spec.ts @@ -122,6 +122,56 @@ test("get: returns value", async (t) => { const result = await kv.get("key"); t.is(result, "value"); }); + +test("bulk get: returns value", async (t) => { + const { kv } = t.context; + await kv.put("key1", "value1"); + const result: any = await kv.get(["key1", "key2"]); + const expectedResult = new Map([ + ["key1", "value1"], + ["key2", null], + ]); + + t.deepEqual(result, expectedResult); +}); + +test("bulk get: check max keys", async (t) => { + const { kv } = t.context; + await kv.put("key1", "value1"); + const keyArray = []; + for (let i = 0; i <= 100; i++) { + keyArray.push(`key${i}`); + } + try { + await kv.get(keyArray); + } catch (error: any) { + t.is(error.message, "KV GET_BULK failed: 400 Bad Request"); + } +}); + +test("bulk get: request json type", async (t) => { + const { kv } = t.context; + await kv.put("key1", '{"example": "ex"}'); + await kv.put("key2", "example"); + let result: any = await kv.get(["key1"]); + let expectedResult: any = new Map([["key1", '{"example": "ex"}']]); + expectedResult = new Map([["key1", '{"example": "ex"}']]); + t.deepEqual(result, expectedResult); + + result = await kv.get(["key1"], "json"); + expectedResult = new Map([["key1", { example: "ex" }]]); + t.deepEqual(result, expectedResult); + + try { + await kv.get(["key1", "key2"], "json"); + } catch (error: any) { + t.is( + error.message, + "KV GET_BULK failed: 400 At least of of the requested keys corresponds to a non-JSON value" + ); + } +}); + test("get: returns null for non-existent keys", async (t) => { const { kv } = t.context; t.is(await kv.get("key"), null); diff --git a/packages/miniflare/test/test-shared/miniflare.ts b/packages/miniflare/test/test-shared/miniflare.ts index c88bf57e13d7..fcf4c08afcba 100644 --- a/packages/miniflare/test/test-shared/miniflare.ts +++ b/packages/miniflare/test/test-shared/miniflare.ts @@ -38,7 +38,23 @@ export function namespace(ns: string, binding: T): Namespaced { return (keys: unknown, ...args: unknown[]) => { if (typeof keys === "string") keys = ns + keys; if (Array.isArray(keys)) keys = keys.map((key) => ns + key); - return (value as (...args: unknown[]) => unknown)(keys, ...args); + const result = (value as (...args: unknown[]) => unknown)( + keys, + ...args + ); + if (result instanceof Promise) { + return result.then((res) => { + if (res instanceof Map) { + let newResult = new Map(); + for (const [key, value] of res) { + newResult.set(key.slice(ns.length), value); + } + return newResult; + } + return res; + }); + } + return result; }; } return value; @@ -54,6 +70,8 @@ export function namespace(ns: string, binding: T): Namespaced { }); } + + export function miniflareTest< Env, Context extends MiniflareTestContext = MiniflareTestContext, @@ -83,7 +101,7 @@ export function miniflareTest< status: 500, headers: { "MF-Experimental-Error-Stack": "true" }, }); - } + } } } `; From 16ec028858e59bf61a83c027d631f64c5fce9d40 Mon Sep 17 00:00:00 2001 From: talves Date: Tue, 25 Mar 2025 17:03:51 +0000 Subject: [PATCH 3/7] fix: lint --- packages/miniflare/src/workers/kv/namespace.worker.ts | 1 - packages/miniflare/test/test-shared/miniflare.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/miniflare/src/workers/kv/namespace.worker.ts b/packages/miniflare/src/workers/kv/namespace.worker.ts index 33fcbacc093f..dda61f5ada7a 100644 --- a/packages/miniflare/src/workers/kv/namespace.worker.ts +++ b/packages/miniflare/src/workers/kv/namespace.worker.ts @@ -22,7 +22,6 @@ import { validatePutOptions, } from "./validator.worker"; -const MAX_BULK_LENGTH = 100 * 1024 * 1024; interface KVParams { key: string; } diff --git a/packages/miniflare/test/test-shared/miniflare.ts b/packages/miniflare/test/test-shared/miniflare.ts index fcf4c08afcba..968813965497 100644 --- a/packages/miniflare/test/test-shared/miniflare.ts +++ b/packages/miniflare/test/test-shared/miniflare.ts @@ -45,7 +45,7 @@ export function namespace(ns: string, binding: T): Namespaced { if (result instanceof Promise) { return result.then((res) => { if (res instanceof Map) { - let newResult = new Map(); + const newResult = new Map(); for (const [key, value] of res) { newResult.set(key.slice(ns.length), value); } @@ -70,8 +70,6 @@ export function namespace(ns: string, binding: T): Namespaced { }); } - - export function miniflareTest< Env, Context extends MiniflareTestContext = MiniflareTestContext, From 816169be66c3c890ff6764ddcb98d4e18df91d17 Mon Sep 17 00:00:00 2001 From: talves Date: Wed, 26 Mar 2025 15:47:27 +0000 Subject: [PATCH 4/7] test: add tests for metadata validation --- .../src/workers/kv/namespace.worker.ts | 26 ++++++---- .../miniflare/test/plugins/kv/index.spec.ts | 51 +++++++++++++++++++ .../miniflare/test/test-shared/miniflare.ts | 2 + 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/packages/miniflare/src/workers/kv/namespace.worker.ts b/packages/miniflare/src/workers/kv/namespace.worker.ts index dda61f5ada7a..bbb85894aaa7 100644 --- a/packages/miniflare/src/workers/kv/namespace.worker.ts +++ b/packages/miniflare/src/workers/kv/namespace.worker.ts @@ -22,6 +22,7 @@ import { validatePutOptions, } from "./validator.worker"; +const MAX_BULK_GET_KEYS = 100; interface KVParams { key: string; } @@ -81,17 +82,22 @@ async function processKeyValue( withMetadata: boolean = false ) { const decoder = new TextDecoder(); - let r = ""; + let decodedValue = ""; if (obj?.value) { for await (const chunk of obj?.value) { - r += decoder.decode(chunk, { stream: true }); + decodedValue += decoder.decode(chunk, { stream: true }); } - r += decoder.decode(); + decodedValue += decoder.decode(); } let val = null; try { - val = obj?.value == null ? null : type === "json" ? JSON.parse(r) : r; + val = + obj?.value == null + ? null + : type === "json" + ? JSON.parse(decodedValue) + : decodedValue; } catch (err: any) { throw new HttpError( 400, @@ -104,7 +110,7 @@ async function processKeyValue( if (withMetadata) { return { value: val, - metadata: obj?.metadata ? JSON.stringify(obj?.metadata) : null, + metadata: obj?.metadata ?? null, }; } return val; @@ -121,20 +127,20 @@ export class KVNamespaceObject extends MiniflareDurableObject { @POST("/bulk/get") get: RouteHandler = async (req, params, url) => { if (req.method === "POST" && req.body != null) { - let r = ""; + let decodedBody = ""; const decoder = new TextDecoder(); for await (const chunk of req.body) { - r += decoder.decode(chunk, { stream: true }); + decodedBody += decoder.decode(chunk, { stream: true }); } - r += decoder.decode(); - const parsedBody = JSON.parse(r); + decodedBody += decoder.decode(); + const parsedBody = JSON.parse(decodedBody); const keys: string[] = parsedBody.keys; const type = parsedBody?.type; if (type && type !== "text" && type !== "json") { return new Response("", { status: 400 }); } const obj: { [key: string]: any } = {}; - if (keys.length > 100) { + if (keys.length > MAX_BULK_GET_KEYS) { return new Response("", { status: 400 }); } for (const key of keys) { diff --git a/packages/miniflare/test/plugins/kv/index.spec.ts b/packages/miniflare/test/plugins/kv/index.spec.ts index a30b73f5de82..84f2365d08ae 100644 --- a/packages/miniflare/test/plugins/kv/index.spec.ts +++ b/packages/miniflare/test/plugins/kv/index.spec.ts @@ -172,6 +172,57 @@ test("bulk get: request json type", async (t) => { } }); +test("bulk get: check metadata", async (t) => { + const { kv } = t.context; + await kv.put("key1", "value1", { + expiration: TIME_FUTURE, + metadata: { testing: true }, + }); + + await kv.put("key2", "value2"); + const result: any = await kv.getWithMetadata(["key1", "key2"]); + const expectedResult: any = new Map([ + ["key1", { value: "value1", metadata: { testing: true } }], + ["key2", { value: "value2", metadata: null }], + ]); + t.deepEqual(result, expectedResult); +}); + +test("bulk get: check metadata with int", async (t) => { + const { kv } = t.context; + await kv.put("key1", "value1", { + expiration: TIME_FUTURE, + metadata: 123, + }); + + const result: any = await kv.getWithMetadata(["key1"]); + const expectedResult: any = new Map([ + ["key1", { value: "value1", metadata: 123 }], + ]); + t.deepEqual(result, expectedResult); +}); + +test("bulk get: check metadata as string", async (t) => { + const { kv } = t.context; + await kv.put("key1", "value1", { + expiration: TIME_FUTURE, + metadata: "example", + }); + const result: any = await kv.getWithMetadata(["key1"]); + const expectedResult: any = new Map([ + ["key1", { value: "value1", metadata: "example" }], + ]); + t.deepEqual(result, expectedResult); +}); + +test("bulk get: get with metadata for 404", async (t) => { + const { kv } = t.context; + + const result: any = await kv.getWithMetadata(["key1"]); + const expectedResult: any = new Map([["key1", null]]); + t.deepEqual(result, expectedResult); +}); + test("get: returns null for non-existent keys", async (t) => { const { kv } = t.context; t.is(await kv.get("key"), null); diff --git a/packages/miniflare/test/test-shared/miniflare.ts b/packages/miniflare/test/test-shared/miniflare.ts index 968813965497..1f53e0ca5f65 100644 --- a/packages/miniflare/test/test-shared/miniflare.ts +++ b/packages/miniflare/test/test-shared/miniflare.ts @@ -44,6 +44,8 @@ export function namespace(ns: string, binding: T): Namespaced { ); if (result instanceof Promise) { return result.then((res) => { + // KV.get([a,b,c]) would be prefixed with ns, so we strip this prefix from response. + // Map keys => [ns-a, ns-b, ns-c] -> [a,b,c] if (res instanceof Map) { const newResult = new Map(); for (const [key, value] of res) { From b0fda7b3d1596c7969a8a8d57639a20bf836cfe1 Mon Sep 17 00:00:00 2001 From: talves Date: Wed, 26 Mar 2025 16:45:31 +0000 Subject: [PATCH 5/7] update changeset --- .changeset/funny-eggs-divide.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/funny-eggs-divide.md diff --git a/.changeset/funny-eggs-divide.md b/.changeset/funny-eggs-divide.md new file mode 100644 index 000000000000..967f35c6d465 --- /dev/null +++ b/.changeset/funny-eggs-divide.md @@ -0,0 +1,5 @@ +--- +"miniflare": minor +--- + +Add bulk gets for Workers KV From e0b46e87439c467dc2b3c24a379d562a2278cb0a Mon Sep 17 00:00:00 2001 From: talves Date: Thu, 27 Mar 2025 10:28:43 +0000 Subject: [PATCH 6/7] fix: address pr comments --- .changeset/funny-eggs-divide.md | 2 +- .../miniflare/src/workers/kv/constants.ts | 1 + .../src/workers/kv/namespace.worker.ts | 28 +++++++++---------- .../miniflare/test/plugins/kv/index.spec.ts | 5 ++-- .../miniflare/test/test-shared/miniflare.ts | 2 +- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/.changeset/funny-eggs-divide.md b/.changeset/funny-eggs-divide.md index 967f35c6d465..6401b4269a64 100644 --- a/.changeset/funny-eggs-divide.md +++ b/.changeset/funny-eggs-divide.md @@ -2,4 +2,4 @@ "miniflare": minor --- -Add bulk gets for Workers KV +Add Miniflare Workers KV bulk get support diff --git a/packages/miniflare/src/workers/kv/constants.ts b/packages/miniflare/src/workers/kv/constants.ts index c211def10763..8529b1ff8808 100644 --- a/packages/miniflare/src/workers/kv/constants.ts +++ b/packages/miniflare/src/workers/kv/constants.ts @@ -34,6 +34,7 @@ export const SiteBindings = { // This ensures edge caching of Workers Sites files is disabled, and the latest // local version is always served. export const SITES_NO_CACHE_PREFIX = "$__MINIFLARE_SITES__$/"; +export const MAX_BULK_GET_KEYS = 100; export function encodeSitesKey(key: string): string { // `encodeURIComponent()` ensures `ETag`s used by `@cloudflare/kv-asset-handler` diff --git a/packages/miniflare/src/workers/kv/namespace.worker.ts b/packages/miniflare/src/workers/kv/namespace.worker.ts index bbb85894aaa7..05912ac3d0ec 100644 --- a/packages/miniflare/src/workers/kv/namespace.worker.ts +++ b/packages/miniflare/src/workers/kv/namespace.worker.ts @@ -12,7 +12,7 @@ import { PUT, RouteHandler, } from "miniflare:shared"; -import { KVHeaders, KVLimits, KVParams } from "./constants"; +import { KVHeaders, KVLimits, KVParams, MAX_BULK_GET_KEYS } from "./constants"; import { decodeKey, decodeListOptions, @@ -22,7 +22,6 @@ import { validatePutOptions, } from "./validator.worker"; -const MAX_BULK_GET_KEYS = 100; interface KVParams { key: string; } @@ -78,8 +77,8 @@ function secondsToMillis(seconds: number): number { async function processKeyValue( obj: KeyValueEntry | null, - type: string = "text", - withMetadata: boolean = false + type: "text" | "json" = "text", + withMetadata = false ) { const decoder = new TextDecoder(); let decodedValue = ""; @@ -92,19 +91,18 @@ async function processKeyValue( let val = null; try { - val = - obj?.value == null - ? null - : type === "json" - ? JSON.parse(decodedValue) - : decodedValue; + val = !obj?.value + ? null + : type === "json" + ? JSON.parse(decodedValue) + : decodedValue; } catch (err: any) { throw new HttpError( 400, - "At least of of the requested keys corresponds to a non-JSON value" + "At least one of the requested keys corresponds to a non-JSON value" ); } - if (val == null) { + if (val === null) { return null; } if (withMetadata) { @@ -137,11 +135,13 @@ export class KVNamespaceObject extends MiniflareDurableObject { const keys: string[] = parsedBody.keys; const type = parsedBody?.type; if (type && type !== "text" && type !== "json") { - return new Response("", { status: 400 }); + return new Response(`Type ${type} is invalid`, { status: 400 }); } const obj: { [key: string]: any } = {}; if (keys.length > MAX_BULK_GET_KEYS) { - return new Response("", { status: 400 }); + return new Response(`Accepting a max of 100 keys, got ${keys.length}`, { + status: 400, + }); } for (const key of keys) { validateGetOptions(key, { cacheTtl: parsedBody?.cacheTtl }); diff --git a/packages/miniflare/test/plugins/kv/index.spec.ts b/packages/miniflare/test/plugins/kv/index.spec.ts index 84f2365d08ae..3560192c5a09 100644 --- a/packages/miniflare/test/plugins/kv/index.spec.ts +++ b/packages/miniflare/test/plugins/kv/index.spec.ts @@ -6,6 +6,7 @@ import consumers from "stream/consumers"; import { Macro, ThrowsExpectation } from "ava"; import { KV_PLUGIN_NAME, + MAX_BULK_GET_KEYS, Miniflare, MiniflareOptions, ReplaceWorkersTypes, @@ -139,7 +140,7 @@ test("bulk get: check max keys", async (t) => { const { kv } = t.context; await kv.put("key1", "value1"); const keyArray = []; - for (let i = 0; i <= 100; i++) { + for (let i = 0; i <= MAX_BULK_GET_KEYS; i++) { keyArray.push(`key${i}`); } try { @@ -167,7 +168,7 @@ test("bulk get: request json type", async (t) => { } catch (error: any) { t.is( error.message, - "KV GET_BULK failed: 400 At least of of the requested keys corresponds to a non-JSON value" + "KV GET_BULK failed: 400 At least one of the requested keys corresponds to a non-JSON value" ); } }); diff --git a/packages/miniflare/test/test-shared/miniflare.ts b/packages/miniflare/test/test-shared/miniflare.ts index 1f53e0ca5f65..3c0c6ec330ca 100644 --- a/packages/miniflare/test/test-shared/miniflare.ts +++ b/packages/miniflare/test/test-shared/miniflare.ts @@ -45,7 +45,7 @@ export function namespace(ns: string, binding: T): Namespaced { if (result instanceof Promise) { return result.then((res) => { // KV.get([a,b,c]) would be prefixed with ns, so we strip this prefix from response. - // Map keys => [ns-a, ns-b, ns-c] -> [a,b,c] + // Map keys => [{ns}{a}, {ns}{b}, {ns}{b}] -> [a,b,c] if (res instanceof Map) { const newResult = new Map(); for (const [key, value] of res) { From 174773ef91980f5231dc32ed1621dcd1506725e1 Mon Sep 17 00:00:00 2001 From: talves Date: Thu, 27 Mar 2025 13:43:48 +0000 Subject: [PATCH 7/7] refactor: small refactor when processing object --- packages/miniflare/src/workers/kv/namespace.worker.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/miniflare/src/workers/kv/namespace.worker.ts b/packages/miniflare/src/workers/kv/namespace.worker.ts index 05912ac3d0ec..f29362a6b26f 100644 --- a/packages/miniflare/src/workers/kv/namespace.worker.ts +++ b/packages/miniflare/src/workers/kv/namespace.worker.ts @@ -102,10 +102,7 @@ async function processKeyValue( "At least one of the requested keys corresponds to a non-JSON value" ); } - if (val === null) { - return null; - } - if (withMetadata) { + if (val && withMetadata) { return { value: val, metadata: obj?.metadata ?? null,