From 37b8f371015e56517e6c0b9efa1773a029983b18 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Mon, 8 Jul 2024 12:22:35 +0300 Subject: [PATCH 01/20] add: readYourWrites option interface --- pkg/auto-pipeline.test.ts | 2 +- pkg/auto-pipeline.ts | 5 +++-- pkg/http.ts | 5 +++++ pkg/redis.ts | 2 ++ pkg/types.ts | 1 + 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pkg/auto-pipeline.test.ts b/pkg/auto-pipeline.test.ts index 24810f08..223bb441 100644 --- a/pkg/auto-pipeline.test.ts +++ b/pkg/auto-pipeline.test.ts @@ -144,7 +144,7 @@ describe("Auto pipeline", () => { redis.zscore(newKey(), "member"), redis.zunionstore(newKey(), 1, [newKey()]), redis.zunion(1, [newKey()]), - redis.json.set(persistentKey3, '$', { log: ["one", "two"] }), + redis.json.set(persistentKey3, "$", { log: ["one", "two"] }), redis.json.arrappend(persistentKey3, "$.log", '"three"'), ]); expect(result).toBeTruthy(); diff --git a/pkg/auto-pipeline.ts b/pkg/auto-pipeline.ts index 8fddbb64..4df131d5 100644 --- a/pkg/auto-pipeline.ts +++ b/pkg/auto-pipeline.ts @@ -35,8 +35,9 @@ export function createAutoPipelineProxy(_redis: Redis, json?: boolean): Redis { // If the method is a function on the pipeline, wrap it with the executor logic const isFunction = json - ? typeof redis.autoPipelineExecutor.pipeline.json[command as keyof Pipeline["json"]] === "function" - : typeof redis.autoPipelineExecutor.pipeline[command as keyof Pipeline] === "function" + ? typeof redis.autoPipelineExecutor.pipeline.json[command as keyof Pipeline["json"]] === + "function" + : typeof redis.autoPipelineExecutor.pipeline[command as keyof Pipeline] === "function"; if (isFunction) { return (...args: CommandArgs) => { // pass the function as a callback diff --git a/pkg/http.ts b/pkg/http.ts index 10df82f4..e89d8f82 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -95,6 +95,7 @@ export type HttpClientConfig = { agent?: any; signal?: AbortSignal; keepAlive?: boolean; + readYourWrites?: boolean; } & RequesterConfig; export class HttpClient implements Requester { @@ -107,12 +108,14 @@ export class HttpClient implements Requester { responseEncoding?: false | "base64"; cache?: CacheSetting; keepAlive: boolean; + readYourWrites?: boolean; }; public readonly retry: { attempts: number; backoff: (retryCount: number) => number; }; + protected upstashSyncToken: string | undefined; public constructor(config: HttpClientConfig) { this.options = { @@ -122,6 +125,7 @@ export class HttpClient implements Requester { cache: config.cache, signal: config.signal, keepAlive: config.keepAlive ?? true, + readYourWrites: config.readYourWrites, }; this.baseUrl = config.baseUrl.replace(/\/$/, ""); @@ -195,6 +199,7 @@ export class HttpClient implements Requester { keepalive: this.options.keepAlive, agent: this.options?.agent, signal: this.options.signal, + readYourWrites: this.options.readYourWrites, /** * Fastly specific diff --git a/pkg/redis.ts b/pkg/redis.ts index a22abec3..d3e44890 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -191,6 +191,7 @@ export class Redis { protected opts?: CommandOptions; protected enableTelemetry: boolean; protected enableAutoPipelining: boolean; + protected readYourWrites: boolean; /** * Create a new redis client @@ -208,6 +209,7 @@ export class Redis { this.opts = opts; this.enableTelemetry = opts?.enableTelemetry ?? true; this.enableAutoPipelining = opts?.enableAutoPipelining ?? false; + this.readYourWrites = opts?.readYourWrites ?? true; } get json() { diff --git a/pkg/types.ts b/pkg/types.ts index fddfc794..970ca887 100644 --- a/pkg/types.ts +++ b/pkg/types.ts @@ -31,4 +31,5 @@ export type RedisOptions = { latencyLogging?: boolean; enableTelemetry?: boolean; enableAutoPipelining?: boolean; + readYourWrites?: boolean; }; From bcdd601a1d32996d1d4ab8413d84cc505119fc51 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Tue, 9 Jul 2024 15:50:57 +0300 Subject: [PATCH 02/20] send local sync token on requests --- pkg/commands/command.ts | 2 ++ pkg/http.ts | 47 +++++++++++++++++++++++------------- pkg/read-your-writes.test.ts | 21 ++++++++++++++++ pkg/redis.ts | 35 ++++++++++++++------------- 4 files changed, 71 insertions(+), 34 deletions(-) create mode 100644 pkg/read-your-writes.test.ts diff --git a/pkg/commands/command.ts b/pkg/commands/command.ts index e25c4b2a..09961526 100644 --- a/pkg/commands/command.ts +++ b/pkg/commands/command.ts @@ -80,7 +80,9 @@ export class Command { public async exec(client: Requester): Promise { const { result, error } = await client.request({ body: this.command, + upstashSyncToken: client.upstashSyncToken, }); + if (error) { throw new UpstashError(error); } diff --git a/pkg/http.ts b/pkg/http.ts index e89d8f82..e2d7583a 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -15,10 +15,13 @@ export type UpstashRequest = { * Request body will be serialized to json */ body?: unknown; + + upstashSyncToken?: string; }; export type UpstashResponse = { result?: TResult; error?: string }; export interface Requester { + upstashSyncToken: string; request: (req: UpstashRequest) => Promise>; } @@ -29,22 +32,22 @@ type ResultError = { export type RetryConfig = | false | { - /** - * The number of retries to attempt before giving up. - * - * @default 5 - */ - retries?: number; - /** - * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. - * - * @default - * ```ts - * Math.exp(retryCount) * 50 - * ``` - */ - backoff?: (retryCount: number) => number; - }; + /** + * The number of retries to attempt before giving up. + * + * @default 5 + */ + retries?: number; + /** + * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. + * + * @default + * ```ts + * Math.exp(retryCount) * 50 + * ``` + */ + backoff?: (retryCount: number) => number; + }; export type Options = { backend?: string; @@ -110,12 +113,13 @@ export class HttpClient implements Requester { keepAlive: boolean; readYourWrites?: boolean; }; + public _upstashSyncToken = ""; public readonly retry: { attempts: number; backoff: (retryCount: number) => number; }; - protected upstashSyncToken: string | undefined; + public upstashSyncToken: string; public constructor(config: HttpClientConfig) { this.options = { @@ -127,6 +131,7 @@ export class HttpClient implements Requester { keepAlive: config.keepAlive ?? true, readYourWrites: config.readYourWrites, }; + this.upstashSyncToken = ""; this.baseUrl = config.baseUrl.replace(/\/$/, ""); @@ -207,6 +212,10 @@ export class HttpClient implements Requester { backend: this.options?.backend, }; + const newHeader = req.upstashSyncToken || this._upstashSyncToken; + this.headers['upstash-sync-token'] = newHeader + + let res: Response | null = null; let error: Error | null = null; for (let i = 0; i <= this.retry.attempts; i++) { @@ -238,6 +247,9 @@ export class HttpClient implements Requester { throw new UpstashError(`${body.error}, command was: ${JSON.stringify(req.body)}`); } + const headers = res.headers + this._upstashSyncToken = headers.get('upstash-sync-token') ?? ""; + if (this.options?.responseEncoding === "base64") { if (Array.isArray(body)) { return body.map(({ result, error }) => ({ @@ -248,6 +260,7 @@ export class HttpClient implements Requester { const result = decode(body.result) as any; return { result, error: body.error }; } + return body as UpstashResponse; } } diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts new file mode 100644 index 00000000..70b9e0f1 --- /dev/null +++ b/pkg/read-your-writes.test.ts @@ -0,0 +1,21 @@ +import { keygen, newHttpClient } from "./test-utils" + +import { afterAll, expect, test, describe } from "bun:test"; +// import { GetCommand } from "./get"; +// import { SetCommand } from "./set"; +import { SetCommand } from "./commands/set"; + +const client = newHttpClient(); + +const { cleanup } = keygen(); +afterAll(cleanup); +describe("Read Your Writes", () => { + test("successfully retrieves Upstash-Sync-Token in the response header", async () => { + const initialSync = client._upstashSyncToken; + await new SetCommand(["key", "value"]).exec(client); + const updatedSync = client._upstashSyncToken; + await new SetCommand(["key", "value"]).exec(client); + + expect(updatedSync).not.toEqual(initialSync); + }); +}); diff --git a/pkg/redis.ts b/pkg/redis.ts index d3e44890..ea4e5ce5 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -193,6 +193,7 @@ export class Redis { protected enableAutoPipelining: boolean; protected readYourWrites: boolean; + /** * Create a new redis client * @@ -452,9 +453,9 @@ export class Redis { sourceKey: string, ...sourceKeys: string[] ) => - new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( - this.client, - ); + new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( + this.client, + ); /** * @see https://redis.io/commands/bitpos @@ -1203,10 +1204,10 @@ export class Redis { ...args: | [key: string, scoreMember: ScoreMember, ...scoreMemberPairs: ScoreMember[]] | [ - key: string, - opts: ZAddCommandOptions, - ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], - ] + key: string, + opts: ZAddCommandOptions, + ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], + ] ) => { if ("score" in args[1]) { return new ZAddCommand( @@ -1281,17 +1282,17 @@ export class Redis { ...args: | [key: string, min: number, max: number, opts?: ZRangeCommandOptions] | [ - key: string, - min: `(${string}` | `[${string}` | "-" | "+", - max: `(${string}` | `[${string}` | "-" | "+", - opts: { byLex: true } & ZRangeCommandOptions, - ] + key: string, + min: `(${string}` | `[${string}` | "-" | "+", + max: `(${string}` | `[${string}` | "-" | "+", + opts: { byLex: true } & ZRangeCommandOptions, + ] | [ - key: string, - min: number | `(${number}` | "-inf" | "+inf", - max: number | `(${number}` | "-inf" | "+inf", - opts: { byScore: true } & ZRangeCommandOptions, - ] + key: string, + min: number | `(${number}` | "-inf" | "+inf", + max: number | `(${number}` | "-inf" | "+inf", + opts: { byScore: true } & ZRangeCommandOptions, + ] ) => new ZRangeCommand(args as any, this.opts).exec(this.client); /** From 105bffdc353416c5f1f4da304ad6b124e4cdbf81 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Tue, 9 Jul 2024 15:51:08 +0300 Subject: [PATCH 03/20] fmt --- pkg/http.ts | 39 ++++++++++++++++++------------------ pkg/read-your-writes.test.ts | 18 ++++++++--------- pkg/redis.ts | 35 ++++++++++++++++---------------- 3 files changed, 45 insertions(+), 47 deletions(-) diff --git a/pkg/http.ts b/pkg/http.ts index e2d7583a..3a8af6b3 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -32,22 +32,22 @@ type ResultError = { export type RetryConfig = | false | { - /** - * The number of retries to attempt before giving up. - * - * @default 5 - */ - retries?: number; - /** - * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. - * - * @default - * ```ts - * Math.exp(retryCount) * 50 - * ``` - */ - backoff?: (retryCount: number) => number; - }; + /** + * The number of retries to attempt before giving up. + * + * @default 5 + */ + retries?: number; + /** + * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. + * + * @default + * ```ts + * Math.exp(retryCount) * 50 + * ``` + */ + backoff?: (retryCount: number) => number; + }; export type Options = { backend?: string; @@ -213,8 +213,7 @@ export class HttpClient implements Requester { }; const newHeader = req.upstashSyncToken || this._upstashSyncToken; - this.headers['upstash-sync-token'] = newHeader - + this.headers["upstash-sync-token"] = newHeader; let res: Response | null = null; let error: Error | null = null; @@ -247,8 +246,8 @@ export class HttpClient implements Requester { throw new UpstashError(`${body.error}, command was: ${JSON.stringify(req.body)}`); } - const headers = res.headers - this._upstashSyncToken = headers.get('upstash-sync-token') ?? ""; + const headers = res.headers; + this._upstashSyncToken = headers.get("upstash-sync-token") ?? ""; if (this.options?.responseEncoding === "base64") { if (Array.isArray(body)) { diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index 70b9e0f1..c7c19716 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -1,6 +1,6 @@ -import { keygen, newHttpClient } from "./test-utils" +import { keygen, newHttpClient } from "./test-utils"; -import { afterAll, expect, test, describe } from "bun:test"; +import { afterAll, describe, expect, test } from "bun:test"; // import { GetCommand } from "./get"; // import { SetCommand } from "./set"; import { SetCommand } from "./commands/set"; @@ -10,12 +10,12 @@ const client = newHttpClient(); const { cleanup } = keygen(); afterAll(cleanup); describe("Read Your Writes", () => { - test("successfully retrieves Upstash-Sync-Token in the response header", async () => { - const initialSync = client._upstashSyncToken; - await new SetCommand(["key", "value"]).exec(client); - const updatedSync = client._upstashSyncToken; - await new SetCommand(["key", "value"]).exec(client); + test("successfully retrieves Upstash-Sync-Token in the response header", async () => { + const initialSync = client._upstashSyncToken; + await new SetCommand(["key", "value"]).exec(client); + const updatedSync = client._upstashSyncToken; + await new SetCommand(["key", "value"]).exec(client); - expect(updatedSync).not.toEqual(initialSync); - }); + expect(updatedSync).not.toEqual(initialSync); + }); }); diff --git a/pkg/redis.ts b/pkg/redis.ts index ea4e5ce5..d3e44890 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -193,7 +193,6 @@ export class Redis { protected enableAutoPipelining: boolean; protected readYourWrites: boolean; - /** * Create a new redis client * @@ -453,9 +452,9 @@ export class Redis { sourceKey: string, ...sourceKeys: string[] ) => - new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( - this.client, - ); + new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( + this.client, + ); /** * @see https://redis.io/commands/bitpos @@ -1204,10 +1203,10 @@ export class Redis { ...args: | [key: string, scoreMember: ScoreMember, ...scoreMemberPairs: ScoreMember[]] | [ - key: string, - opts: ZAddCommandOptions, - ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], - ] + key: string, + opts: ZAddCommandOptions, + ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], + ] ) => { if ("score" in args[1]) { return new ZAddCommand( @@ -1282,17 +1281,17 @@ export class Redis { ...args: | [key: string, min: number, max: number, opts?: ZRangeCommandOptions] | [ - key: string, - min: `(${string}` | `[${string}` | "-" | "+", - max: `(${string}` | `[${string}` | "-" | "+", - opts: { byLex: true } & ZRangeCommandOptions, - ] + key: string, + min: `(${string}` | `[${string}` | "-" | "+", + max: `(${string}` | `[${string}` | "-" | "+", + opts: { byLex: true } & ZRangeCommandOptions, + ] | [ - key: string, - min: number | `(${number}` | "-inf" | "+inf", - max: number | `(${number}` | "-inf" | "+inf", - opts: { byScore: true } & ZRangeCommandOptions, - ] + key: string, + min: number | `(${number}` | "-inf" | "+inf", + max: number | `(${number}` | "-inf" | "+inf", + opts: { byScore: true } & ZRangeCommandOptions, + ] ) => new ZRangeCommand(args as any, this.opts).exec(this.client); /** From 6f9ca94533efee5ebfc21bc9e34c7c68a230cd73 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Tue, 9 Jul 2024 17:50:05 +0300 Subject: [PATCH 04/20] add promise.all tests --- pkg/http.ts | 2 +- pkg/pipeline.ts | 1 + pkg/read-your-writes.test.ts | 49 +++++++++++++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/pkg/http.ts b/pkg/http.ts index 3a8af6b3..199586c7 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -212,7 +212,7 @@ export class HttpClient implements Requester { backend: this.options?.backend, }; - const newHeader = req.upstashSyncToken || this._upstashSyncToken; + const newHeader = this._upstashSyncToken; this.headers["upstash-sync-token"] = newHeader; let res: Response | null = null; diff --git a/pkg/pipeline.ts b/pkg/pipeline.ts index 003bd61d..f5cd50f6 100644 --- a/pkg/pipeline.ts +++ b/pkg/pipeline.ts @@ -279,6 +279,7 @@ export class Pipeline[] = []> { throw new Error("Pipeline is empty"); } const path = this.multiExec ? ["multi-exec"] : ["pipeline"]; + const res = (await this.client.request({ path, body: Object.values(this.commands).map((c) => c.command), diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index c7c19716..558177df 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -4,13 +4,13 @@ import { afterAll, describe, expect, test } from "bun:test"; // import { GetCommand } from "./get"; // import { SetCommand } from "./set"; import { SetCommand } from "./commands/set"; - -const client = newHttpClient(); +import { Redis } from "./redis"; const { cleanup } = keygen(); afterAll(cleanup); describe("Read Your Writes", () => { - test("successfully retrieves Upstash-Sync-Token in the response header", async () => { + test("successfully retrieves Upstash-Sync-Token in the response header and updates local state", async () => { + const client = newHttpClient(); const initialSync = client._upstashSyncToken; await new SetCommand(["key", "value"]).exec(client); const updatedSync = client._upstashSyncToken; @@ -18,4 +18,47 @@ describe("Read Your Writes", () => { expect(updatedSync).not.toEqual(initialSync); }); + + test.skip("succesfully updates sync state with pipeline", async () => { + const client = newHttpClient(); + + await new SetCommand(["key", "value"]).exec(client); + const initialSync1 = client._upstashSyncToken; + console.log("initialSync1", initialSync1); + + const initialSync = client._upstashSyncToken; + console.log("initialSync", initialSync); + const { pipeline } = new Redis(client); + const p = pipeline(); + + p.set("key1", "value1"); + p.set("key2", "value2"); + p.set("key3", "value3"); + p.set("key4", "value4"); + p.set("key5", "value5"); + + await p.exec(); + + const updatedSync = client._upstashSyncToken; + + await new SetCommand(["key", "value"]).exec(client); + const updatedSync2 = client._upstashSyncToken; + + console.log("updatedSync", updatedSync); + console.log("updatedSync2", updatedSync2); + }); + + test("updates after each element of promise.all", async () => { + const client = newHttpClient(); + let currentSync = client._upstashSyncToken; + + const promises = Array.from({ length: 3 }, (_, i) => + new SetCommand([`key${i}`, `value${i}`]).exec(client).then(() => { + expect(client._upstashSyncToken).not.toEqual(currentSync); + currentSync = client._upstashSyncToken; + }), + ); + + await Promise.all(promises); + }); }); From 963abaea76f1827446801368b461bf8c87089901 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Tue, 9 Jul 2024 17:53:15 +0300 Subject: [PATCH 05/20] add: lua script test --- pkg/read-your-writes.test.ts | 126 ++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 52 deletions(-) diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index 558177df..748215a3 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -9,56 +9,78 @@ import { Redis } from "./redis"; const { cleanup } = keygen(); afterAll(cleanup); describe("Read Your Writes", () => { - test("successfully retrieves Upstash-Sync-Token in the response header and updates local state", async () => { - const client = newHttpClient(); - const initialSync = client._upstashSyncToken; - await new SetCommand(["key", "value"]).exec(client); - const updatedSync = client._upstashSyncToken; - await new SetCommand(["key", "value"]).exec(client); - - expect(updatedSync).not.toEqual(initialSync); - }); - - test.skip("succesfully updates sync state with pipeline", async () => { - const client = newHttpClient(); - - await new SetCommand(["key", "value"]).exec(client); - const initialSync1 = client._upstashSyncToken; - console.log("initialSync1", initialSync1); - - const initialSync = client._upstashSyncToken; - console.log("initialSync", initialSync); - const { pipeline } = new Redis(client); - const p = pipeline(); - - p.set("key1", "value1"); - p.set("key2", "value2"); - p.set("key3", "value3"); - p.set("key4", "value4"); - p.set("key5", "value5"); - - await p.exec(); - - const updatedSync = client._upstashSyncToken; - - await new SetCommand(["key", "value"]).exec(client); - const updatedSync2 = client._upstashSyncToken; - - console.log("updatedSync", updatedSync); - console.log("updatedSync2", updatedSync2); - }); - - test("updates after each element of promise.all", async () => { - const client = newHttpClient(); - let currentSync = client._upstashSyncToken; - - const promises = Array.from({ length: 3 }, (_, i) => - new SetCommand([`key${i}`, `value${i}`]).exec(client).then(() => { - expect(client._upstashSyncToken).not.toEqual(currentSync); - currentSync = client._upstashSyncToken; - }), - ); - - await Promise.all(promises); - }); + test("successfully retrieves Upstash-Sync-Token in the response header and updates local state", async () => { + const client = newHttpClient(); + const initialSync = client._upstashSyncToken; + await new SetCommand(["key", "value"]).exec(client); + const updatedSync = client._upstashSyncToken; + await new SetCommand(["key", "value"]).exec(client); + + expect(updatedSync).not.toEqual(initialSync); + }); + + test.skip("succesfully updates sync state with pipeline", async () => { + const client = newHttpClient(); + + await new SetCommand(["key", "value"]).exec(client); + const initialSync1 = client._upstashSyncToken; + console.log("initialSync1", initialSync1); + + const initialSync = client._upstashSyncToken; + console.log("initialSync", initialSync); + const { pipeline } = new Redis(client); + const p = pipeline(); + + p.set("key1", "value1"); + p.set("key2", "value2"); + p.set("key3", "value3"); + p.set("key4", "value4"); + p.set("key5", "value5"); + + await p.exec(); + + const updatedSync = client._upstashSyncToken; + + await new SetCommand(["key", "value"]).exec(client); + const updatedSync2 = client._upstashSyncToken; + + console.log("updatedSync", updatedSync); + console.log("updatedSync2", updatedSync2); + }); + + test("updates after each element of promise.all", async () => { + const client = newHttpClient(); + let currentSync = client._upstashSyncToken; + + const promises = Array.from({ length: 3 }, (_, i) => + new SetCommand([`key${i}`, `value${i}`]).exec(client).then(() => { + expect(client._upstashSyncToken).not.toEqual(currentSync); + currentSync = client._upstashSyncToken; + }), + ); + + await Promise.all(promises); + }); + + test("updates after successful lua script call", async () => { + + const s = `redis.call('SET', 'mykey', 'myvalue') + return 1 + ` + + const client = newHttpClient(); + await new SetCommand(["key", "value"]).exec(client); + const initialSync = client._upstashSyncToken; + + const redis = new Redis(client); + const script = redis.createScript(s); + + await script.exec([], []); + + const updatedSync = client._upstashSyncToken; + + expect(updatedSync).not.toEqual(initialSync); + + + }) }); From cf10255948c53f185d089da029edee373507ff0a Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 09:25:55 +0300 Subject: [PATCH 06/20] format tests --- pkg/read-your-writes.test.ts | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index 748215a3..67fd9def 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -1,16 +1,16 @@ import { keygen, newHttpClient } from "./test-utils"; import { afterAll, describe, expect, test } from "bun:test"; -// import { GetCommand } from "./get"; -// import { SetCommand } from "./set"; + import { SetCommand } from "./commands/set"; import { Redis } from "./redis"; +const client = newHttpClient(); const { cleanup } = keygen(); afterAll(cleanup); -describe("Read Your Writes", () => { +describe("Read Your Writes Feature", () => { test("successfully retrieves Upstash-Sync-Token in the response header and updates local state", async () => { - const client = newHttpClient(); + const initialSync = client._upstashSyncToken; await new SetCommand(["key", "value"]).exec(client); const updatedSync = client._upstashSyncToken; @@ -19,37 +19,26 @@ describe("Read Your Writes", () => { expect(updatedSync).not.toEqual(initialSync); }); - test.skip("succesfully updates sync state with pipeline", async () => { - const client = newHttpClient(); - - await new SetCommand(["key", "value"]).exec(client); - const initialSync1 = client._upstashSyncToken; - console.log("initialSync1", initialSync1); - + test("succesfully updates sync state with pipeline", async () => { const initialSync = client._upstashSyncToken; - console.log("initialSync", initialSync); + const { pipeline } = new Redis(client); const p = pipeline(); p.set("key1", "value1"); p.set("key2", "value2"); p.set("key3", "value3"); - p.set("key4", "value4"); - p.set("key5", "value5"); await p.exec(); const updatedSync = client._upstashSyncToken; - await new SetCommand(["key", "value"]).exec(client); - const updatedSync2 = client._upstashSyncToken; - console.log("updatedSync", updatedSync); - console.log("updatedSync2", updatedSync2); + expect(initialSync).not.toEqual(updatedSync); }); test("updates after each element of promise.all", async () => { - const client = newHttpClient(); + let currentSync = client._upstashSyncToken; const promises = Array.from({ length: 3 }, (_, i) => @@ -68,8 +57,6 @@ describe("Read Your Writes", () => { return 1 ` - const client = newHttpClient(); - await new SetCommand(["key", "value"]).exec(client); const initialSync = client._upstashSyncToken; const redis = new Redis(client); @@ -81,6 +68,5 @@ describe("Read Your Writes", () => { expect(updatedSync).not.toEqual(initialSync); - }) }); From eed34dfb67e46183638f8e580f3d8c83ac3b1ad9 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 09:26:15 +0300 Subject: [PATCH 07/20] fmt --- pkg/read-your-writes.test.ts | 81 +++++++++++++++++------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index 67fd9def..303c213e 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -9,64 +9,59 @@ const client = newHttpClient(); const { cleanup } = keygen(); afterAll(cleanup); describe("Read Your Writes Feature", () => { - test("successfully retrieves Upstash-Sync-Token in the response header and updates local state", async () => { + test("successfully retrieves Upstash-Sync-Token in the response header and updates local state", async () => { + const initialSync = client._upstashSyncToken; + await new SetCommand(["key", "value"]).exec(client); + const updatedSync = client._upstashSyncToken; + await new SetCommand(["key", "value"]).exec(client); - const initialSync = client._upstashSyncToken; - await new SetCommand(["key", "value"]).exec(client); - const updatedSync = client._upstashSyncToken; - await new SetCommand(["key", "value"]).exec(client); + expect(updatedSync).not.toEqual(initialSync); + }); - expect(updatedSync).not.toEqual(initialSync); - }); + test("succesfully updates sync state with pipeline", async () => { + const initialSync = client._upstashSyncToken; - test("succesfully updates sync state with pipeline", async () => { - const initialSync = client._upstashSyncToken; + const { pipeline } = new Redis(client); + const p = pipeline(); - const { pipeline } = new Redis(client); - const p = pipeline(); + p.set("key1", "value1"); + p.set("key2", "value2"); + p.set("key3", "value3"); - p.set("key1", "value1"); - p.set("key2", "value2"); - p.set("key3", "value3"); + await p.exec(); - await p.exec(); + const updatedSync = client._upstashSyncToken; - const updatedSync = client._upstashSyncToken; + expect(initialSync).not.toEqual(updatedSync); + }); + test("updates after each element of promise.all", async () => { + let currentSync = client._upstashSyncToken; - expect(initialSync).not.toEqual(updatedSync); - }); + const promises = Array.from({ length: 3 }, (_, i) => + new SetCommand([`key${i}`, `value${i}`]).exec(client).then(() => { + expect(client._upstashSyncToken).not.toEqual(currentSync); + currentSync = client._upstashSyncToken; + }), + ); - test("updates after each element of promise.all", async () => { + await Promise.all(promises); + }); - let currentSync = client._upstashSyncToken; - - const promises = Array.from({ length: 3 }, (_, i) => - new SetCommand([`key${i}`, `value${i}`]).exec(client).then(() => { - expect(client._upstashSyncToken).not.toEqual(currentSync); - currentSync = client._upstashSyncToken; - }), - ); - - await Promise.all(promises); - }); - - test("updates after successful lua script call", async () => { - - const s = `redis.call('SET', 'mykey', 'myvalue') + test("updates after successful lua script call", async () => { + const s = `redis.call('SET', 'mykey', 'myvalue') return 1 - ` - - const initialSync = client._upstashSyncToken; + `; - const redis = new Redis(client); - const script = redis.createScript(s); + const initialSync = client._upstashSyncToken; - await script.exec([], []); + const redis = new Redis(client); + const script = redis.createScript(s); - const updatedSync = client._upstashSyncToken; + await script.exec([], []); - expect(updatedSync).not.toEqual(initialSync); + const updatedSync = client._upstashSyncToken; - }) + expect(updatedSync).not.toEqual(initialSync); + }); }); From 74c005cf036fd96c9ce66541ece5826346c7c41b Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 09:31:49 +0300 Subject: [PATCH 08/20] change upstashSyncToken convention --- pkg/http.ts | 39 ++++++++++++++++++------------------ pkg/read-your-writes.test.ts | 18 ++++++++--------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/pkg/http.ts b/pkg/http.ts index 199586c7..26468612 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -32,22 +32,22 @@ type ResultError = { export type RetryConfig = | false | { - /** - * The number of retries to attempt before giving up. - * - * @default 5 - */ - retries?: number; - /** - * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. - * - * @default - * ```ts - * Math.exp(retryCount) * 50 - * ``` - */ - backoff?: (retryCount: number) => number; - }; + /** + * The number of retries to attempt before giving up. + * + * @default 5 + */ + retries?: number; + /** + * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. + * + * @default + * ```ts + * Math.exp(retryCount) * 50 + * ``` + */ + backoff?: (retryCount: number) => number; + }; export type Options = { backend?: string; @@ -113,13 +113,12 @@ export class HttpClient implements Requester { keepAlive: boolean; readYourWrites?: boolean; }; - public _upstashSyncToken = ""; + public upstashSyncToken = ""; public readonly retry: { attempts: number; backoff: (retryCount: number) => number; }; - public upstashSyncToken: string; public constructor(config: HttpClientConfig) { this.options = { @@ -212,7 +211,7 @@ export class HttpClient implements Requester { backend: this.options?.backend, }; - const newHeader = this._upstashSyncToken; + const newHeader = this.upstashSyncToken; this.headers["upstash-sync-token"] = newHeader; let res: Response | null = null; @@ -247,7 +246,7 @@ export class HttpClient implements Requester { } const headers = res.headers; - this._upstashSyncToken = headers.get("upstash-sync-token") ?? ""; + this.upstashSyncToken = headers.get("upstash-sync-token") ?? ""; if (this.options?.responseEncoding === "base64") { if (Array.isArray(body)) { diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index 303c213e..d4a6200e 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -10,16 +10,16 @@ const { cleanup } = keygen(); afterAll(cleanup); describe("Read Your Writes Feature", () => { test("successfully retrieves Upstash-Sync-Token in the response header and updates local state", async () => { - const initialSync = client._upstashSyncToken; + const initialSync = client.upstashSyncToken; await new SetCommand(["key", "value"]).exec(client); - const updatedSync = client._upstashSyncToken; + const updatedSync = client.upstashSyncToken; await new SetCommand(["key", "value"]).exec(client); expect(updatedSync).not.toEqual(initialSync); }); test("succesfully updates sync state with pipeline", async () => { - const initialSync = client._upstashSyncToken; + const initialSync = client.upstashSyncToken; const { pipeline } = new Redis(client); const p = pipeline(); @@ -30,18 +30,18 @@ describe("Read Your Writes Feature", () => { await p.exec(); - const updatedSync = client._upstashSyncToken; + const updatedSync = client.upstashSyncToken; expect(initialSync).not.toEqual(updatedSync); }); test("updates after each element of promise.all", async () => { - let currentSync = client._upstashSyncToken; + let currentSync = client.upstashSyncToken; const promises = Array.from({ length: 3 }, (_, i) => new SetCommand([`key${i}`, `value${i}`]).exec(client).then(() => { - expect(client._upstashSyncToken).not.toEqual(currentSync); - currentSync = client._upstashSyncToken; + expect(client.upstashSyncToken).not.toEqual(currentSync); + currentSync = client.upstashSyncToken; }), ); @@ -53,14 +53,14 @@ describe("Read Your Writes Feature", () => { return 1 `; - const initialSync = client._upstashSyncToken; + const initialSync = client.upstashSyncToken; const redis = new Redis(client); const script = redis.createScript(s); await script.exec([], []); - const updatedSync = client._upstashSyncToken; + const updatedSync = client.upstashSyncToken; expect(updatedSync).not.toEqual(initialSync); }); From 5e8630442a141edb461f5e665e6e7f7383b7fd15 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 10:08:01 +0300 Subject: [PATCH 09/20] add public redis client test --- pkg/http.ts | 23 ++++++++++++++------ pkg/read-your-writes.test.ts | 28 ++++++++++++++++++++++++ pkg/redis.ts | 41 +++++++++++++++++++----------------- platforms/nodejs.ts | 6 ++++-- 4 files changed, 70 insertions(+), 28 deletions(-) diff --git a/pkg/http.ts b/pkg/http.ts index 26468612..fd6028a0 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -21,6 +21,7 @@ export type UpstashRequest = { export type UpstashResponse = { result?: TResult; error?: string }; export interface Requester { + readYourWrites: boolean; upstashSyncToken: string; request: (req: UpstashRequest) => Promise>; } @@ -104,6 +105,7 @@ export type HttpClientConfig = { export class HttpClient implements Requester { public baseUrl: string; public headers: Record; + public readonly options: { backend?: string; agent: any; @@ -111,10 +113,12 @@ export class HttpClient implements Requester { responseEncoding?: false | "base64"; cache?: CacheSetting; keepAlive: boolean; - readYourWrites?: boolean; + }; + public readYourWrites = true; public upstashSyncToken = ""; + public readonly retry: { attempts: number; backoff: (retryCount: number) => number; @@ -128,9 +132,10 @@ export class HttpClient implements Requester { cache: config.cache, signal: config.signal, keepAlive: config.keepAlive ?? true, - readYourWrites: config.readYourWrites, + }; this.upstashSyncToken = ""; + this.readYourWrites = config.readYourWrites ?? true; this.baseUrl = config.baseUrl.replace(/\/$/, ""); @@ -203,7 +208,6 @@ export class HttpClient implements Requester { keepalive: this.options.keepAlive, agent: this.options?.agent, signal: this.options.signal, - readYourWrites: this.options.readYourWrites, /** * Fastly specific @@ -211,8 +215,11 @@ export class HttpClient implements Requester { backend: this.options?.backend, }; - const newHeader = this.upstashSyncToken; - this.headers["upstash-sync-token"] = newHeader; + if (this.readYourWrites) { + const newHeader = this.upstashSyncToken; + this.headers["upstash-sync-token"] = newHeader; + } + let res: Response | null = null; let error: Error | null = null; @@ -245,8 +252,10 @@ export class HttpClient implements Requester { throw new UpstashError(`${body.error}, command was: ${JSON.stringify(req.body)}`); } - const headers = res.headers; - this.upstashSyncToken = headers.get("upstash-sync-token") ?? ""; + if (this.readYourWrites) { + const headers = res.headers; + this.upstashSyncToken = headers.get("upstash-sync-token") ?? ""; + } if (this.options?.responseEncoding === "base64") { if (Array.isArray(body)) { diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index d4a6200e..bde72740 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -4,6 +4,7 @@ import { afterAll, describe, expect, test } from "bun:test"; import { SetCommand } from "./commands/set"; import { Redis } from "./redis"; +import { Redis as PublicRedis } from "../platforms/nodejs" const client = newHttpClient(); const { cleanup } = keygen(); @@ -64,4 +65,31 @@ describe("Read Your Writes Feature", () => { expect(updatedSync).not.toEqual(initialSync); }); + + test("should not update the sync state in case of Redis client with manuel HTTP client and opt-out ryw", async () => { + const optOutClient = newHttpClient() + const redis = new Redis(optOutClient, { readYourWrites: false }); + + const initialSync = optOutClient.upstashSyncToken; + + await redis.set('key', 'value'); + + const updatedSync = optOutClient.upstashSyncToken; + + expect(updatedSync).toEqual(initialSync); + }); + + test("should not update the sync state when public Redis interface is provided", async () => { + const redis = new PublicRedis({ url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN, readYourWrites: false }); + + // @ts-expect-error - We need the sync token for this test, which resides on the client + const initialSync = redis.client.upstashSyncToken; + + await redis.set('key', 'value'); + + // @ts-expect-error - We need the sync token for this test, which resides on the client + const updatedSync = redis.client.upstashSyncToken; + + expect(updatedSync).toEqual(initialSync); + }) }); diff --git a/pkg/redis.ts b/pkg/redis.ts index d3e44890..1744eea5 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -191,7 +191,6 @@ export class Redis { protected opts?: CommandOptions; protected enableTelemetry: boolean; protected enableAutoPipelining: boolean; - protected readYourWrites: boolean; /** * Create a new redis client @@ -209,7 +208,11 @@ export class Redis { this.opts = opts; this.enableTelemetry = opts?.enableTelemetry ?? true; this.enableAutoPipelining = opts?.enableAutoPipelining ?? false; - this.readYourWrites = opts?.readYourWrites ?? true; + // this.readYourWrites = opts?.readYourWrites ?? true; + if (opts?.readYourWrites === false) { + this.client.readYourWrites = false + } + } get json() { @@ -452,9 +455,9 @@ export class Redis { sourceKey: string, ...sourceKeys: string[] ) => - new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( - this.client, - ); + new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( + this.client, + ); /** * @see https://redis.io/commands/bitpos @@ -1203,10 +1206,10 @@ export class Redis { ...args: | [key: string, scoreMember: ScoreMember, ...scoreMemberPairs: ScoreMember[]] | [ - key: string, - opts: ZAddCommandOptions, - ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], - ] + key: string, + opts: ZAddCommandOptions, + ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], + ] ) => { if ("score" in args[1]) { return new ZAddCommand( @@ -1281,17 +1284,17 @@ export class Redis { ...args: | [key: string, min: number, max: number, opts?: ZRangeCommandOptions] | [ - key: string, - min: `(${string}` | `[${string}` | "-" | "+", - max: `(${string}` | `[${string}` | "-" | "+", - opts: { byLex: true } & ZRangeCommandOptions, - ] + key: string, + min: `(${string}` | `[${string}` | "-" | "+", + max: `(${string}` | `[${string}` | "-" | "+", + opts: { byLex: true } & ZRangeCommandOptions, + ] | [ - key: string, - min: number | `(${number}` | "-inf" | "+inf", - max: number | `(${number}` | "-inf" | "+inf", - opts: { byScore: true } & ZRangeCommandOptions, - ] + key: string, + min: number | `(${number}` | "-inf" | "+inf", + max: number | `(${number}` | "-inf" | "+inf", + opts: { byScore: true } & ZRangeCommandOptions, + ] ) => new ZRangeCommand(args as any, this.opts).exec(this.client); /** diff --git a/platforms/nodejs.ts b/platforms/nodejs.ts index 7bf7d8a1..c75cd681 100644 --- a/platforms/nodejs.ts +++ b/platforms/nodejs.ts @@ -58,6 +58,7 @@ export type RedisConfigNodejs = { latencyLogging?: boolean; agent?: any; keepAlive?: boolean; + readYourWrites?: boolean; } & core.RedisOptions & RequesterConfig; @@ -102,11 +103,11 @@ export class Redis extends core.Redis { return; } - if(!configOrRequester.url) { + if (!configOrRequester.url) { throw new Error(`[Upstash Redis] The 'url' property is missing or undefined in your Redis config.`) } - if(!configOrRequester.token) { + if (!configOrRequester.token) { throw new Error(`[Upstash Redis] The 'token' property is missing or undefined in your Redis config.`) } @@ -134,6 +135,7 @@ export class Redis extends core.Redis { cache: configOrRequester.cache || "no-store", signal: configOrRequester.signal, keepAlive: configOrRequester.keepAlive, + readYourWrites: configOrRequester.readYourWrites, }); super(client, { From d84dfed0950ed7d9b0a316ef4a39da9f6637e25e Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 10:10:50 +0300 Subject: [PATCH 10/20] add: fastly and cloudflare clients ryw support --- pkg/http.ts | 2 +- platforms/cloudflare.ts | 6 ++++-- platforms/fastly.ts | 6 ++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/http.ts b/pkg/http.ts index fd6028a0..561f9628 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -115,7 +115,7 @@ export class HttpClient implements Requester { keepAlive: boolean; }; - public readYourWrites = true; + public readYourWrites: boolean; public upstashSyncToken = ""; diff --git a/platforms/cloudflare.ts b/platforms/cloudflare.ts index 35881664..9128ec5c 100644 --- a/platforms/cloudflare.ts +++ b/platforms/cloudflare.ts @@ -30,6 +30,7 @@ export type RedisConfigCloudflare = { */ signal?: AbortSignal; keepAlive?: boolean; + readYourWrites?: boolean; } & core.RedisOptions & RequesterConfig & Env; @@ -50,11 +51,11 @@ export class Redis extends core.Redis { * ``` */ constructor(config: RedisConfigCloudflare, env?: Env) { - if(!config.url) { + if (!config.url) { throw new Error(`[Upstash Redis] The 'url' property is missing or undefined in your Redis config.`) } - if(!config.token) { + if (!config.token) { throw new Error(`[Upstash Redis] The 'token' property is missing or undefined in your Redis config.`) } @@ -72,6 +73,7 @@ export class Redis extends core.Redis { responseEncoding: config.responseEncoding, signal: config.signal, keepAlive: config.keepAlive, + readYourWrites: config.readYourWrites, }); super(client, { diff --git a/platforms/fastly.ts b/platforms/fastly.ts index 6dcef11a..265fbdab 100644 --- a/platforms/fastly.ts +++ b/platforms/fastly.ts @@ -28,6 +28,7 @@ export type RedisConfigFastly = { */ backend: string; keepAlive?: boolean; + readYourWrites?: boolean; } & core.RedisOptions & RequesterConfig; @@ -48,11 +49,11 @@ export class Redis extends core.Redis { * ``` */ constructor(config: RedisConfigFastly) { - if(!config.url) { + if (!config.url) { throw new Error(`[Upstash Redis] The 'url' property is missing or undefined in your Redis config.`) } - if(!config.token) { + if (!config.token) { throw new Error(`[Upstash Redis] The 'token' property is missing or undefined in your Redis config.`) } @@ -70,6 +71,7 @@ export class Redis extends core.Redis { options: { backend: config.backend }, responseEncoding: config.responseEncoding, keepAlive: config.keepAlive, + readYourWrites: config.readYourWrites, }); super(client, { From e1fc32a80b89b67f90af0edf72cce60508c8b4cb Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 10:10:59 +0300 Subject: [PATCH 11/20] fmt --- pkg/http.ts | 36 ++++++++++++++++------------------- pkg/read-your-writes.test.ts | 16 ++++++++++------ pkg/redis.ts | 37 ++++++++++++++++++------------------ 3 files changed, 44 insertions(+), 45 deletions(-) diff --git a/pkg/http.ts b/pkg/http.ts index 561f9628..9deb8d5d 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -33,22 +33,22 @@ type ResultError = { export type RetryConfig = | false | { - /** - * The number of retries to attempt before giving up. - * - * @default 5 - */ - retries?: number; - /** - * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. - * - * @default - * ```ts - * Math.exp(retryCount) * 50 - * ``` - */ - backoff?: (retryCount: number) => number; - }; + /** + * The number of retries to attempt before giving up. + * + * @default 5 + */ + retries?: number; + /** + * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. + * + * @default + * ```ts + * Math.exp(retryCount) * 50 + * ``` + */ + backoff?: (retryCount: number) => number; + }; export type Options = { backend?: string; @@ -113,12 +113,10 @@ export class HttpClient implements Requester { responseEncoding?: false | "base64"; cache?: CacheSetting; keepAlive: boolean; - }; public readYourWrites: boolean; public upstashSyncToken = ""; - public readonly retry: { attempts: number; backoff: (retryCount: number) => number; @@ -132,7 +130,6 @@ export class HttpClient implements Requester { cache: config.cache, signal: config.signal, keepAlive: config.keepAlive ?? true, - }; this.upstashSyncToken = ""; this.readYourWrites = config.readYourWrites ?? true; @@ -220,7 +217,6 @@ export class HttpClient implements Requester { this.headers["upstash-sync-token"] = newHeader; } - let res: Response | null = null; let error: Error | null = null; for (let i = 0; i <= this.retry.attempts; i++) { diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index bde72740..aec90e2f 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -2,9 +2,9 @@ import { keygen, newHttpClient } from "./test-utils"; import { afterAll, describe, expect, test } from "bun:test"; +import { Redis as PublicRedis } from "../platforms/nodejs"; import { SetCommand } from "./commands/set"; import { Redis } from "./redis"; -import { Redis as PublicRedis } from "../platforms/nodejs" const client = newHttpClient(); const { cleanup } = keygen(); @@ -67,12 +67,12 @@ describe("Read Your Writes Feature", () => { }); test("should not update the sync state in case of Redis client with manuel HTTP client and opt-out ryw", async () => { - const optOutClient = newHttpClient() + const optOutClient = newHttpClient(); const redis = new Redis(optOutClient, { readYourWrites: false }); const initialSync = optOutClient.upstashSyncToken; - await redis.set('key', 'value'); + await redis.set("key", "value"); const updatedSync = optOutClient.upstashSyncToken; @@ -80,16 +80,20 @@ describe("Read Your Writes Feature", () => { }); test("should not update the sync state when public Redis interface is provided", async () => { - const redis = new PublicRedis({ url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN, readYourWrites: false }); + const redis = new PublicRedis({ + url: process.env.UPSTASH_REDIS_REST_URL, + token: process.env.UPSTASH_REDIS_REST_TOKEN, + readYourWrites: false, + }); // @ts-expect-error - We need the sync token for this test, which resides on the client const initialSync = redis.client.upstashSyncToken; - await redis.set('key', 'value'); + await redis.set("key", "value"); // @ts-expect-error - We need the sync token for this test, which resides on the client const updatedSync = redis.client.upstashSyncToken; expect(updatedSync).toEqual(initialSync); - }) + }); }); diff --git a/pkg/redis.ts b/pkg/redis.ts index 1744eea5..767b2909 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -210,9 +210,8 @@ export class Redis { this.enableAutoPipelining = opts?.enableAutoPipelining ?? false; // this.readYourWrites = opts?.readYourWrites ?? true; if (opts?.readYourWrites === false) { - this.client.readYourWrites = false + this.client.readYourWrites = false; } - } get json() { @@ -455,9 +454,9 @@ export class Redis { sourceKey: string, ...sourceKeys: string[] ) => - new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( - this.client, - ); + new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( + this.client, + ); /** * @see https://redis.io/commands/bitpos @@ -1206,10 +1205,10 @@ export class Redis { ...args: | [key: string, scoreMember: ScoreMember, ...scoreMemberPairs: ScoreMember[]] | [ - key: string, - opts: ZAddCommandOptions, - ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], - ] + key: string, + opts: ZAddCommandOptions, + ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], + ] ) => { if ("score" in args[1]) { return new ZAddCommand( @@ -1284,17 +1283,17 @@ export class Redis { ...args: | [key: string, min: number, max: number, opts?: ZRangeCommandOptions] | [ - key: string, - min: `(${string}` | `[${string}` | "-" | "+", - max: `(${string}` | `[${string}` | "-" | "+", - opts: { byLex: true } & ZRangeCommandOptions, - ] + key: string, + min: `(${string}` | `[${string}` | "-" | "+", + max: `(${string}` | `[${string}` | "-" | "+", + opts: { byLex: true } & ZRangeCommandOptions, + ] | [ - key: string, - min: number | `(${number}` | "-inf" | "+inf", - max: number | `(${number}` | "-inf" | "+inf", - opts: { byScore: true } & ZRangeCommandOptions, - ] + key: string, + min: number | `(${number}` | "-inf" | "+inf", + max: number | `(${number}` | "-inf" | "+inf", + opts: { byScore: true } & ZRangeCommandOptions, + ] ) => new ZRangeCommand(args as any, this.opts).exec(this.client); /** From d6b3307ace12bc72cfa3e2fbd9f19352ea4644b1 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 10:17:25 +0300 Subject: [PATCH 12/20] add default test --- pkg/read-your-writes.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index aec90e2f..ef274acb 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -79,7 +79,7 @@ describe("Read Your Writes Feature", () => { expect(updatedSync).toEqual(initialSync); }); - test("should not update the sync state when public Redis interface is provided", async () => { + test("should not update the sync state when public Redis interface is provided with opt-out", async () => { const redis = new PublicRedis({ url: process.env.UPSTASH_REDIS_REST_URL, token: process.env.UPSTASH_REDIS_REST_TOKEN, @@ -96,4 +96,21 @@ describe("Read Your Writes Feature", () => { expect(updatedSync).toEqual(initialSync); }); + + test("should update the sync state when public Redis interface is provided with default behaviour", async () => { + const redis = new PublicRedis({ + url: process.env.UPSTASH_REDIS_REST_URL, + token: process.env.UPSTASH_REDIS_REST_TOKEN, + }); + + // @ts-expect-error - We need the sync token for this test, which resides on the client + const initialSync = redis.client.upstashSyncToken; + + await redis.set("key", "value"); + + // @ts-expect-error - We need the sync token for this test, which resides on the client + const updatedSync = redis.client.upstashSyncToken; + expect(updatedSync).not.toEqual(initialSync); + }) + }); From 4eb9e4d7ae57eaf8f08a5388e3f9e2bed64b22e2 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 10:19:45 +0300 Subject: [PATCH 13/20] add: comments --- platforms/cloudflare.ts | 4 ++++ platforms/fastly.ts | 4 ++++ platforms/nodejs.ts | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/platforms/cloudflare.ts b/platforms/cloudflare.ts index 9128ec5c..0d690643 100644 --- a/platforms/cloudflare.ts +++ b/platforms/cloudflare.ts @@ -30,6 +30,10 @@ export type RedisConfigCloudflare = { */ signal?: AbortSignal; keepAlive?: boolean; + + /** + * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. + */ readYourWrites?: boolean; } & core.RedisOptions & RequesterConfig & diff --git a/platforms/fastly.ts b/platforms/fastly.ts index 265fbdab..08d4d91e 100644 --- a/platforms/fastly.ts +++ b/platforms/fastly.ts @@ -28,6 +28,10 @@ export type RedisConfigFastly = { */ backend: string; keepAlive?: boolean; + + /** + * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. + */ readYourWrites?: boolean; } & core.RedisOptions & RequesterConfig; diff --git a/platforms/nodejs.ts b/platforms/nodejs.ts index c75cd681..22fbf692 100644 --- a/platforms/nodejs.ts +++ b/platforms/nodejs.ts @@ -58,6 +58,10 @@ export type RedisConfigNodejs = { latencyLogging?: boolean; agent?: any; keepAlive?: boolean; + + /** + * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. + */ readYourWrites?: boolean; } & core.RedisOptions & RequesterConfig; From b563e66ef6ae572b6d85ce95a5bba44b931678ab Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 10:20:45 +0300 Subject: [PATCH 14/20] add: http comment --- pkg/http.ts | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/pkg/http.ts b/pkg/http.ts index 9deb8d5d..c0859130 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -21,6 +21,10 @@ export type UpstashRequest = { export type UpstashResponse = { result?: TResult; error?: string }; export interface Requester { + /** + * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. + */ + readYourWrites: boolean; upstashSyncToken: string; request: (req: UpstashRequest) => Promise>; @@ -33,22 +37,22 @@ type ResultError = { export type RetryConfig = | false | { - /** - * The number of retries to attempt before giving up. - * - * @default 5 - */ - retries?: number; - /** - * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. - * - * @default - * ```ts - * Math.exp(retryCount) * 50 - * ``` - */ - backoff?: (retryCount: number) => number; - }; + /** + * The number of retries to attempt before giving up. + * + * @default 5 + */ + retries?: number; + /** + * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. + * + * @default + * ```ts + * Math.exp(retryCount) * 50 + * ``` + */ + backoff?: (retryCount: number) => number; + }; export type Options = { backend?: string; @@ -99,6 +103,10 @@ export type HttpClientConfig = { agent?: any; signal?: AbortSignal; keepAlive?: boolean; + + /** + * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. + */ readYourWrites?: boolean; } & RequesterConfig; From b21bff1135ef38b77f5aab7e212a6b6b8b3a040a Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 10:22:13 +0300 Subject: [PATCH 15/20] sync token docs --- pkg/http.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/http.ts b/pkg/http.ts index c0859130..a4ed3d71 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -24,8 +24,11 @@ export interface Requester { /** * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. */ - readYourWrites: boolean; + + /** + * This token is used to ensure that the client is in sync with the server. On each request, we send this token in the header, and the server will return a new token. + */ upstashSyncToken: string; request: (req: UpstashRequest) => Promise>; } From 5be9815467c4b5d9cefa0787b87390c4e1733afa Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Wed, 10 Jul 2024 10:23:29 +0300 Subject: [PATCH 16/20] remove comment --- pkg/redis.ts | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/pkg/redis.ts b/pkg/redis.ts index 767b2909..85feaa5b 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -208,7 +208,6 @@ export class Redis { this.opts = opts; this.enableTelemetry = opts?.enableTelemetry ?? true; this.enableAutoPipelining = opts?.enableAutoPipelining ?? false; - // this.readYourWrites = opts?.readYourWrites ?? true; if (opts?.readYourWrites === false) { this.client.readYourWrites = false; } @@ -454,9 +453,9 @@ export class Redis { sourceKey: string, ...sourceKeys: string[] ) => - new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( - this.client, - ); + new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( + this.client, + ); /** * @see https://redis.io/commands/bitpos @@ -1205,10 +1204,10 @@ export class Redis { ...args: | [key: string, scoreMember: ScoreMember, ...scoreMemberPairs: ScoreMember[]] | [ - key: string, - opts: ZAddCommandOptions, - ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], - ] + key: string, + opts: ZAddCommandOptions, + ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], + ] ) => { if ("score" in args[1]) { return new ZAddCommand( @@ -1283,17 +1282,17 @@ export class Redis { ...args: | [key: string, min: number, max: number, opts?: ZRangeCommandOptions] | [ - key: string, - min: `(${string}` | `[${string}` | "-" | "+", - max: `(${string}` | `[${string}` | "-" | "+", - opts: { byLex: true } & ZRangeCommandOptions, - ] + key: string, + min: `(${string}` | `[${string}` | "-" | "+", + max: `(${string}` | `[${string}` | "-" | "+", + opts: { byLex: true } & ZRangeCommandOptions, + ] | [ - key: string, - min: number | `(${number}` | "-inf" | "+inf", - max: number | `(${number}` | "-inf" | "+inf", - opts: { byScore: true } & ZRangeCommandOptions, - ] + key: string, + min: number | `(${number}` | "-inf" | "+inf", + max: number | `(${number}` | "-inf" | "+inf", + opts: { byScore: true } & ZRangeCommandOptions, + ] ) => new ZRangeCommand(args as any, this.opts).exec(this.client); /** From 14fd82cb5049f06bab3cd95df7efd72d1f570857 Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Mon, 22 Jul 2024 14:50:19 +0200 Subject: [PATCH 17/20] fix readYourWrites arg comment --- pkg/http.ts | 38 ++++++++++++++++++------------------ pkg/read-your-writes.test.ts | 3 +-- pkg/redis.ts | 34 ++++++++++++++++---------------- platforms/cloudflare.ts | 2 +- platforms/fastly.ts | 2 +- platforms/nodejs.ts | 2 +- 6 files changed, 40 insertions(+), 41 deletions(-) diff --git a/pkg/http.ts b/pkg/http.ts index a4ed3d71..6347e555 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -22,8 +22,8 @@ export type UpstashResponse = { result?: TResult; error?: string }; export interface Requester { /** - * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. - */ + * When this flag is enabled, any subsequent commands issued by this client are guaranteed to observe the effects of all earlier writes submitted by the same client. + */ readYourWrites: boolean; /** @@ -40,22 +40,22 @@ type ResultError = { export type RetryConfig = | false | { - /** - * The number of retries to attempt before giving up. - * - * @default 5 - */ - retries?: number; - /** - * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. - * - * @default - * ```ts - * Math.exp(retryCount) * 50 - * ``` - */ - backoff?: (retryCount: number) => number; - }; + /** + * The number of retries to attempt before giving up. + * + * @default 5 + */ + retries?: number; + /** + * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. + * + * @default + * ```ts + * Math.exp(retryCount) * 50 + * ``` + */ + backoff?: (retryCount: number) => number; + }; export type Options = { backend?: string; @@ -108,7 +108,7 @@ export type HttpClientConfig = { keepAlive?: boolean; /** - * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. + * When this flag is enabled, any subsequent commands issued by this client are guaranteed to observe the effects of all earlier writes submitted by the same client. */ readYourWrites?: boolean; } & RequesterConfig; diff --git a/pkg/read-your-writes.test.ts b/pkg/read-your-writes.test.ts index ef274acb..b8feba9b 100644 --- a/pkg/read-your-writes.test.ts +++ b/pkg/read-your-writes.test.ts @@ -111,6 +111,5 @@ describe("Read Your Writes Feature", () => { // @ts-expect-error - We need the sync token for this test, which resides on the client const updatedSync = redis.client.upstashSyncToken; expect(updatedSync).not.toEqual(initialSync); - }) - + }); }); diff --git a/pkg/redis.ts b/pkg/redis.ts index 85feaa5b..45bb6cc1 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -453,9 +453,9 @@ export class Redis { sourceKey: string, ...sourceKeys: string[] ) => - new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( - this.client, - ); + new BitOpCommand([op as any, destinationKey, sourceKey, ...sourceKeys], this.opts).exec( + this.client, + ); /** * @see https://redis.io/commands/bitpos @@ -1204,10 +1204,10 @@ export class Redis { ...args: | [key: string, scoreMember: ScoreMember, ...scoreMemberPairs: ScoreMember[]] | [ - key: string, - opts: ZAddCommandOptions, - ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], - ] + key: string, + opts: ZAddCommandOptions, + ...scoreMemberPairs: [ScoreMember, ...ScoreMember[]], + ] ) => { if ("score" in args[1]) { return new ZAddCommand( @@ -1282,17 +1282,17 @@ export class Redis { ...args: | [key: string, min: number, max: number, opts?: ZRangeCommandOptions] | [ - key: string, - min: `(${string}` | `[${string}` | "-" | "+", - max: `(${string}` | `[${string}` | "-" | "+", - opts: { byLex: true } & ZRangeCommandOptions, - ] + key: string, + min: `(${string}` | `[${string}` | "-" | "+", + max: `(${string}` | `[${string}` | "-" | "+", + opts: { byLex: true } & ZRangeCommandOptions, + ] | [ - key: string, - min: number | `(${number}` | "-inf" | "+inf", - max: number | `(${number}` | "-inf" | "+inf", - opts: { byScore: true } & ZRangeCommandOptions, - ] + key: string, + min: number | `(${number}` | "-inf" | "+inf", + max: number | `(${number}` | "-inf" | "+inf", + opts: { byScore: true } & ZRangeCommandOptions, + ] ) => new ZRangeCommand(args as any, this.opts).exec(this.client); /** diff --git a/platforms/cloudflare.ts b/platforms/cloudflare.ts index 0d690643..c09bd68c 100644 --- a/platforms/cloudflare.ts +++ b/platforms/cloudflare.ts @@ -32,7 +32,7 @@ export type RedisConfigCloudflare = { keepAlive?: boolean; /** - * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. + * When this flag is enabled, any subsequent commands issued by this client are guaranteed to observe the effects of all earlier writes submitted by the same client. */ readYourWrites?: boolean; } & core.RedisOptions & diff --git a/platforms/fastly.ts b/platforms/fastly.ts index 08d4d91e..55069005 100644 --- a/platforms/fastly.ts +++ b/platforms/fastly.ts @@ -30,7 +30,7 @@ export type RedisConfigFastly = { keepAlive?: boolean; /** - * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. + * When this flag is enabled, any subsequent commands issued by this client are guaranteed to observe the effects of all earlier writes submitted by the same client. */ readYourWrites?: boolean; } & core.RedisOptions & diff --git a/platforms/nodejs.ts b/platforms/nodejs.ts index 22fbf692..cb3d454e 100644 --- a/platforms/nodejs.ts +++ b/platforms/nodejs.ts @@ -60,7 +60,7 @@ export type RedisConfigNodejs = { keepAlive?: boolean; /** - * When this flag is not disabled, new commands of this client expects the previous write commands to be finalized before executing. + * When this flag is enabled, any subsequent commands issued by this client are guaranteed to observe the effects of all earlier writes submitted by the same client. */ readYourWrites?: boolean; } & core.RedisOptions & From f3497aac795c2d131b4b8a644bdae3dd2c82196f Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Thu, 25 Jul 2024 20:00:49 +0200 Subject: [PATCH 18/20] add: ryw operation comments --- pkg/http.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/http.ts b/pkg/http.ts index b34629f7..4fe18832 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -206,6 +206,9 @@ export class HttpClient implements Requester { backend: this.options.backend, }; + /** + * We've recieved a new `upstash-sync-token` in the previous response. We use it in the next request to observe the effects of previous requests. + */ if (this.readYourWrites) { const newHeader = this.upstashSyncToken; this.headers["upstash-sync-token"] = newHeader; @@ -247,6 +250,10 @@ export class HttpClient implements Requester { this.upstashSyncToken = headers.get("upstash-sync-token") ?? ""; } + + /** + * We save the new `upstash-sync-token` in the response header to use it in the next request. + */ if (this.readYourWrites) { const headers = res.headers; this.upstashSyncToken = headers.get("upstash-sync-token") ?? ""; From 733290970dab7e121e4e60995719cf713ea7245b Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Tue, 30 Jul 2024 10:31:56 +0200 Subject: [PATCH 19/20] revert requester --- pkg/http.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/http.ts b/pkg/http.ts index 4fe18832..053dcaca 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -20,16 +20,7 @@ export type UpstashRequest = { }; export type UpstashResponse = { result?: TResult; error?: string }; -export interface Requester { - /** - * When this flag is enabled, any subsequent commands issued by this client are guaranteed to observe the effects of all earlier writes submitted by the same client. - */ - readYourWrites: boolean; - - /** - * This token is used to ensure that the client is in sync with the server. On each request, we send this token in the header, and the server will return a new token. - */ - upstashSyncToken: string; +export type Requester = { request: (req: UpstashRequest) => Promise>; }; From 60233cb9d6cab7a96d7e9fee6102ddaada7531aa Mon Sep 17 00:00:00 2001 From: fahreddinozcan Date: Tue, 30 Jul 2024 10:34:26 +0200 Subject: [PATCH 20/20] revert requester interface --- pkg/http.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/http.ts b/pkg/http.ts index 053dcaca..7dd5a73a 100644 --- a/pkg/http.ts +++ b/pkg/http.ts @@ -20,7 +20,16 @@ export type UpstashRequest = { }; export type UpstashResponse = { result?: TResult; error?: string }; -export type Requester = { +export interface Requester { + /** + * When this flag is enabled, any subsequent commands issued by this client are guaranteed to observe the effects of all earlier writes submitted by the same client. + */ + readYourWrites?: boolean; + + /** + * This token is used to ensure that the client is in sync with the server. On each request, we send this token in the header, and the server will return a new token. + */ + upstashSyncToken?: string; request: (req: UpstashRequest) => Promise>; };