diff --git a/packages/memcache-client/src/lib/client.ts b/packages/memcache-client/src/lib/client.ts index c18b278..21138ab 100644 --- a/packages/memcache-client/src/lib/client.ts +++ b/packages/memcache-client/src/lib/client.ts @@ -17,7 +17,6 @@ import { CommandContext, SingleServerEntry, PackedData, - CompressorLibrary, MultiServerManager, } from "../types"; import { MemcacheConnection } from "./connection"; @@ -33,6 +32,10 @@ type StoreCommandOptions = CommonCommandOption & { ignoreNotStored?: boolean } & compress?: boolean; }>; +type RetrieveCommands = "get" | "gets" | "mg"; +type StoreCommands = "set" | "add" | "replace" | "append" | "prepend" | "cas"; +type Command = RetrieveCommands | StoreCommands; + // Exported for testing export type CasCommandOptions = CommonCommandOption & StoreCommandOptions & @@ -175,7 +178,7 @@ export class MemcacheClient extends EventEmitter { this.options = options; this.socketID = 1; this._packer = new ValuePacker( - options.compressor || (Zstd as CompressorLibrary), + options.compressor || Zstd, options.assumeBuffer || false ); this._logger = options.logger !== undefined ? options.logger : nullLogger; @@ -215,15 +218,10 @@ export class MemcacheClient extends EventEmitter { send( data: StoreParams | SocketCallback, key: string, - options?: CommonCommandOption | ErrorFirstCallback, + options?: CommonCommandOption, callback?: ErrorFirstCallback ): Promise { - if (typeof options === "function") { - callback = options; - options = {}; - } else if (options === undefined) { - options = {}; - } + options = options || {}; return this._callbackSend(data, key, options, callback); } @@ -266,15 +264,14 @@ export class MemcacheClient extends EventEmitter { set( key: string, value: StoreParams, - options?: StoreCommandOptions | OperationCallback, + options?: StoreCommandOptions, callback?: OperationCallback ): Promise { options = options || {}; - if ((options as StoreCommandOptions).ignoreNotStored === undefined) { - (options as StoreCommandOptions).ignoreNotStored = this.options.ignoreNotStored; + if (options.ignoreNotStored === undefined) { + options.ignoreNotStored = this.options.ignoreNotStored; } - // it's tricky to threat optional object as callback - return this.store("set", key, value, options as StoreCommandOptions, callback); + return this.store("set", key, value, options, callback); } // "add" means "store this data, but only if the server *doesn't* already @@ -282,10 +279,10 @@ export class MemcacheClient extends EventEmitter { add( key: string, value: StoreParams, - options?: StoreCommandOptions | OperationCallback, + options?: StoreCommandOptions, callback?: OperationCallback ): Promise { - return this.store("add", key, value, options as StoreCommandOptions, callback); + return this.store("add", key, value, options, callback); } // "replace" means "store this data, but only if the server *does* @@ -293,30 +290,30 @@ export class MemcacheClient extends EventEmitter { replace( key: string, value: StoreParams, - options?: StoreCommandOptions | OperationCallback, + options?: StoreCommandOptions, callback?: OperationCallback ): Promise { - return this.store("replace", key, value, options as StoreCommandOptions, callback); + return this.store("replace", key, value, options, callback); } // "append" means "add this data to an existing key after existing data". append( key: string, value: StoreParams, - options?: StoreCommandOptions | OperationCallback, + options?: StoreCommandOptions, callback?: OperationCallback ): Promise { - return this.store("append", key, value, options as StoreCommandOptions, callback); + return this.store("append", key, value, options, callback); } // "prepend" means "add this data to an existing key before existing data". prepend( key: string, value: StoreParams, - options?: StoreCommandOptions | OperationCallback, + options?: StoreCommandOptions, callback?: OperationCallback ): Promise { - return this.store("prepend", key, value, options as StoreCommandOptions, callback); + return this.store("prepend", key, value, options, callback); } // "cas" is a check and set operation which means "store this data but @@ -337,60 +334,40 @@ export class MemcacheClient extends EventEmitter { // delete key, fire & forget with options.noreply delete( key: string, - options?: CommonCommandOption | OperationCallback, + options?: CommonCommandOption, callback?: OperationCallback ): Promise { - return this.cmd( - `delete ${key}`, - key, - options as CommonCommandOption, - callback - ); + return this.cmd(`delete ${key}`, key, options, callback); } // incr key by value, fire & forget with options.noreply incr( key: string, value: number, - options?: StoreCommandOptions | OperationCallback, + options?: StoreCommandOptions, callback?: OperationCallback ): Promise { - return this.cmd( - `incr ${key} ${value}`, - key, - options as StoreCommandOptions, - callback - ); + return this.cmd(`incr ${key} ${value}`, key, options, callback); } // decrease key by value, fire & forget with options.noreply decr( key: string, value: number, - options?: StoreCommandOptions | OperationCallback, + options?: StoreCommandOptions, callback?: OperationCallback ): Promise { - return this.cmd( - `decr ${key} ${value}`, - key, - options as StoreCommandOptions, - callback - ); + return this.cmd(`decr ${key} ${value}`, key, options, callback); } // touch key with exp time, fire & forget with options.noreply touch( key: string, exptime: string | number, - options?: CommonCommandOption | OperationCallback, + options?: CommonCommandOption, callback?: OperationCallback ): Promise { - return this.cmd( - `touch ${key} ${exptime}`, - key, - options as CommonCommandOption, - callback - ); + return this.cmd(`touch ${key} ${exptime}`, key, options, callback); } // get version of server @@ -401,11 +378,11 @@ export class MemcacheClient extends EventEmitter { // flush all keys from the server, optionally after a delay in seconds flush( exptime?: number, - options?: CommonCommandOption | OperationCallback, + options?: CommonCommandOption, callback?: OperationCallback ): Promise { const cmd = exptime !== undefined ? `flush_all ${exptime}` : "flush_all"; - return this.cmd(cmd, "", options as CommonCommandOption, callback) as unknown as Promise; + return this.cmd(cmd, "", options, callback) as Promise; } async versionAll( @@ -439,18 +416,13 @@ export class MemcacheClient extends EventEmitter { // a generic API for issuing one of the store commands store( - cmd: string, + cmd: StoreCommands, key: string, value: StoreParams, - options?: Partial | OperationCallback, + options?: Partial, callback?: OperationCallback ): Promise { - if (typeof options === "function") { - callback = options; - options = {}; - } else if (options === undefined) { - options = {}; - } + options = options || {}; const lifetime = options.lifetime !== undefined ? options.lifetime : this.options.lifetime || 60; @@ -462,10 +434,7 @@ export class MemcacheClient extends EventEmitter { // [noreply]\r\n // const _data: SocketCallback = (socket?: Socket) => { - const packed = this._packer.pack( - value, - (options as Partial)?.compress === true - ); + const packed = this._packer.pack(value, options.compress === true); const bytes = Buffer.byteLength(packed.data); socket?.write( Buffer.concat([ @@ -476,12 +445,12 @@ export class MemcacheClient extends EventEmitter { ); }; - return this._callbackSend(_data, key, options, callback) as unknown as Promise; + return this._callbackSend(_data, key, options, callback) as Promise; } get( key: string | string[], - options?: StoreCommandOptions | OperationCallback>, + options?: StoreCommandOptions, callback?: OperationCallback> ): Promise> { return this.retrieve("get", key, options, callback); @@ -524,7 +493,7 @@ export class MemcacheClient extends EventEmitter { gets( key: string | string[], - options?: StoreCommandOptions | OperationCallback>, + options?: StoreCommandOptions, callback?: OperationCallback> ): Promise> { return this.retrieve("gets", key, options, callback); @@ -554,22 +523,19 @@ export class MemcacheClient extends EventEmitter { // A generic API for issuing get or gets command retrieve( - cmd: string, + cmd: RetrieveCommands, key: string[] | string, - options?: StoreCommandOptions | OperationCallback, + options?: StoreCommandOptions, callback?: ErrorFirstCallback, metaFlags?: string ): Promise { - if (typeof options === "function") { - callback = options; - options = {}; - } - return nodeify(this.xretrieve(cmd, key, options, metaFlags), callback) as unknown as Promise; + options = options || {}; + return nodeify(this.xretrieve(cmd, key, options, metaFlags), callback) as Promise; } // the promise only version of retrieve xretrieve( - cmd: string, + cmd: RetrieveCommands, key: string | string[], options?: StoreCommandOptions, metaFlags?: string @@ -596,7 +562,7 @@ export class MemcacheClient extends EventEmitter { // retrieve one or more keys from a single server _xretrieverByServer( - cmd: string, + cmd: RetrieveCommands, key: string | string[], options?: StoreCommandOptions, metaFlags?: string @@ -631,7 +597,7 @@ export class MemcacheClient extends EventEmitter { // the promise only version of retrieve that catches errors per-server // instead of failing fast, allowing partial results to be returned async xretrieveWithErrors( - cmd: string, + cmd: RetrieveCommands, keys: Keys[], options?: StoreCommandOptions ): Promise> { diff --git a/packages/memcache-client/src/lib/cmd-actions.ts b/packages/memcache-client/src/lib/cmd-actions.ts index 5ce74d3..490f525 100644 --- a/packages/memcache-client/src/lib/cmd-actions.ts +++ b/packages/memcache-client/src/lib/cmd-actions.ts @@ -1,54 +1,50 @@ -/* eslint-disable no-shadow */ -enum ActionTypes { - ACTION_OK = "OK", - ACTION_ERROR = "ERROR", - ACTION_RESULT = "RESULT", - ACTION_SINGLE_RESULT = "SINGLE_RESULT", - ACTION_SELF = "SELF", -} +export const ActionTypes = ["OK", "ERROR", "RESULT", "SINGLE_RESULT", "SELF"] as const; -const CmdActions: Record = { - OK: ActionTypes.ACTION_OK, - END: ActionTypes.ACTION_SELF, - DELETED: ActionTypes.ACTION_OK, - TOUCHED: ActionTypes.ACTION_OK, - STORED: ActionTypes.ACTION_OK, +export type ActionType = (typeof ActionTypes)[number]; + +const CmdActions = { + OK: "OK", + END: "SELF", + DELETED: "OK", + TOUCHED: "OK", + STORED: "OK", // - VALUE: ActionTypes.ACTION_SELF, - STAT: ActionTypes.ACTION_RESULT, - VERSION: ActionTypes.ACTION_SINGLE_RESULT, + VALUE: "SELF", + STAT: "RESULT", + VERSION: "SINGLE_RESULT", // - NOT_STORED: ActionTypes.ACTION_ERROR, - EXISTS: ActionTypes.ACTION_ERROR, - NOT_FOUND: ActionTypes.ACTION_ERROR, + NOT_STORED: "ERROR", + EXISTS: "ERROR", + NOT_FOUND: "ERROR", // - ERROR: ActionTypes.ACTION_ERROR, - CLIENT_ERROR: ActionTypes.ACTION_ERROR, - SERVER_ERROR: ActionTypes.ACTION_ERROR, + ERROR: "ERROR", + CLIENT_ERROR: "ERROR", + SERVER_ERROR: "ERROR", // Slabs Reassign error responses // - "BUSY [message]" to indicate a page is already being processed, try again // later. // - "BUSY [message]" to indicate the crawler is already processing a request. - BUSY: ActionTypes.ACTION_ERROR, + BUSY: "ERROR", // - "BADCLASS [message]" a bad class id was specified - BADCLASS: ActionTypes.ACTION_ERROR, + BADCLASS: "ERROR", // - "NOSPARE [message]" source class has no spare pages - NOSPARE: ActionTypes.ACTION_ERROR, + NOSPARE: "ERROR", // - "NOTFULL [message]" dest class must be full to move new pages to it - NOTFULL: ActionTypes.ACTION_ERROR, + NOTFULL: "ERROR", // - "UNSAFE [message]" source class cannot move a page right now - UNSAFE: ActionTypes.ACTION_ERROR, + UNSAFE: "ERROR", // - "SAME [message]" must specify different source/dest ids. - SAME: ActionTypes.ACTION_ERROR, + SAME: "ERROR", // Meta Protocol commands uses two letter codes - HD: ActionTypes.ACTION_OK, - VA: ActionTypes.ACTION_SELF, - EN: ActionTypes.ACTION_SELF, - ME: ActionTypes.ACTION_SELF, - NS: ActionTypes.ACTION_ERROR, - EX: ActionTypes.ACTION_ERROR, - NF: ActionTypes.ACTION_ERROR, - MN: ActionTypes.ACTION_SELF, -}; + HD: "OK", + VA: "SELF", + EN: "SELF", + ME: "SELF", + NS: "ERROR", + EX: "ERROR", + NF: "ERROR", + MN: "SELF", +} as const; +export type CommandAction = keyof typeof CmdActions; export default CmdActions; diff --git a/packages/memcache-client/src/lib/connection.ts b/packages/memcache-client/src/lib/connection.ts index 913a164..74aaef2 100644 --- a/packages/memcache-client/src/lib/connection.ts +++ b/packages/memcache-client/src/lib/connection.ts @@ -1,15 +1,11 @@ import Net, { Socket } from "net"; import Tls from "tls"; import assert from "assert"; -import { optionalRequire } from "optional-require"; -const Promise = optionalRequire("bluebird", { - default: global.Promise, -}); import { MemcacheNode } from "./memcache-node"; import { MemcacheParser, ParserPendingData } from "memcache-parser"; import { MemcacheClient, MetaResult } from "./client"; -import cmdActions from "./cmd-actions"; +import cmdActions, { ActionType, CommandAction } from "./cmd-actions"; import defaults from "./defaults"; import { CommandContext, @@ -23,19 +19,14 @@ import { /* eslint-disable no-bitwise,no-magic-numbers,max-params,no-unused-vars */ /* eslint-disable no-console,camelcase,max-statements,no-var */ -export const Status = { - INIT: 1, - CONNECTING: 2, - READY: 3, - SHUTDOWN: 4, -}; +export const Status = [ + "INIT", + "CONNECTING", + "READY", + "SHUTDOWN", +] as const; -const StatusStr = { - [Status.INIT]: "INIT", - [Status.CONNECTING]: "CONNECTING", - [Status.READY]: "READY", - [Status.SHUTDOWN]: "SHUTDOWN", -}; +export type Status = (typeof Status)[number]; type QueueError = Error & { cmdTokens?: string[] }; @@ -47,17 +38,32 @@ export type DangleWaitResponse = { err?: Error; }; export class MemcacheConnection extends MemcacheParser { - node; + node: MemcacheNode | undefined; client?: MemcacheClient; socket: Socket | undefined; _cmdQueue: Array; - _id; - _cmdTimeout; - _connectPromise: ConnectingPromise | undefined | string; - _status; + _id: number; + _cmdTimeout: number; + _connectPromise: ConnectingPromise | undefined; + _status: Status; _reset = false; private _checkCmdTimer: ReturnType | undefined; - private _cmdCheckInterval; + private _cmdCheckInterval: number; + + private readonly cmdActionHandlers: Record void> = { + OK: (t) => this.cmdAction_OK(t), + ERROR: (t) => this.cmdAction_ERROR(t), + RESULT: (t) => this.cmdAction_RESULT(t), + SINGLE_RESULT: (t) => this.cmdAction_SINGLE_RESULT(t), + SELF: (t) => this.cmdAction_SELF(t), + }; + + private readonly selfCmdHandlers: Record void> = { + VALUE: (t) => this.cmd_VALUE(t), + VA: (t) => this.cmd_VA(t), + EN: (t) => this.cmd_EN(t), + END: (t) => this.cmd_END(t), + }; // TODO: still don't know which type client is constructor(client: MemcacheClient, node?: MemcacheNode) { @@ -73,7 +79,7 @@ export class MemcacheConnection extends MemcacheParser { assert(this._cmdTimeout > 0, "cmdTimeout must be > 0"); this._cmdCheckInterval = Math.min(250, Math.ceil(this._cmdTimeout / 4)); this._cmdCheckInterval = Math.max(50, this._cmdCheckInterval); - this._status = Status.INIT; + this._status = "INIT"; } waitDangleSocket(socket?: Socket): void { @@ -117,7 +123,7 @@ export class MemcacheConnection extends MemcacheParser { socket = Net.createConnection({ host, port }); } this._connectPromise = new Promise((resolve: ResolveCallback, reject: RejectCallback) => { - this._status = Status.CONNECTING; + this._status = "CONNECTING"; const selfTimeout = () => { if (!((this.client?.options?.connectTimeout ?? 0) > 0)) return undefined; @@ -157,7 +163,7 @@ export class MemcacheConnection extends MemcacheParser { socket.once("connect", () => { this.socket = socket; - this._status = Status.READY; + this._status = "READY"; this._connectPromise = undefined; socket.removeAllListeners("error"); this._setupConnection(socket); @@ -174,25 +180,25 @@ export class MemcacheConnection extends MemcacheParser { } isReady(): boolean { - return this._status === Status.READY; + return this._status === "READY"; } isConnecting(): boolean { - return this._status === Status.CONNECTING; + return this._status === "CONNECTING"; } isShutdown(): boolean { - return this._status === Status.SHUTDOWN; + return this._status === "SHUTDOWN"; } - getStatusStr(): string { - return StatusStr[this._status] || "UNKNOWN"; + getStatusStr(): Status { + return this._status; } waitReady(): ConnectingPromise { if (this.isConnecting()) { assert(this._connectPromise, "MemcacheConnection not pending connect"); - return this._connectPromise as ConnectingPromise; + return this._connectPromise; } else if (this.isReady()) { return Promise.resolve(this); } else { @@ -208,7 +214,7 @@ export class MemcacheConnection extends MemcacheParser { dequeueCommand(): QueuedCommandContext | undefined { if (this.isShutdown()) { - return { callback: () => undefined } as unknown as QueuedCommandContext; + return undefined; } return this._cmdQueue.pop(); } @@ -218,8 +224,13 @@ export class MemcacheConnection extends MemcacheParser { } processCmd(cmdTokens: string[]): number { - const action = cmdActions[cmdTokens[0]]; - return (this as any)[`cmdAction_${action}` as keyof MemcacheConnection](cmdTokens); + const action = cmdActions[cmdTokens[0] as CommandAction]; + const handler = action ? this.cmdActionHandlers[action] : undefined; + if (handler) { + handler(cmdTokens); + return cmdTokens.length; + } + return this.cmdAction_undefined(cmdTokens) ? cmdTokens.length : 0; } _processMetaItem(token: string, metadata: MetaResult): void { @@ -344,8 +355,8 @@ export class MemcacheConnection extends MemcacheParser { this.cmdAction_OK(cmdTokens); } - cmdAction_SELF(cmdTokens: string[]): number | void { - (this as any)[`cmd_${cmdTokens[0]}` as keyof MemcacheConnection](cmdTokens); + cmdAction_SELF(cmdTokens: string[]): void { + this.selfCmdHandlers[cmdTokens[0]]?.(cmdTokens); } cmdAction_undefined(cmdTokens: string[]): boolean { @@ -388,7 +399,7 @@ export class MemcacheConnection extends MemcacheParser { while ((cmd = this.dequeueCommand())) { cmd.callback(new Error(msg)); } - this._status = Status.SHUTDOWN; + this._status = "SHUTDOWN"; // reset connection this.node?.endConnection(this); if (this.socket) { @@ -454,5 +465,3 @@ export class MemcacheConnection extends MemcacheParser { }); } } - -(MemcacheConnection as unknown as Record>).Status = Status; diff --git a/packages/memcache-client/src/test/spec/client.spec.ts b/packages/memcache-client/src/test/spec/client.spec.ts index dee02df..215f64d 100644 --- a/packages/memcache-client/src/test/spec/client.spec.ts +++ b/packages/memcache-client/src/test/spec/client.spec.ts @@ -198,9 +198,9 @@ describe("memcache client", function () { it("should use callback on get and set", (done) => { const x = new MemcacheClient({ server }); const key = `foo_${Date.now()}`; - x.set(key, "bar", (err?: Error | null) => { + x.set(key, "bar", {}, (err?: Error | null) => { expect(err).toBeNull(); - x.get(key, (gerr, data) => { + x.get(key, {}, (gerr, data) => { expect(gerr).toBeNull(); expect(data?.value).toEqual("bar"); x.shutdown(); @@ -227,7 +227,7 @@ describe("memcache client", function () { it("should set value with custom lifetime", () => { const x = new MemcacheClient({ server }); let testOptions: Partial = {}; - (x as any)._callbackSend = (data: unknown, key: string, options: Partial | undefined): Promise => { + x._callbackSend = (data: unknown, key: string, options?: Partial): Promise => { testOptions = options || {}; return Promise.resolve(); }; diff --git a/packages/memcache-client/src/test/spec/connection.spec.ts b/packages/memcache-client/src/test/spec/connection.spec.ts index 3ff77c8..089d739 100644 --- a/packages/memcache-client/src/test/spec/connection.spec.ts +++ b/packages/memcache-client/src/test/spec/connection.spec.ts @@ -1,16 +1,17 @@ -import { MemcacheConnection, Status, MemcacheClient, MemcacheNode } from "../.."; +import { MemcacheConnection, MemcacheClient, MemcacheNode } from "../.."; describe.only("MemcacheConnection", function () { it("waitReady should resolve connect Promise status is CONNECTING", () => { const x = new MemcacheConnection({ socketID: 1 } as unknown as MemcacheClient); - x._connectPromise = "test"; - x._status = Status.CONNECTING; - expect(x.waitReady()).toBe("test"); + const mockPromise = Promise.resolve("test"); + x._connectPromise = mockPromise; + x._status = "CONNECTING"; + expect(x.waitReady()).toBe(mockPromise); }); it("waitReady should resolve self if status is READY", (done) => { const x = new MemcacheConnection({ socketID: 1 } as unknown as MemcacheClient); - x._status = Status.READY; + x._status = "READY"; x.waitReady().then((data) => { expect(data).not.toBeUndefined(); done(); @@ -19,23 +20,21 @@ describe.only("MemcacheConnection", function () { it("waitReady should fail if status is INIT", () => { const x = new MemcacheConnection({ socketID: 1 } as unknown as MemcacheClient); - x._status = Status.INIT; + x._status = "INIT"; expect(() => x.waitReady()).toThrowError(Error); }); it("getStatuStr should return correct strings", () => { const x = new MemcacheConnection({ socketID: 1 } as unknown as MemcacheClient); // const Status = Status; - x._status = Status.INIT; + x._status = "INIT"; expect(x.getStatusStr()).toBe("INIT"); - x._status = Status.CONNECTING; + x._status = "CONNECTING"; expect(x.getStatusStr()).toBe("CONNECTING"); - x._status = Status.READY; + x._status = "READY"; expect(x.getStatusStr()).toBe("READY"); - x._status = Status.SHUTDOWN; + x._status = "SHUTDOWN"; expect(x.getStatusStr()).toBe("SHUTDOWN"); - x._status = 0; - expect(x.getStatusStr()).toBe("UNKNOWN"); }); it("cmdAction_ERROR should append message after command", () => { @@ -75,7 +74,7 @@ describe.only("MemcacheConnection", function () { } as unknown as MemcacheNode ); x._shutdown("test"); - expect(x.dequeueCommand()?.callback()).toBe(undefined); + expect(x.dequeueCommand()).toBeUndefined(); }); it("waitDangleSocket should do nothing if socket is falsy", () => {