Skip to content

Commit

Permalink
infer chained pipeline responses (#355)
Browse files Browse the repository at this point in the history
* Infer chained response types in Pipeline

* test: fix build and test case

* test: update deps

* ci: set environment for cf workers

* refactor(tests.yaml): remove redundant job name 'add environment'
fix(tests.yaml): fix indentation of 'run' command in 'Add environment' job

* ci(tests.yaml): add push event to trigger workflow on main branch

---------

Co-authored-by: Sam Martin <[email protected]>
  • Loading branch information
chronark and smartinio authored May 31, 2023
1 parent 6d83184 commit ab36634
Show file tree
Hide file tree
Showing 11 changed files with 64 additions and 88 deletions.
19 changes: 15 additions & 4 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: Tests
on:
push:
branches:
- main
pull_request:
schedule:
- cron: "0 0 * * *" # daily
Expand Down Expand Up @@ -466,8 +469,12 @@ jobs:
pnpm add -g wrangler
working-directory: examples/cloudflare-workers

- name: Add account ID
run: echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml
- name: Add environment
run: |
echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml
echo '[vars]' >> wrangler.toml
echo 'UPSTASH_REDIS_REST_URL = "${{ secrets.UPSTASH_REDIS_REST_URL }}"' >> ./wrangler.toml
echo 'UPSTASH_REDIS_REST_TOKEN = "${{ secrets.UPSTASH_REDIS_REST_TOKEN }}"' >> ./wrangler.toml
working-directory: examples/cloudflare-workers

- name: Start example
Expand Down Expand Up @@ -554,8 +561,12 @@ jobs:
working-directory: examples/cloudflare-workers-with-typescript

- name: Add account ID
run: echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml
- name: Add environment
run: |
echo 'account_id = "${{ secrets.CLOUDFLARE_ACCOUNT_ID }}"' >> wrangler.toml
echo '[vars]' >> wrangler.toml
echo 'UPSTASH_REDIS_REST_URL = "${{ secrets.UPSTASH_REDIS_REST_URL }}"' >> ./wrangler.toml
echo 'UPSTASH_REDIS_REST_TOKEN = "${{ secrets.UPSTASH_REDIS_REST_TOKEN }}"' >> ./wrangler.toml
working-directory: examples/cloudflare-workers-with-typescript

- name: Start example
Expand Down
2 changes: 1 addition & 1 deletion cmd/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const outDir = "./dist";

await dnt.emptyDir(outDir);

const version = Deno.args.length > 0 ? Deno.args[0] : "development";
const version = Deno.args.length > 0 ? Deno.args[0] : "v0.0.0";
Deno.writeFileSync(
"version.ts",
new TextEncoder().encode(`export const VERSION = "${version}"`),
Expand Down
2 changes: 1 addition & 1 deletion examples/cloudflare-workers-with-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"publish": "wrangler publish"
},
"dependencies": {
"@upstash/redis": "latest"
"@upstash/redis": "../../dist"
}
}
4 changes: 2 additions & 2 deletions examples/cloudflare-workers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
"publish": "wrangler publish"
},
"devDependencies": {
"wrangler": "^2.4.4"
"wrangler": "^2.20.0"
},
"dependencies": {
"@upstash/redis": "latest"
"@upstash/redis": "link:../../dist"
}
}
30 changes: 0 additions & 30 deletions examples/nextjs_edge/middleware.ts

This file was deleted.

4 changes: 2 additions & 2 deletions examples/nextjs_edge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"start": "next start"
},
"dependencies": {
"@upstash/redis": "latest",
"next": "^13.0.5",
"@upstash/redis": "../../dist",
"next": "^13.4.4",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand Down
15 changes: 11 additions & 4 deletions examples/nextjs_edge/pages/api/counter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { NextApiRequest, NextApiResponse } from "next";
import { Redis } from "@upstash/redis";
import { NextRequest, NextResponse } from "next/server";

export default (_req: NextApiRequest, res: NextApiResponse) => {
res.status(200);
res.send("OK");
export const config = {
runtime: "edge",
};

const redis = Redis.fromEnv();

export default async (_req: NextRequest) => {
const counter = await redis.incr("vercel_edge_counter");
return NextResponse.json({ counter });
};
4 changes: 1 addition & 3 deletions examples/nextjs_edge/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ Deno.test("works", async () => {

const res = await fetch(url);
assertEquals(res.status, 200);
const counterString = res.headers.get("Counter");
const counter = parseInt(counterString!);
const { counter } = await res.json() as { counter: number };
assertEquals("number", typeof counter);
assertEquals("OK", await res.text());
});
3 changes: 2 additions & 1 deletion pkg/commands/set.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ Deno.test("get with xx", async (t) => {
const value = randomID();
await new SetCommand([key, old]).exec(client);

const res = await new SetCommand([key, value, { get: true, xx: true }]).exec(client);
const res = await new SetCommand([key, value, { get: true, xx: true }])
.exec(client);
assertEquals(res, old);
});
});
Expand Down
67 changes: 28 additions & 39 deletions pkg/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@ import { ZMScoreCommand } from "./commands/zmscore.ts";
import { HRandFieldCommand } from "./commands/hrandfield.ts";
import { ZDiffStoreCommand } from "./commands/zdiffstore.ts";

type Chain = <T>(command: Command<any, T>) => Pipeline;
// Given a tuple of commands, returns a tuple of the response data of each command
type InferResponseData<T extends unknown[]> = {
[K in keyof T]: T[K] extends Command<any, infer TData> ? TData : unknown;
};

/**
* Upstash REST API supports command pipelining to send multiple commands in
Expand Down Expand Up @@ -182,7 +185,7 @@ type Chain = <T>(command: Command<any, T>) => Pipeline;
* const res = await p.set("key","value").get("key").exec()
* ```
*
* It's not possible to infer correct types with a dynamic pipeline, so you can
* Return types are inferred if all commands are chained, but you can still
* override the response type manually:
* ```ts
* redis.pipeline()
Expand All @@ -192,9 +195,9 @@ type Chain = <T>(command: Command<any, T>) => Pipeline;
*
* ```
*/
export class Pipeline {
export class Pipeline<TCommands extends Command<any, any>[] = []> {
private client: Requester;
private commands: Command<unknown, unknown>[];
private commands: TCommands;
private commandOptions?: CommandOptions<any, any>;
private multiExec: boolean;
constructor(opts: {
Expand All @@ -204,7 +207,7 @@ export class Pipeline {
}) {
this.client = opts.client;

this.commands = [];
this.commands = ([] as unknown) as TCommands; // the TCommands generic in the class definition is only used for carrying through chained command types and should never be explicitly set when instantiating the class
this.commandOptions = opts.commandOptions;
this.multiExec = opts.multiExec ?? false;
}
Expand All @@ -214,13 +217,16 @@ export class Pipeline {
*
* Returns an array with the results of all pipelined commands.
*
* You can define a return type manually to make working in typescript easier
* If all commands are statically chained from start to finish, types are inferred. You can still define a return type manually if necessary though:
* ```ts
* redis.pipeline().get("key").exec<[{ greeting: string }]>()
* const p = redis.pipeline()
* p.get("key")
* const result = p.exec<[{ greeting: string }]>()
* ```
*/
exec = async <
TCommandResults extends unknown[] = unknown[],
TCommandResults extends unknown[] = [] extends TCommands ? unknown[]
: InferResponseData<TCommands>,
>(): Promise<TCommandResults> => {
if (this.commands.length === 0) {
throw new Error("Pipeline is empty");
Expand All @@ -245,12 +251,14 @@ export class Pipeline {
};

/**
* Pushes a command into the pipelien and returns a chainable instance of the
* Pushes a command into the pipeline and returns a chainable instance of the
* pipeline
*/
private chain<T>(command: Command<any, T>): this {
private chain<T>(
command: Command<any, T>,
): Pipeline<[...TCommands, Command<any, T>]> {
this.commands.push(command);
return this;
return this as any; // TS thinks we're returning Pipeline<[]> here, because we're not creating a new instance of the class, hence the cast
}

/**
Expand All @@ -274,14 +282,18 @@ export class Pipeline {
destinationKey: string,
sourceKey: string,
...sourceKeys: string[]
): Pipeline;
(op: "not", destinationKey: string, sourceKey: string): Pipeline;
): Pipeline<[...TCommands, BitOpCommand]>;
(
op: "not",
destinationKey: string,
sourceKey: string,
): Pipeline<[...TCommands, BitOpCommand]>;
} = (
op: "and" | "or" | "xor" | "not",
destinationKey: string,
sourceKey: string,
...sourceKeys: string[]
): Pipeline =>
) =>
this.chain(
new BitOpCommand(
[op as any, destinationKey, sourceKey, ...sourceKeys],
Expand Down Expand Up @@ -549,7 +561,7 @@ export class Pipeline {
direction: "before" | "after",
pivot: TData,
value: TData,
): Pipeline =>
) =>
this.chain(
new LInsertCommand<TData>(
[key, direction, pivot, value],
Expand Down Expand Up @@ -1070,30 +1082,7 @@ export class Pipeline {
/**
* @see https://redis.io/commands/?group=json
*/
get json(): {
arrappend: (...args: CommandArgs<typeof JsonArrAppendCommand>) => Pipeline;
arrindex: (...args: CommandArgs<typeof JsonArrIndexCommand>) => Pipeline;
arrinsert: (...args: CommandArgs<typeof JsonArrInsertCommand>) => Pipeline;
arrlen: (...args: CommandArgs<typeof JsonArrLenCommand>) => Pipeline;
arrpop: (...args: CommandArgs<typeof JsonArrPopCommand>) => Pipeline;
arrtrim: (...args: CommandArgs<typeof JsonArrTrimCommand>) => Pipeline;
clear: (...args: CommandArgs<typeof JsonClearCommand>) => Pipeline;
del: (...args: CommandArgs<typeof JsonDelCommand>) => Pipeline;
forget: (...args: CommandArgs<typeof JsonForgetCommand>) => Pipeline;
get: (...args: CommandArgs<typeof JsonGetCommand>) => Pipeline;
mget: (...args: CommandArgs<typeof JsonMGetCommand>) => Pipeline;
numincrby: (...args: CommandArgs<typeof JsonNumIncrByCommand>) => Pipeline;
nummultby: (...args: CommandArgs<typeof JsonNumMultByCommand>) => Pipeline;
objkeys: (...args: CommandArgs<typeof JsonObjKeysCommand>) => Pipeline;
objlen: (...args: CommandArgs<typeof JsonObjLenCommand>) => Pipeline;
resp: (...args: CommandArgs<typeof JsonRespCommand>) => Pipeline;
set: (...args: CommandArgs<typeof JsonSetCommand>) => Pipeline;
strappend: (...args: CommandArgs<typeof JsonStrAppendCommand>) => Pipeline;
strlen: (...args: CommandArgs<typeof JsonStrLenCommand>) => Pipeline;
toggle: (...args: CommandArgs<typeof JsonToggleCommand>) => Pipeline;
type: (...args: CommandArgs<typeof JsonTypeCommand>) => Pipeline;
} {
// For some reason we needed to define the types manually, otherwise Deno wouldn't build it
get json() {
return {
/**
* @see https://redis.io/commands/json.arrappend
Expand Down
2 changes: 1 addition & 1 deletion version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = "development"
export const VERSION = "v0.0.0";

0 comments on commit ab36634

Please sign in to comment.