wadis 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.
Binary file
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "wadis",
3
+ "version": "1.0.0",
4
+ "description": "Wadis is a WebAssembly (Wasm) build of the Redis® in-memory datastore, purpose-built for automated testing 🧪",
5
+ "repository": "https://github.com/cah4a/wadis",
6
+ "type": "module",
7
+ "license": "Redis Source Available License v2 (RSALv2)",
8
+ "author": "Sancha",
9
+ "main": "dist/index.cjs",
10
+ "module": "dist/index.js",
11
+ "types": "dist/index.d.ts",
12
+ "engines": {
13
+ "node": ">=20.0.0",
14
+ "bun": ">=1.1.0"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "packageManager": "pnpm@10.19.0",
23
+ "scripts": {
24
+ "build": "rm -rf dist && tsup",
25
+ "build:wasm": "./compile && pnpm run build",
26
+ "test": "vitest run",
27
+ "lint": "eslint .",
28
+ "prepare": "pnpm run build",
29
+ "patch:apply": "patch -p1 --forward -d vendor/redis < patches/redis-8.2.1.patch",
30
+ "patch:create": "cd vendor/redis && git diff > ../../patches/redis-8.2.1.patch && cd -",
31
+ "fmt": "biome format . --write",
32
+ "fix": "biome check . --write"
33
+ },
34
+ "peerDependencies": {
35
+ "ioredis": "^5.4.1"
36
+ },
37
+ "devDependencies": {
38
+ "@biomejs/biome": "^2.2.4",
39
+ "@types/node": "^22.7.4",
40
+ "bullmq": "^5.61.2",
41
+ "esbuild-plugin-copy": "^2.1.1",
42
+ "tsup": "^8.5.0",
43
+ "typescript": "^5.6.3",
44
+ "vite-tsconfig-paths": "^5.1.4",
45
+ "vitest": "^2.1.3"
46
+ }
47
+ }
@@ -0,0 +1,111 @@
1
+ import type { WadisServer } from "wadisServer";
2
+ import { Socket } from "node:net";
3
+
4
+ export class WadisServerConnector extends Socket {
5
+ private conn: {
6
+ write: (data: Uint8Array | Buffer) => void;
7
+ read: () => Uint8Array | null;
8
+ wantsClose: () => boolean;
9
+ close: () => void;
10
+ };
11
+ private poll?: NodeJS.Timeout;
12
+ private _closed = false;
13
+ private queue: Promise<void> = Promise.resolve();
14
+
15
+ constructor(server: WadisServer) {
16
+ super();
17
+ this.conn = server.createConnection();
18
+ this.startPolling();
19
+ setImmediate(() => this.emit("connect"));
20
+ }
21
+
22
+ override setTimeout(_msecs: number, _callback?: () => void): this {
23
+ return this;
24
+ }
25
+ override setNoDelay(_noDelay?: boolean): this {
26
+ return this;
27
+ }
28
+ override setKeepAlive(_enable?: boolean, _initialDelay?: number): this {
29
+ return this;
30
+ }
31
+
32
+ override write(
33
+ chunk: Uint8Array | string,
34
+ encoding?: BufferEncoding | ((err?: Error) => void),
35
+ cb?: (err?: Error) => void,
36
+ ): boolean {
37
+ let callback: ((err?: Error) => void) | undefined;
38
+ let enc: BufferEncoding | undefined;
39
+ if (typeof encoding === "function") {
40
+ callback = encoding;
41
+ } else {
42
+ enc = encoding as BufferEncoding | undefined;
43
+ callback = cb;
44
+ }
45
+ const buf =
46
+ typeof chunk === "string"
47
+ ? Buffer.from(chunk, enc ?? "utf8")
48
+ : Buffer.from(chunk);
49
+ // Ensure replies are emitted asynchronously after write returns,
50
+ // and preserve write ordering with a micro-queue.
51
+ this.queue = this.queue.then(
52
+ () =>
53
+ new Promise<void>((resolve) => {
54
+ setImmediate(() => {
55
+ try {
56
+ this.conn.write(buf);
57
+ } catch (err) {
58
+ this.emit("error", err as Error);
59
+ } finally {
60
+ resolve();
61
+ }
62
+ });
63
+ }),
64
+ );
65
+ if (callback) setImmediate(() => callback());
66
+ return true;
67
+ }
68
+
69
+ override end(..._args: unknown[]): this {
70
+ if (this._closed) return this;
71
+ this._closed = true;
72
+ if (this.poll) {
73
+ clearInterval(this.poll);
74
+ this.poll = undefined;
75
+ }
76
+ this.conn.close();
77
+ setImmediate(() => this.emit("end"));
78
+ setImmediate(() => this.emit("close", false));
79
+ return this;
80
+ }
81
+ override destroy(_error?: Error): this {
82
+ if (this._closed) return this;
83
+ this._closed = true;
84
+ if (this.poll) {
85
+ clearInterval(this.poll);
86
+ this.poll = undefined;
87
+ }
88
+ this.conn.close();
89
+ setImmediate(() => this.emit("close", true));
90
+ return this;
91
+ }
92
+ private startPolling() {
93
+ this.poll = setInterval(() => this.drain(), 10);
94
+ // Do not keep the process alive solely due to this timer
95
+ // (important for tests that don't explicitly quit the base connection)
96
+ this.poll.unref?.();
97
+ }
98
+
99
+ private drain() {
100
+ while (true) {
101
+ const out = this.conn.read();
102
+ if (!out || out.length === 0) break;
103
+ this.emit("data", Buffer.from(out));
104
+ }
105
+ // After draining all pending output, if Redis marked this client
106
+ // to close after reply (QUIT), close the socket gracefully.
107
+ if (!this._closed && this.conn.wantsClose()) {
108
+ this.end();
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,62 @@
1
+ import { type Job, Queue, QueueEvents, Worker } from "bullmq";
2
+ import { Wadis, WadisServer } from "index";
3
+ import { describe, expect, test } from "vitest";
4
+
5
+ describe("bullmq", () => {
6
+ test("adding jobs", async () => {
7
+ const redis = new Wadis();
8
+ const myQueue = new Queue("my-queue", { connection: redis });
9
+ const job = await myQueue.add("my-job", { name: "Test Job" });
10
+
11
+ expect(await job.getState()).toEqual("waiting");
12
+ await myQueue.close();
13
+ await redis.quit();
14
+ });
15
+
16
+ test(
17
+ "processing jobs",
18
+ async () => {
19
+ const redis = new Wadis();
20
+ const myQueue = new Queue("my-queue", { connection: redis });
21
+ const job = await myQueue.add("my-job", { name: "Test Job" });
22
+ const { promise, resolve } = Promise.withResolvers<void>();
23
+
24
+ new Worker(
25
+ "my-queue",
26
+ async (job: Job) => {
27
+ setTimeout(resolve, 10);
28
+ return `Processed ${job.data.name}`;
29
+ },
30
+ { connection: redis },
31
+ );
32
+
33
+ await promise;
34
+ expect(await job.getState()).toBe("completed");
35
+ expect((await myQueue.getJob(job.id || ""))?.returnvalue).toBe(
36
+ "Processed Test Job",
37
+ );
38
+ },
39
+ { timeout: 1000 },
40
+ );
41
+
42
+ test("queue events", async () => {
43
+ const server = await WadisServer.new();
44
+ const redis = new Wadis({
45
+ server,
46
+ maxRetriesPerRequest: null,
47
+ });
48
+
49
+ const myQueue = new Queue("my-queue", { connection: redis });
50
+ const myQueueEvents = new QueueEvents("my-queue", { connection: redis });
51
+ await myQueueEvents.waitUntilReady();
52
+
53
+ const job = await myQueue.add("my-job", { name: "Test Job" });
54
+
55
+ new Worker("my-queue", async (job: Job) => `Processed ${job.data.name}`, {
56
+ connection: redis,
57
+ });
58
+
59
+ await job.waitUntilFinished(myQueueEvents, 1000);
60
+ expect(await job.getState()).toBe("completed");
61
+ });
62
+ });
@@ -0,0 +1,98 @@
1
+ import { Wadis, WadisServer } from "index";
2
+ import { describe, expect, test } from "vitest";
3
+
4
+ describe("ioredis adapter", () => {
5
+ test("works", async () => {
6
+ const redis = new Wadis();
7
+
8
+ await redis.set("foo", "bar");
9
+ const duplicate = redis.duplicate();
10
+ expect(await duplicate.get("foo")).toBe("bar");
11
+ await duplicate.set("foo", "baz");
12
+ expect(await redis.get("foo")).toBe("baz");
13
+ await duplicate.quit();
14
+
15
+ expect(await redis.get("foo")).toBe("baz");
16
+ });
17
+
18
+ test("pub/sub works", async () => {
19
+ const redis = new Wadis();
20
+
21
+ const subscriber = redis.duplicate();
22
+ await subscriber.subscribe("my-channel");
23
+
24
+ const messages: string[] = [];
25
+ subscriber.on("message", (_channel, message) => {
26
+ messages.push(message);
27
+ });
28
+ const publisher = redis.duplicate();
29
+ await publisher.publish("my-channel", "hello");
30
+ await publisher.publish("my-channel", "world");
31
+
32
+ // wait a tick for messages to be received
33
+ await new Promise((resolve) => setTimeout(resolve, 100));
34
+
35
+ expect(messages).toEqual(["hello", "world"]);
36
+
37
+ await subscriber.quit();
38
+ await publisher.quit();
39
+ });
40
+
41
+ test("lua scripting works", async () => {
42
+ const redis = new Wadis();
43
+
44
+ const script = `return ARGV[1] .. ARGV[2]`;
45
+ const result = await redis.eval(script, 0, "Hello, ", "world!");
46
+
47
+ expect(result).toBe("Hello, world!");
48
+ });
49
+
50
+ test("cjson.encode works", async () => {
51
+ const r = new Wadis();
52
+ const res = await r.eval("return cjson.encode({a=1,b='x'})", 0);
53
+ expect(res).toBe('{"a":1,"b":"x"}');
54
+ });
55
+
56
+ test("cjson.decode works", async () => {
57
+ const r = new Wadis();
58
+ const res = await r.eval(
59
+ "local t=cjson.decode(ARGV[1]); return t.a+t.b",
60
+ 0,
61
+ '{"a":2,"b":3}',
62
+ );
63
+ expect(res).toBe(5);
64
+ });
65
+
66
+ test("streams blocking unblocks", async () => {
67
+ const server = await WadisServer.new();
68
+ const c1 = new Wadis({ server });
69
+ const c2 = new Wadis({ server });
70
+
71
+ await c1.xgroup("CREATE", "s2", "g2", "0", "MKSTREAM");
72
+
73
+ const p = c1.xreadgroup(
74
+ "GROUP",
75
+ "g2",
76
+ "w1",
77
+ "BLOCK" as never,
78
+ 2000,
79
+ "COUNT",
80
+ 1,
81
+ "STREAMS",
82
+ "s2",
83
+ ">",
84
+ );
85
+
86
+ // wait a tick then add
87
+ setTimeout(async () => {
88
+ await c2.xadd("s2", "*", "f", "v");
89
+ }, 100);
90
+
91
+ const res = await p;
92
+ expect(Array.isArray(res)).toBe(true);
93
+
94
+ await c1.quit();
95
+ await c2.quit();
96
+ await server.terminate();
97
+ });
98
+ });
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { WadisServer } from "wadisServer";
2
+ import { WadisServerConnector } from "adapters/ioredis";
3
+ import Redis, { AbstractConnector, type RedisOptions } from "ioredis";
4
+ import type { ErrorEmitter } from "ioredis/built/connectors/AbstractConnector";
5
+
6
+ export * from "wadisServer";
7
+
8
+ export type WadisOptions = Omit<
9
+ RedisOptions,
10
+ "Connector" | "host" | "port" | "username" | "password" | "db"
11
+ > & {
12
+ server?: WadisServer | Promise<WadisServer>;
13
+ };
14
+
15
+ export class Wadis extends Redis {
16
+ constructor({ server, ...options }: WadisOptions = {}) {
17
+ const wasmServer = server ?? WadisServer.new();
18
+
19
+ class Connector extends AbstractConnector {
20
+ constructor(something: unknown) {
21
+ // shitty ioredis types here
22
+ super(something as never);
23
+ }
24
+
25
+ async connect(_: ErrorEmitter) {
26
+ return new WadisServerConnector(await wasmServer);
27
+ }
28
+ }
29
+
30
+ super({ Connector, maxRetriesPerRequest: null, ...options });
31
+ }
32
+ }
@@ -0,0 +1,65 @@
1
+ import { WadisServer } from "wadisServer";
2
+ import { TextDecoder } from "node:util";
3
+ import { beforeEach, describe, expect, test } from "vitest";
4
+
5
+ describe("wadis server", () => {
6
+ let server: WadisServer;
7
+
8
+ beforeEach(async () => {
9
+ server = await WadisServer.new();
10
+ });
11
+
12
+ describe("basic commands", () => {
13
+ test("PING works", async () => {
14
+ const resp = await server.call("PING");
15
+ expect(new TextDecoder().decode(resp)).toBe("+PONG\r\n");
16
+ });
17
+
18
+ test("SET/GET works", async () => {
19
+ const server = await WadisServer.new({ maxmemory: "50mb" });
20
+ const ok = await server.call("SET", "foo", "bar");
21
+ expect(new TextDecoder().decode(ok)).toBe("+OK\r\n");
22
+
23
+ const val = await server.call("GET", "foo");
24
+ expect(new TextDecoder().decode(val)).toBe("$3\r\nbar\r\n");
25
+ });
26
+
27
+ test("INCR increments integer keys", async () => {
28
+ const v1 = await server.call("INCR", "counter");
29
+ expect(new TextDecoder().decode(v1)).toBe(":1\r\n");
30
+ const v2 = await server.call("INCR", "counter");
31
+ expect(new TextDecoder().decode(v2)).toBe(":2\r\n");
32
+ });
33
+
34
+ test("returns error replies for wrong types", async () => {
35
+ await server.call("SET", "notint", "x");
36
+ const err = await server.call("INCR", "notint");
37
+ const s = new TextDecoder().decode(err);
38
+ expect(s.startsWith("-ERR")).toBe(true);
39
+ });
40
+ });
41
+
42
+ describe("lua scripts", () => {
43
+ test("EVAL returns array of values", async () => {
44
+ const script = "return {KEYS[1], ARGV[1]}";
45
+ const resp = await server.call("EVAL", script, "1", "key1", "val1");
46
+ const s = new TextDecoder().decode(resp);
47
+ expect(s.startsWith("*2\r\n")).toBe(true);
48
+ expect(s.includes("$4\r\nkey1\r\n")).toBe(true);
49
+ expect(s.includes("$4\r\nval1\r\n")).toBe(true);
50
+ });
51
+
52
+ test("EVAL can SET/GET", async () => {
53
+ const script =
54
+ "redis.call('SET', KEYS[1], ARGV[1]); return redis.call('GET', KEYS[1])";
55
+ const resp = await server.call("EVAL", script, "1", "foo", "bar");
56
+ expect(new TextDecoder().decode(resp)).toBe("$3\r\nbar\r\n");
57
+ });
58
+
59
+ test("EVAL returns integer", async () => {
60
+ const script = "return 7";
61
+ const resp = await server.call("EVAL", script, "0");
62
+ expect(new TextDecoder().decode(resp)).toBe(":7\r\n");
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,222 @@
1
+ // @ts-expect-error
2
+ import redis from "wasm/redis.js";
3
+
4
+ export type WadisOptions = {
5
+ // Configurable Redis memory cap. Accepts a number of bytes or a Redis-size string like '128mb'.
6
+ maxmemory?: number | string;
7
+ // Eviction policy, matches Redis config: 'noeviction', 'allkeys-lru', etc.
8
+ maxmemoryPolicy?:
9
+ | "noeviction"
10
+ | "allkeys-lru"
11
+ | "allkeys-lfu"
12
+ | "allkeys-random"
13
+ | "volatile-lru"
14
+ | "volatile-lfu"
15
+ | "volatile-random"
16
+ | "volatile-ttl"
17
+ | string;
18
+ // Redis loglevel: 'debug' | 'verbose' | 'notice' | 'warning'
19
+ loglevel?: "debug" | "verbose" | "notice" | "warning" | string;
20
+ };
21
+
22
+ type EmscriptenModule = {
23
+ cwrap: (
24
+ ident: string,
25
+ returnType: string,
26
+ argTypes: string[],
27
+ ) => (...args: unknown[]) => unknown;
28
+ _malloc: (n: number) => number;
29
+ _free: (ptr: number) => void;
30
+ writeArrayToMemory?: (arr: Uint8Array, ptr: number) => void;
31
+ setValue?: (ptr: number, value: number, type: string) => void;
32
+ getValue: (ptr: number, type: string) => number;
33
+ };
34
+
35
+ export class WadisServer {
36
+ private Module: EmscriptenModule | null = null;
37
+ private _clientHandle: number | null = null;
38
+
39
+ private constructor(private _opts: WadisOptions = {}) {}
40
+
41
+ static async new(
42
+ opts: WadisOptions = { loglevel: "warning" },
43
+ ): Promise<WadisServer> {
44
+ const server = new WadisServer(opts);
45
+ await server.start();
46
+ return server;
47
+ }
48
+
49
+ async start(): Promise<void> {
50
+ this.Module = (await redis({ noInitialRun: true })) as EmscriptenModule;
51
+
52
+ const init = this.Module.cwrap("redis_init", "number", ["number"]) as (
53
+ level: number,
54
+ ) => number;
55
+ const rc = init(this.loglevelToInt(this._opts.loglevel ?? "warning"));
56
+ if (rc !== 0) throw new Error(`redis_init failed: ${rc}`);
57
+
58
+ // Create a persistent client used by call(). This unifies command execution
59
+ // with the same mechanism the adapter uses, enabling push semantics when needed.
60
+ this._clientHandle = this._createHandle();
61
+
62
+ // Apply runtime configuration overrides if provided.
63
+ if (this._opts.maxmemory !== undefined) {
64
+ const val =
65
+ typeof this._opts.maxmemory === "number"
66
+ ? String(this._opts.maxmemory)
67
+ : this._opts.maxmemory;
68
+ await this.call("CONFIG", "SET", "maxmemory", val);
69
+ }
70
+ if (this._opts.maxmemoryPolicy) {
71
+ await this.call(
72
+ "CONFIG",
73
+ "SET",
74
+ "maxmemory-policy",
75
+ this._opts.maxmemoryPolicy,
76
+ );
77
+ }
78
+ if (this._opts.loglevel) {
79
+ await this.call("CONFIG", "SET", "loglevel", this._opts.loglevel);
80
+ }
81
+ }
82
+
83
+ // Minimal RESP encoder for array of bulk strings: *N\r\n$len\r\nfoo\r\n...
84
+ private encodeCommand(parts: (string | Buffer)[]): Uint8Array {
85
+ const chunks: Buffer[] = [];
86
+ chunks.push(Buffer.from(`*${parts.length}\r\n`));
87
+ for (const p of parts) {
88
+ const b = Buffer.isBuffer(p) ? p : Buffer.from(p);
89
+ chunks.push(Buffer.from(`$${b.length}\r\n`));
90
+ chunks.push(b);
91
+ chunks.push(Buffer.from(`\r\n`));
92
+ }
93
+ return Buffer.concat(chunks);
94
+ }
95
+
96
+ async call(...parts: (string | Buffer)[]): Promise<Uint8Array> {
97
+ if (!this.Module) throw new Error("WASM not started");
98
+ if (!this._clientHandle) this._clientHandle = this._createHandle();
99
+ const input = this.encodeCommand(parts);
100
+ this._clientFeed(this._clientHandle, input);
101
+ const out = this._clientRead(this._clientHandle);
102
+ return out ?? new Uint8Array(0);
103
+ }
104
+
105
+ private _createHandle(): number {
106
+ if (!this.Module) throw new Error("WASM not started");
107
+ const mod = this.Module as EmscriptenModule;
108
+ const fn = mod.cwrap("redis_create_handle", "number", []) as () => number;
109
+ const h = fn();
110
+ if (!h) throw new Error("redis_create_handle failed");
111
+ return h;
112
+ }
113
+
114
+ private _clientFeed(handle: number, data: Uint8Array | Buffer): void {
115
+ if (!this.Module) throw new Error("WASM not started");
116
+ const mod = this.Module as EmscriptenModule;
117
+ const feed = mod.cwrap("redis_client_feed", "number", [
118
+ "number",
119
+ "number",
120
+ "number",
121
+ ]) as (h: number, ptr: number, len: number) => number;
122
+ const malloc = mod._malloc as (n: number) => number;
123
+ const free = mod._free as (ptr: number) => void;
124
+ const input = Buffer.isBuffer(data) ? data : Buffer.from(data);
125
+ const inPtr = malloc(input.length);
126
+ if (typeof mod.writeArrayToMemory === "function") {
127
+ mod.writeArrayToMemory(input, inPtr);
128
+ } else if (typeof mod.setValue === "function") {
129
+ for (let i = 0; i < input.length; i++)
130
+ mod.setValue(inPtr + i, input[i], "i8");
131
+ } else {
132
+ throw new Error(
133
+ "Emscripten runtime missing writeArrayToMemory/setValue",
134
+ );
135
+ }
136
+ const rc = feed(handle, inPtr, input.length);
137
+ free(inPtr);
138
+ if (rc !== 0) throw new Error(`redis_client_feed failed: ${rc}`);
139
+ }
140
+
141
+ private _clientRead(handle: number): Uint8Array | null {
142
+ if (!this.Module) throw new Error("WASM not started");
143
+ const mod = this.Module as EmscriptenModule;
144
+ const malloc = mod._malloc as (n: number) => number;
145
+ const free = mod._free as (ptr: number) => void;
146
+ const read = mod.cwrap("redis_client_read", "number", [
147
+ "number",
148
+ "number",
149
+ "number",
150
+ ]) as (h: number, outPtrPtr: number, outLenPtr: number) => number;
151
+ const freeOut = mod.cwrap("redis_free", "void", ["number", "number"]) as (
152
+ ptr: number,
153
+ len: number,
154
+ ) => void;
155
+
156
+ const outPtrPtr = malloc(4);
157
+ const outLenPtr = malloc(4);
158
+ const rc = read(handle, outPtrPtr, outLenPtr);
159
+ if (rc !== 0) {
160
+ free(outPtrPtr);
161
+ free(outLenPtr);
162
+ throw new Error(`redis_client_read failed: ${rc}`);
163
+ }
164
+ const outPtr = mod.getValue(outPtrPtr, "i32") as number;
165
+ const outLen = mod.getValue(outLenPtr, "i32") as number;
166
+ free(outPtrPtr);
167
+ free(outLenPtr);
168
+ if (!outPtr || outLen === 0) return null;
169
+
170
+ const out = new Uint8Array(outLen);
171
+ for (let i = 0; i < outLen; i++)
172
+ out[i] = mod.getValue(outPtr + i, "i8") as number;
173
+ freeOut(outPtr, outLen);
174
+ return out;
175
+ }
176
+
177
+ private _clientFree(handle: number): void {
178
+ if (!this.Module) return;
179
+ const mod = this.Module as EmscriptenModule;
180
+ const fn = mod.cwrap("redis_client_free", "void", ["number"]) as (
181
+ h: number,
182
+ ) => void;
183
+ fn(handle);
184
+ }
185
+
186
+ private _clientWantsClose(handle: number): boolean {
187
+ if (!this.Module) return false;
188
+ const mod = this.Module as EmscriptenModule;
189
+ const fn = mod.cwrap("redis_client_wants_close", "number", [
190
+ "number",
191
+ ]) as (h: number) => number;
192
+ return fn(handle) !== 0;
193
+ }
194
+
195
+ createConnection() {
196
+ if (!this.Module) throw new Error("WASM not started");
197
+ const handle = this._createHandle();
198
+ return {
199
+ write: (data: Uint8Array | Buffer) => this._clientFeed(handle, data),
200
+ read: () => this._clientRead(handle),
201
+ wantsClose: () => this._clientWantsClose(handle),
202
+ close: () => this._clientFree(handle),
203
+ } as const;
204
+ }
205
+
206
+ async terminate() {
207
+ if (this._clientHandle !== null) {
208
+ this._clientFree(this._clientHandle);
209
+ }
210
+
211
+ this.Module = null;
212
+ this._clientHandle = null;
213
+ }
214
+
215
+ private loglevelToInt(level: string): number {
216
+ const s = level.toLowerCase();
217
+ if (s === "debug") return 0;
218
+ if (s === "verbose") return 1;
219
+ if (s === "notice") return 2;
220
+ return 3;
221
+ }
222
+ }