From ad26e7c990d58681987ad3607e05ff3c8a4a9ab6 Mon Sep 17 00:00:00 2001 From: Matt Lewis Date: Mon, 30 Mar 2026 20:38:49 +0100 Subject: [PATCH] IMAGES-2139: update images binding mock to chainable handle pattern - images.worker.ts: replace flat methods with ImageHandleImpl (RpcTarget) and synchronous image(imageId) factory on the service entrypoint - index.spec.ts: update tests to use new handle API - ts-expect-error annotations pending workerd PR #6288 types landing --- .changeset/images-chainable-handle.md | 7 ++ .../src/workers/images/images.worker.ts | 96 +++++++++++-------- .../test/plugins/images/index.spec.ts | 24 +++-- 3 files changed, 77 insertions(+), 50 deletions(-) create mode 100644 .changeset/images-chainable-handle.md diff --git a/.changeset/images-chainable-handle.md b/.changeset/images-chainable-handle.md new file mode 100644 index 0000000000..9a5ed42386 --- /dev/null +++ b/.changeset/images-chainable-handle.md @@ -0,0 +1,7 @@ +--- +"miniflare": patch +--- + +Update Images binding local mock to use chainable handle pattern + +`hosted.image(imageId)` now returns a handle with `details()`, `bytes()`, `update()`, and `delete()` methods, aligning with the updated workerd API (https://github.com/cloudflare/workerd/pull/6288). diff --git a/packages/miniflare/src/workers/images/images.worker.ts b/packages/miniflare/src/workers/images/images.worker.ts index f322d3b7a5..265a22ee4a 100644 --- a/packages/miniflare/src/workers/images/images.worker.ts +++ b/packages/miniflare/src/workers/images/images.worker.ts @@ -2,7 +2,7 @@ // Image data is stored as KV values, metadata as KV metadata // Transforms and info operations are handled via HTTP loopback to Node.js Sharp -import { WorkerEntrypoint } from "cloudflare:workers"; +import { RpcTarget, WorkerEntrypoint } from "cloudflare:workers"; import { CoreBindings, CoreHeaders } from "../core/constants"; interface Env { @@ -29,23 +29,70 @@ async function base64DecodeStream( return base64DecodeArrayBuffer(buffer); } -export default class ImagesService extends WorkerEntrypoint { - async details(imageId: string): Promise { - const result = await this.env.IMAGES_STORE.getWithMetadata( - imageId, +class ImageHandleImpl extends RpcTarget { + readonly #imageId: string; + readonly #store: KVNamespace; + + constructor(imageId: string, store: KVNamespace) { + super(); + this.#imageId = imageId; + this.#store = store; + } + + async details(): Promise { + const result = await this.#store.getWithMetadata( + this.#imageId, "arrayBuffer" ); return result.metadata ?? null; } - async image(imageId: string): Promise | null> { - const data = await this.env.IMAGES_STORE.get(imageId, "arrayBuffer"); + async bytes(): Promise | null> { + const data = await this.#store.get(this.#imageId, "arrayBuffer"); if (data === null) { return null; } return new Blob([data]).stream(); } + async update(options: ImageUpdateOptions): Promise { + const existing = await this.#store.getWithMetadata( + this.#imageId, + "arrayBuffer" + ); + if (existing.value === null || existing.metadata === null) { + throw new Error(`Image not found: ${this.#imageId}`); + } + + const updatedMetadata: ImageMetadata = { + ...existing.metadata, + requireSignedURLs: + options.requireSignedURLs ?? existing.metadata.requireSignedURLs, + meta: options.metadata ?? existing.metadata.meta, + creator: options.creator ?? existing.metadata.creator, + }; + + await this.#store.put(this.#imageId, existing.value, { + metadata: updatedMetadata, + }); + return updatedMetadata; + } + + async delete(): Promise { + const existing = await this.#store.get(this.#imageId, "arrayBuffer"); + if (existing === null) { + return false; + } + await this.#store.delete(this.#imageId); + return true; + } +} + +export default class ImagesService extends WorkerEntrypoint { + image(imageId: string): ImageHandleImpl { + return new ImageHandleImpl(imageId, this.env.IMAGES_STORE); + } + async upload( image: ReadableStream | ArrayBuffer, options?: ImageUploadOptions @@ -80,41 +127,6 @@ export default class ImagesService extends WorkerEntrypoint { return metadata; } - async update( - imageId: string, - options: ImageUpdateOptions - ): Promise { - const existing = await this.env.IMAGES_STORE.getWithMetadata( - imageId, - "arrayBuffer" - ); - if (existing.value === null || existing.metadata === null) { - throw new Error(`Image not found: ${imageId}`); - } - - const updatedMetadata: ImageMetadata = { - ...existing.metadata, - requireSignedURLs: - options.requireSignedURLs ?? existing.metadata.requireSignedURLs, - meta: options.metadata ?? existing.metadata.meta, - creator: options.creator ?? existing.metadata.creator, - }; - - await this.env.IMAGES_STORE.put(imageId, existing.value, { - metadata: updatedMetadata, - }); - return updatedMetadata; - } - - async delete(imageId: string): Promise { - const existing = await this.env.IMAGES_STORE.get(imageId, "arrayBuffer"); - if (existing === null) { - return false; - } - await this.env.IMAGES_STORE.delete(imageId); - return true; - } - async list(options?: ImageListOptions): Promise { const limit = options?.limit ?? 50; diff --git a/packages/miniflare/test/plugins/images/index.spec.ts b/packages/miniflare/test/plugins/images/index.spec.ts index ece312a501..0d34283c5d 100644 --- a/packages/miniflare/test/plugins/images/index.spec.ts +++ b/packages/miniflare/test/plugins/images/index.spec.ts @@ -41,7 +41,8 @@ describe("Images hosted CRUD", () => { await images.hosted.upload(imageBuffer(), { id: "blob-test" }); - const stream = await images.hosted.image("blob-test"); + // @ts-expect-error updated types pending workerd PR #6288 + const stream = await images.hosted.image("blob-test").bytes(); assert(stream !== null); const data = new Uint8Array(await new Response(stream).arrayBuffer()); expect(data).toEqual(TEST_IMAGE_BYTES); @@ -64,7 +65,8 @@ describe("Images hosted CRUD", () => { ); expect(metadata.id).toBe("base64-test"); - const stream = await images.hosted.image("base64-test"); + // @ts-expect-error updated types pending workerd PR #6288 + const stream = await images.hosted.image("base64-test").bytes(); assert(stream !== null); const data = new Uint8Array(await new Response(stream).arrayBuffer()); expect(data).toEqual(TEST_IMAGE_BYTES); @@ -77,7 +79,8 @@ describe("Images hosted CRUD", () => { useDispose(mf); const images = await mf.getImagesBinding("IMAGES"); - const metadata = await images.hosted.details("does-not-exist"); + // @ts-expect-error updated types pending workerd PR #6288 + const metadata = await images.hosted.image("does-not-exist").details(); expect(metadata).toBe(null); }); @@ -88,7 +91,8 @@ describe("Images hosted CRUD", () => { useDispose(mf); const images = await mf.getImagesBinding("IMAGES"); - const stream = await images.hosted.image("does-not-exist"); + // @ts-expect-error updated types pending workerd PR #6288 + const stream = await images.hosted.image("does-not-exist").bytes(); expect(stream).toBe(null); }); @@ -99,7 +103,8 @@ describe("Images hosted CRUD", () => { await images.hosted.upload(imageBuffer(), { id: "update-test" }); - const metadata = await images.hosted.update("update-test", { + // @ts-expect-error updated types pending workerd PR #6288 + const metadata = await images.hosted.image("update-test").update({ requireSignedURLs: true, }); expect(metadata.requireSignedURLs).toBe(true); @@ -112,10 +117,12 @@ describe("Images hosted CRUD", () => { await images.hosted.upload(imageBuffer(), { id: "delete-test" }); - const deleted = await images.hosted.delete("delete-test"); + // @ts-expect-error updated types pending workerd PR #6288 + const deleted = await images.hosted.image("delete-test").delete(); expect(deleted).toBe(true); - const metadata = await images.hosted.details("delete-test"); + // @ts-expect-error updated types pending workerd PR #6288 + const metadata = await images.hosted.image("delete-test").details(); expect(metadata).toBe(null); }); @@ -124,7 +131,8 @@ describe("Images hosted CRUD", () => { useDispose(mf); const images = await mf.getImagesBinding("IMAGES"); - const deleted = await images.hosted.delete("does-not-exist"); + // @ts-expect-error updated types pending workerd PR #6288 + const deleted = await images.hosted.image("does-not-exist").delete(); expect(deleted).toBe(false); });