memcache 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.md +14 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +6 -0
- package/.github/workflows/code-coverage.yaml +41 -0
- package/.github/workflows/codeql.yaml +75 -0
- package/.github/workflows/release.yaml +41 -0
- package/.github/workflows/tests.yaml +40 -0
- package/.nvmrc +1 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/CONTRIBUTING.md +27 -0
- package/LICENSE +21 -0
- package/README.md +369 -71
- package/SECURITY.md +3 -0
- package/biome.json +35 -0
- package/dist/index.cjs +1502 -0
- package/dist/index.d.cts +501 -0
- package/dist/index.d.ts +501 -0
- package/dist/index.js +1475 -0
- package/docker-compose.yml +24 -0
- package/package.json +38 -17
- package/pnpm-workspace.yaml +2 -0
- package/site/favicon.ico +0 -0
- package/site/logo.ai +7222 -37
- package/site/logo.png +0 -0
- package/site/logo.svg +7 -0
- package/site/logo.webp +0 -0
- package/site/logo_medium.png +0 -0
- package/site/logo_small.png +0 -0
- package/src/index.ts +1130 -0
- package/src/ketama.ts +449 -0
- package/src/node.ts +488 -0
- package/test/index.test.ts +2734 -0
- package/test/ketama.test.ts +526 -0
- package/test/memcache-node-instances.test.ts +102 -0
- package/test/node.test.ts +809 -0
- package/tsconfig.json +29 -0
- package/vitest.config.ts +16 -0
- package/.gitignore +0 -2
- package/Makefile +0 -13
- package/example.js +0 -68
- package/index.js +0 -1
- package/lib/memcache.js +0 -344
- package/test/test-memcache.js +0 -238
package/src/node.ts
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { createConnection, type Socket } from "node:net";
|
|
2
|
+
import { Hookified } from "hookified";
|
|
3
|
+
|
|
4
|
+
export interface MemcacheNodeOptions {
|
|
5
|
+
timeout?: number;
|
|
6
|
+
keepAlive?: boolean;
|
|
7
|
+
keepAliveDelay?: number;
|
|
8
|
+
weight?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CommandOptions {
|
|
12
|
+
isMultiline?: boolean;
|
|
13
|
+
isStats?: boolean;
|
|
14
|
+
requestedKeys?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface MemcacheStats {
|
|
18
|
+
[key: string]: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type CommandQueueItem = {
|
|
22
|
+
command: string;
|
|
23
|
+
// biome-ignore lint/suspicious/noExplicitAny: expected
|
|
24
|
+
resolve: (value: any) => void;
|
|
25
|
+
// biome-ignore lint/suspicious/noExplicitAny: expected
|
|
26
|
+
reject: (reason?: any) => void;
|
|
27
|
+
isMultiline?: boolean;
|
|
28
|
+
isStats?: boolean;
|
|
29
|
+
requestedKeys?: string[];
|
|
30
|
+
foundKeys?: string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* MemcacheNode represents a single memcache server connection.
|
|
35
|
+
* It handles the socket connection, command queue, and protocol parsing for one node.
|
|
36
|
+
*/
|
|
37
|
+
export class MemcacheNode extends Hookified {
|
|
38
|
+
private _host: string;
|
|
39
|
+
private _port: number;
|
|
40
|
+
private _socket: Socket | undefined = undefined;
|
|
41
|
+
private _timeout: number;
|
|
42
|
+
private _keepAlive: boolean;
|
|
43
|
+
private _keepAliveDelay: number;
|
|
44
|
+
private _weight: number;
|
|
45
|
+
private _connected: boolean = false;
|
|
46
|
+
private _commandQueue: CommandQueueItem[] = [];
|
|
47
|
+
private _buffer: string = "";
|
|
48
|
+
private _currentCommand: CommandQueueItem | undefined = undefined;
|
|
49
|
+
private _multilineData: string[] = [];
|
|
50
|
+
private _pendingValueBytes: number = 0;
|
|
51
|
+
|
|
52
|
+
constructor(host: string, port: number, options?: MemcacheNodeOptions) {
|
|
53
|
+
super();
|
|
54
|
+
this._host = host;
|
|
55
|
+
this._port = port;
|
|
56
|
+
this._timeout = options?.timeout || 5000;
|
|
57
|
+
this._keepAlive = options?.keepAlive !== false;
|
|
58
|
+
this._keepAliveDelay = options?.keepAliveDelay || 1000;
|
|
59
|
+
this._weight = options?.weight || 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the host of this node
|
|
64
|
+
*/
|
|
65
|
+
public get host(): string {
|
|
66
|
+
return this._host;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the port of this node
|
|
71
|
+
*/
|
|
72
|
+
public get port(): number {
|
|
73
|
+
return this._port;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the unique identifier for this node (host:port format)
|
|
78
|
+
*/
|
|
79
|
+
public get id(): string {
|
|
80
|
+
return this._port === 0 ? this._host : `${this._host}:${this._port}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the full uri like memcache://localhost:11211
|
|
85
|
+
*/
|
|
86
|
+
public get uri(): string {
|
|
87
|
+
return `memcache://${this.id}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get the socket connection
|
|
92
|
+
*/
|
|
93
|
+
public get socket(): Socket | undefined {
|
|
94
|
+
return this._socket;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the weight of this node (used for consistent hashing distribution)
|
|
99
|
+
*/
|
|
100
|
+
public get weight(): number {
|
|
101
|
+
return this._weight;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Set the weight of this node (used for consistent hashing distribution)
|
|
106
|
+
*/
|
|
107
|
+
public set weight(value: number) {
|
|
108
|
+
this._weight = value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the keepAlive setting for this node
|
|
113
|
+
*/
|
|
114
|
+
public get keepAlive(): boolean {
|
|
115
|
+
return this._keepAlive;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Set the keepAlive setting for this node
|
|
120
|
+
*/
|
|
121
|
+
public set keepAlive(value: boolean) {
|
|
122
|
+
this._keepAlive = value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get the keepAliveDelay setting for this node
|
|
127
|
+
*/
|
|
128
|
+
public get keepAliveDelay(): number {
|
|
129
|
+
return this._keepAliveDelay;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Set the keepAliveDelay setting for this node
|
|
134
|
+
*/
|
|
135
|
+
public set keepAliveDelay(value: number) {
|
|
136
|
+
this._keepAliveDelay = value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get the command queue
|
|
141
|
+
*/
|
|
142
|
+
public get commandQueue(): CommandQueueItem[] {
|
|
143
|
+
return this._commandQueue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Connect to the memcache server
|
|
148
|
+
*/
|
|
149
|
+
public async connect(): Promise<void> {
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
if (this._connected) {
|
|
152
|
+
resolve();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this._socket = createConnection({
|
|
157
|
+
host: this._host,
|
|
158
|
+
port: this._port,
|
|
159
|
+
keepAlive: this._keepAlive,
|
|
160
|
+
keepAliveInitialDelay: this._keepAliveDelay,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
this._socket.setTimeout(this._timeout);
|
|
164
|
+
this._socket.setEncoding("utf8");
|
|
165
|
+
|
|
166
|
+
this._socket.on("connect", () => {
|
|
167
|
+
this._connected = true;
|
|
168
|
+
this.emit("connect");
|
|
169
|
+
resolve();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
this._socket.on("data", (data: string) => {
|
|
173
|
+
this.handleData(data);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this._socket.on("error", (error: Error) => {
|
|
177
|
+
this.emit("error", error);
|
|
178
|
+
if (!this._connected) {
|
|
179
|
+
/* v8 ignore next -- @preserve */
|
|
180
|
+
reject(error);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
this._socket.on("close", () => {
|
|
185
|
+
this._connected = false;
|
|
186
|
+
this.emit("close");
|
|
187
|
+
this.rejectPendingCommands(new Error("Connection closed"));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
this._socket.on("timeout", () => {
|
|
191
|
+
this.emit("timeout");
|
|
192
|
+
this._socket?.destroy();
|
|
193
|
+
reject(new Error("Connection timeout"));
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Disconnect from the memcache server
|
|
200
|
+
*/
|
|
201
|
+
public async disconnect(): Promise<void> {
|
|
202
|
+
/* v8 ignore next -- @preserve */
|
|
203
|
+
if (this._socket) {
|
|
204
|
+
this._socket.destroy();
|
|
205
|
+
this._socket = undefined;
|
|
206
|
+
this._connected = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Reconnect to the memcache server by disconnecting and connecting again
|
|
212
|
+
*/
|
|
213
|
+
public async reconnect(): Promise<void> {
|
|
214
|
+
// First disconnect if currently connected
|
|
215
|
+
if (this._connected || this._socket) {
|
|
216
|
+
await this.disconnect();
|
|
217
|
+
// Clear any pending commands with a reconnection error
|
|
218
|
+
this.rejectPendingCommands(
|
|
219
|
+
new Error("Connection reset for reconnection"),
|
|
220
|
+
);
|
|
221
|
+
// Clear the buffer and current command state
|
|
222
|
+
this._buffer = "";
|
|
223
|
+
this._currentCommand = undefined;
|
|
224
|
+
this._multilineData = [];
|
|
225
|
+
this._pendingValueBytes = 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Now establish a fresh connection
|
|
229
|
+
await this.connect();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Gracefully quit the connection (send quit command then disconnect)
|
|
234
|
+
*/
|
|
235
|
+
public async quit(): Promise<void> {
|
|
236
|
+
/* v8 ignore next -- @preserve */
|
|
237
|
+
if (this._connected && this._socket) {
|
|
238
|
+
try {
|
|
239
|
+
await this.command("quit");
|
|
240
|
+
// biome-ignore lint/correctness/noUnusedVariables: expected
|
|
241
|
+
} catch (error) {
|
|
242
|
+
// Ignore errors from quit command as the server closes the connection
|
|
243
|
+
}
|
|
244
|
+
await this.disconnect();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Check if connected to the memcache server
|
|
250
|
+
*/
|
|
251
|
+
public isConnected(): boolean {
|
|
252
|
+
return this._connected;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Send a generic command to the memcache server
|
|
257
|
+
* @param cmd The command string to send (without trailing \r\n)
|
|
258
|
+
* @param options Command options for response parsing
|
|
259
|
+
*/
|
|
260
|
+
public async command(
|
|
261
|
+
cmd: string,
|
|
262
|
+
options?: CommandOptions,
|
|
263
|
+
// biome-ignore lint/suspicious/noExplicitAny: expected
|
|
264
|
+
): Promise<any> {
|
|
265
|
+
if (!this._connected || !this._socket) {
|
|
266
|
+
throw new Error(`Not connected to memcache server ${this.id}`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return new Promise((resolve, reject) => {
|
|
270
|
+
this._commandQueue.push({
|
|
271
|
+
command: cmd,
|
|
272
|
+
resolve,
|
|
273
|
+
reject,
|
|
274
|
+
isMultiline: options?.isMultiline,
|
|
275
|
+
isStats: options?.isStats,
|
|
276
|
+
requestedKeys: options?.requestedKeys,
|
|
277
|
+
});
|
|
278
|
+
// biome-ignore lint/style/noNonNullAssertion: socket is checked
|
|
279
|
+
this._socket!.write(`${cmd}\r\n`);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private handleData(data: string): void {
|
|
284
|
+
this._buffer += data;
|
|
285
|
+
|
|
286
|
+
while (true) {
|
|
287
|
+
// If we're waiting for value data, try to read it first
|
|
288
|
+
if (this._pendingValueBytes > 0) {
|
|
289
|
+
if (this._buffer.length >= this._pendingValueBytes + 2) {
|
|
290
|
+
const value = this._buffer.substring(0, this._pendingValueBytes);
|
|
291
|
+
this._buffer = this._buffer.substring(this._pendingValueBytes + 2);
|
|
292
|
+
this._multilineData.push(value);
|
|
293
|
+
this._pendingValueBytes = 0;
|
|
294
|
+
} else {
|
|
295
|
+
// Not enough data yet, wait for more
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const lineEnd = this._buffer.indexOf("\r\n");
|
|
301
|
+
if (lineEnd === -1) break;
|
|
302
|
+
|
|
303
|
+
const line = this._buffer.substring(0, lineEnd);
|
|
304
|
+
this._buffer = this._buffer.substring(lineEnd + 2);
|
|
305
|
+
|
|
306
|
+
this.processLine(line);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private processLine(line: string): void {
|
|
311
|
+
if (!this._currentCommand) {
|
|
312
|
+
this._currentCommand = this._commandQueue.shift();
|
|
313
|
+
if (!this._currentCommand) return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (this._currentCommand.isStats) {
|
|
317
|
+
if (line === "END") {
|
|
318
|
+
const stats: MemcacheStats = {};
|
|
319
|
+
for (const statLine of this._multilineData) {
|
|
320
|
+
const [, key, value] = statLine.split(" ");
|
|
321
|
+
/* v8 ignore next -- @preserve */
|
|
322
|
+
if (key && value) {
|
|
323
|
+
stats[key] = value;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
this._currentCommand.resolve(stats);
|
|
327
|
+
this._multilineData = [];
|
|
328
|
+
this._currentCommand = undefined;
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (line.startsWith("STAT ")) {
|
|
333
|
+
this._multilineData.push(line);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (
|
|
338
|
+
line.startsWith("ERROR") ||
|
|
339
|
+
line.startsWith("CLIENT_ERROR") ||
|
|
340
|
+
line.startsWith("SERVER_ERROR")
|
|
341
|
+
) {
|
|
342
|
+
this._currentCommand.reject(new Error(line));
|
|
343
|
+
this._currentCommand = undefined;
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (this._currentCommand.isMultiline) {
|
|
351
|
+
// Track found keys only if requestedKeys is provided
|
|
352
|
+
if (
|
|
353
|
+
this._currentCommand.requestedKeys &&
|
|
354
|
+
!this._currentCommand.foundKeys
|
|
355
|
+
) {
|
|
356
|
+
this._currentCommand.foundKeys = [];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (line.startsWith("VALUE ")) {
|
|
360
|
+
const parts = line.split(" ");
|
|
361
|
+
const key = parts[1];
|
|
362
|
+
const bytes = parseInt(parts[3], 10);
|
|
363
|
+
if (this._currentCommand.requestedKeys) {
|
|
364
|
+
this._currentCommand.foundKeys?.push(key);
|
|
365
|
+
}
|
|
366
|
+
// Set pending bytes so handleData will read the value
|
|
367
|
+
this._pendingValueBytes = bytes;
|
|
368
|
+
} else if (line === "END") {
|
|
369
|
+
let result:
|
|
370
|
+
| string[]
|
|
371
|
+
| { values: string[] | undefined; foundKeys: string[] }
|
|
372
|
+
| undefined;
|
|
373
|
+
|
|
374
|
+
// If requestedKeys is present, return object with keys and values
|
|
375
|
+
if (
|
|
376
|
+
this._currentCommand.requestedKeys &&
|
|
377
|
+
this._currentCommand.foundKeys
|
|
378
|
+
) {
|
|
379
|
+
result = {
|
|
380
|
+
values:
|
|
381
|
+
this._multilineData.length > 0 ? this._multilineData : undefined,
|
|
382
|
+
foundKeys: this._currentCommand.foundKeys,
|
|
383
|
+
};
|
|
384
|
+
} else {
|
|
385
|
+
result =
|
|
386
|
+
this._multilineData.length > 0 ? this._multilineData : undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Emit hit/miss events if we have requested keys
|
|
390
|
+
/* v8 ignore next -- @preserve */
|
|
391
|
+
if (
|
|
392
|
+
this._currentCommand.requestedKeys &&
|
|
393
|
+
this._currentCommand.foundKeys
|
|
394
|
+
) {
|
|
395
|
+
const foundKeys = this._currentCommand.foundKeys;
|
|
396
|
+
for (let i = 0; i < foundKeys.length; i++) {
|
|
397
|
+
this.emit("hit", foundKeys[i], this._multilineData[i]);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Emit miss events for keys that weren't found
|
|
401
|
+
const missedKeys = this._currentCommand.requestedKeys.filter(
|
|
402
|
+
(key) => !foundKeys.includes(key),
|
|
403
|
+
);
|
|
404
|
+
for (const key of missedKeys) {
|
|
405
|
+
this.emit("miss", key);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
this._currentCommand.resolve(result);
|
|
410
|
+
this._multilineData = [];
|
|
411
|
+
this._currentCommand = undefined;
|
|
412
|
+
} else if (
|
|
413
|
+
line.startsWith("ERROR") ||
|
|
414
|
+
line.startsWith("CLIENT_ERROR") ||
|
|
415
|
+
line.startsWith("SERVER_ERROR")
|
|
416
|
+
) {
|
|
417
|
+
this._currentCommand.reject(new Error(line));
|
|
418
|
+
this._multilineData = [];
|
|
419
|
+
this._currentCommand = undefined;
|
|
420
|
+
}
|
|
421
|
+
} else {
|
|
422
|
+
if (
|
|
423
|
+
line === "STORED" ||
|
|
424
|
+
line === "DELETED" ||
|
|
425
|
+
line === "OK" ||
|
|
426
|
+
line === "TOUCHED" ||
|
|
427
|
+
line === "EXISTS" ||
|
|
428
|
+
line === "NOT_FOUND"
|
|
429
|
+
) {
|
|
430
|
+
this._currentCommand.resolve(line);
|
|
431
|
+
} else if (line === "NOT_STORED") {
|
|
432
|
+
this._currentCommand.resolve(false);
|
|
433
|
+
} else if (
|
|
434
|
+
line.startsWith("ERROR") ||
|
|
435
|
+
line.startsWith("CLIENT_ERROR") ||
|
|
436
|
+
line.startsWith("SERVER_ERROR")
|
|
437
|
+
) {
|
|
438
|
+
this._currentCommand.reject(new Error(line));
|
|
439
|
+
} else if (/^\d+$/.test(line)) {
|
|
440
|
+
this._currentCommand.resolve(parseInt(line, 10));
|
|
441
|
+
} else {
|
|
442
|
+
this._currentCommand.resolve(line);
|
|
443
|
+
}
|
|
444
|
+
this._currentCommand = undefined;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private rejectPendingCommands(error: Error): void {
|
|
449
|
+
if (this._currentCommand) {
|
|
450
|
+
/* v8 ignore next -- @preserve */
|
|
451
|
+
this._currentCommand.reject(error);
|
|
452
|
+
/* v8 ignore next -- @preserve */
|
|
453
|
+
this._currentCommand = undefined;
|
|
454
|
+
}
|
|
455
|
+
while (this._commandQueue.length > 0) {
|
|
456
|
+
const cmd = this._commandQueue.shift();
|
|
457
|
+
/* v8 ignore next -- @preserve */
|
|
458
|
+
if (cmd) {
|
|
459
|
+
cmd.reject(error);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Factory function to create a new MemcacheNode instance.
|
|
467
|
+
* @param host - The hostname or IP address of the memcache server
|
|
468
|
+
* @param port - The port number of the memcache server
|
|
469
|
+
* @param options - Optional configuration for the node
|
|
470
|
+
* @returns A new MemcacheNode instance
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* ```typescript
|
|
474
|
+
* const node = createNode('localhost', 11211, {
|
|
475
|
+
* timeout: 5000,
|
|
476
|
+
* keepAlive: true,
|
|
477
|
+
* weight: 1
|
|
478
|
+
* });
|
|
479
|
+
* await node.connect();
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
export function createNode(
|
|
483
|
+
host: string,
|
|
484
|
+
port: number,
|
|
485
|
+
options?: MemcacheNodeOptions,
|
|
486
|
+
): MemcacheNode {
|
|
487
|
+
return new MemcacheNode(host, port, options);
|
|
488
|
+
}
|