Skip to content

Commit c9fbb44

Browse files
dahliacodex
andcommitted
Accept generated KV binding types
wrangler and the Cloudflare Vite plugin can generate local KV binding declarations whose module identity does not match the nominal KVNamespace type imported from @cloudflare/workers-types. That still made WorkersKvStore and orderingKv reject otherwise compatible KV bindings after the earlier experimental-entrypoint fix. Switch the KV-facing public surface to a minimal structural binding interface and add a type regression test that compiles against a generated-like local declaration source. Fixes #665 Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 6dd4028 commit c9fbb44

5 files changed

Lines changed: 162 additions & 10 deletions

File tree

CHANGES.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ Version 2.0.12
88

99
To be released.
1010

11+
### @fedify/cfworkers
12+
13+
- Fixed a remaining TypeScript type mismatch for Cloudflare Workers users who
14+
pass `wrangler types` or `@cloudflare/vite-plugin` generated KV bindings to
15+
`WorkersKvStore`. The package now accepts a minimal structural KV binding
16+
interface for `WorkersKvStore` and `WorkersMessageQueue`'s `orderingKv`
17+
option instead of requiring the nominal `KVNamespace` type imported from
18+
`@cloudflare/workers-types`, so generated local declarations compile
19+
without casts or `@ts-expect-error`. [[#665]]
20+
21+
[#665]: https://github.com/fedify-dev/fedify/issues/665
22+
1123

1224
Version 2.0.11
1325
--------------

packages/cfworkers/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"build": "pnpm --filter @fedify/cfworkers... run build:self",
6767
"prepack": "pnpm build",
6868
"prepublish": "pnpm build",
69-
"test": "vitest run"
69+
"pretest": "pnpm build",
70+
"test": "tsc -p test/typecheck/tsconfig.json --noEmit && vitest run"
7071
}
7172
}

packages/cfworkers/src/mod.ts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,7 @@
88
* @module
99
* @since 1.9.0
1010
*/
11-
import type {
12-
KVNamespace,
13-
MessageSendRequest,
14-
Queue,
15-
} from "@cloudflare/workers-types";
11+
import type { MessageSendRequest, Queue } from "@cloudflare/workers-types";
1612
import type {
1713
KvKey,
1814
KvStore,
@@ -27,6 +23,67 @@ interface KvMetadata {
2723
expires?: number;
2824
}
2925

26+
interface WorkersKvNamespaceGetWithMetadataResult<Value, Metadata> {
27+
readonly value: Value | null;
28+
readonly metadata: Metadata | null;
29+
}
30+
31+
interface WorkersKvNamespaceListKey<Metadata, Key extends string = string> {
32+
readonly name: Key;
33+
readonly expiration?: number;
34+
readonly metadata?: Metadata;
35+
}
36+
37+
interface WorkersKvNamespaceListResult<Metadata, Key extends string = string> {
38+
readonly list_complete: boolean;
39+
readonly keys: readonly WorkersKvNamespaceListKey<Metadata, Key>[];
40+
readonly cursor?: string;
41+
}
42+
43+
interface WorkersKvNamespaceListOptions {
44+
readonly limit?: number;
45+
readonly prefix?: string | null;
46+
readonly cursor?: string | null;
47+
}
48+
49+
interface WorkersKvNamespacePutOptions {
50+
readonly expiration?: number;
51+
readonly expirationTtl?: number;
52+
readonly metadata?: unknown;
53+
}
54+
55+
/**
56+
* Minimal Cloudflare Workers KV binding shape used by this package.
57+
* Compatible with both `@cloudflare/workers-types` and `wrangler types`
58+
* generated declarations.
59+
* @since 2.0.12
60+
*/
61+
export interface WorkersKvNamespaceLike<Key extends string = string> {
62+
get(key: Key): Promise<string | null>;
63+
get<ExpectedValue = unknown>(
64+
key: Key,
65+
type: "json",
66+
): Promise<ExpectedValue | null>;
67+
getWithMetadata<Metadata = unknown>(
68+
key: Key,
69+
): Promise<WorkersKvNamespaceGetWithMetadataResult<string, Metadata>>;
70+
getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(
71+
key: Key,
72+
type: "json",
73+
): Promise<
74+
WorkersKvNamespaceGetWithMetadataResult<ExpectedValue, Metadata>
75+
>;
76+
put(
77+
key: Key,
78+
value: string,
79+
options?: WorkersKvNamespacePutOptions,
80+
): Promise<void>;
81+
delete(key: Key): Promise<void>;
82+
list<Metadata = unknown>(
83+
options?: WorkersKvNamespaceListOptions,
84+
): Promise<WorkersKvNamespaceListResult<Metadata, Key>>;
85+
}
86+
3087
/**
3188
* Internal message wrapper that includes ordering key metadata.
3289
*/
@@ -74,9 +131,9 @@ export interface ProcessMessageResult {
74131
* @since 1.9.0
75132
*/
76133
export class WorkersKvStore implements KvStore {
77-
#namespace: KVNamespace<string>;
134+
#namespace: WorkersKvNamespaceLike<string>;
78135

79-
constructor(namespace: KVNamespace<string>) {
136+
constructor(namespace: WorkersKvNamespaceLike<string>) {
80137
this.#namespace = namespace;
81138
}
82139

@@ -202,7 +259,7 @@ export interface WorkersMessageQueueOptions {
202259
* guarantees are best-effort. For strict ordering requirements, consider
203260
* using Durable Objects.
204261
*/
205-
readonly orderingKv?: KVNamespace<string>;
262+
readonly orderingKv?: WorkersKvNamespaceLike<string>;
206263

207264
/**
208265
* The prefix for ordering key lock keys. Defaults to `"__fedify_ordering_"`.
@@ -233,7 +290,7 @@ export interface WorkersMessageQueueOptions {
233290
*/
234291
export class WorkersMessageQueue implements MessageQueue {
235292
#queue: Queue;
236-
#orderingKv?: KVNamespace<string>;
293+
#orderingKv?: WorkersKvNamespaceLike<string>;
237294
#orderingKeyPrefix: string;
238295
#orderingLockTtl: number;
239296

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"module": "ESNext",
5+
"moduleResolution": "bundler",
6+
"strict": true,
7+
"esModuleInterop": true,
8+
"skipLibCheck": true
9+
},
10+
"include": [
11+
"./**/*.ts"
12+
]
13+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { Queue } from "@cloudflare/workers-types";
2+
import { WorkersKvStore, WorkersMessageQueue } from "../../dist/mod.js";
3+
4+
interface GeneratedKvGetWithMetadataResult<Value, Metadata> {
5+
readonly value: Value | null;
6+
readonly metadata: Metadata | null;
7+
readonly cacheStatus: string | null;
8+
}
9+
10+
interface GeneratedKvListKey<Metadata, Key extends string = string> {
11+
readonly name: Key;
12+
readonly expiration?: number;
13+
readonly metadata?: Metadata;
14+
}
15+
16+
type GeneratedKvListResult<Metadata, Key extends string = string> =
17+
| {
18+
readonly list_complete: false;
19+
readonly keys: readonly GeneratedKvListKey<Metadata, Key>[];
20+
readonly cursor: string;
21+
readonly cacheStatus: string | null;
22+
}
23+
| {
24+
readonly list_complete: true;
25+
readonly keys: readonly GeneratedKvListKey<Metadata, Key>[];
26+
readonly cacheStatus: string | null;
27+
};
28+
29+
/**
30+
* Mirrors the minimal single-key Cloudflare KV declaration shape emitted by
31+
* `wrangler types`, but comes from a distinct local declaration source.
32+
*/
33+
interface GeneratedKvNamespace<Key extends string = string> {
34+
get(key: Key): Promise<string | null>;
35+
get<ExpectedValue = unknown>(
36+
key: Key,
37+
type: "json",
38+
): Promise<ExpectedValue | null>;
39+
getWithMetadata<Metadata = unknown>(
40+
key: Key,
41+
): Promise<GeneratedKvGetWithMetadataResult<string, Metadata>>;
42+
getWithMetadata<ExpectedValue = unknown, Metadata = unknown>(
43+
key: Key,
44+
type: "json",
45+
): Promise<GeneratedKvGetWithMetadataResult<ExpectedValue, Metadata>>;
46+
put(
47+
key: Key,
48+
value: string | ArrayBuffer | ArrayBufferView | ReadableStream,
49+
options?: {
50+
expiration?: number;
51+
expirationTtl?: number;
52+
metadata?: any | null;
53+
},
54+
): Promise<void>;
55+
delete(key: Key): Promise<void>;
56+
list<Metadata = unknown>(
57+
options?: {
58+
limit?: number;
59+
prefix?: string | null;
60+
cursor?: string | null;
61+
},
62+
): Promise<GeneratedKvListResult<Metadata, Key>>;
63+
}
64+
65+
declare const queue: Queue;
66+
declare const generatedKv: GeneratedKvNamespace<string>;
67+
68+
new WorkersKvStore(generatedKv);
69+
new WorkersMessageQueue(queue, { orderingKv: generatedKv });

0 commit comments

Comments
 (0)