From 9bae18c1b3249ac4c8e550c7672daffdaa9e0de3 Mon Sep 17 00:00:00 2001
From: Shaya Potter <shaya@redislabs.com>
Date: Wed, 17 Apr 2024 15:39:35 +0300
Subject: [PATCH 1/7] add support for all hash field expiration commands

---
 packages/client/lib/commands/HEXPIRE.spec.ts  | 40 ++++++++++++++++
 packages/client/lib/commands/HEXPIRE.ts       | 42 ++++++++++++++++
 .../client/lib/commands/HEXPIREAT.spec.ts     | 48 +++++++++++++++++++
 packages/client/lib/commands/HEXPIREAT.ts     | 23 +++++++++
 .../client/lib/commands/HEXPIRETIME.spec.ts   | 33 +++++++++++++
 packages/client/lib/commands/HEXPIRETIME.ts   | 20 ++++++++
 packages/client/lib/commands/HPERSIST.spec.ts | 33 +++++++++++++
 packages/client/lib/commands/HPERSIST.ts      | 10 ++++
 packages/client/lib/commands/HPEXPIRE.spec.ts | 40 ++++++++++++++++
 packages/client/lib/commands/HPEXPIRE.ts      | 22 +++++++++
 .../client/lib/commands/HPEXPIREAT.spec.ts    | 48 +++++++++++++++++++
 packages/client/lib/commands/HPEXPIREAT.ts    | 23 +++++++++
 .../client/lib/commands/HPEXPIRETIME.spec.ts  | 33 +++++++++++++
 packages/client/lib/commands/HPEXPIRETIME.ts  | 11 +++++
 packages/client/lib/commands/HPTTL.spec.ts    | 33 +++++++++++++
 packages/client/lib/commands/HPTTL.ts         | 11 +++++
 packages/client/lib/commands/HTTL.spec.ts     | 34 +++++++++++++
 packages/client/lib/commands/HTTL.ts          | 11 +++++
 packages/client/lib/commands/index.ts         | 27 +++++++++++
 19 files changed, 542 insertions(+)
 create mode 100644 packages/client/lib/commands/HEXPIRE.spec.ts
 create mode 100644 packages/client/lib/commands/HEXPIRE.ts
 create mode 100644 packages/client/lib/commands/HEXPIREAT.spec.ts
 create mode 100644 packages/client/lib/commands/HEXPIREAT.ts
 create mode 100644 packages/client/lib/commands/HEXPIRETIME.spec.ts
 create mode 100644 packages/client/lib/commands/HEXPIRETIME.ts
 create mode 100644 packages/client/lib/commands/HPERSIST.spec.ts
 create mode 100644 packages/client/lib/commands/HPERSIST.ts
 create mode 100644 packages/client/lib/commands/HPEXPIRE.spec.ts
 create mode 100644 packages/client/lib/commands/HPEXPIRE.ts
 create mode 100644 packages/client/lib/commands/HPEXPIREAT.spec.ts
 create mode 100644 packages/client/lib/commands/HPEXPIREAT.ts
 create mode 100644 packages/client/lib/commands/HPEXPIRETIME.spec.ts
 create mode 100644 packages/client/lib/commands/HPEXPIRETIME.ts
 create mode 100644 packages/client/lib/commands/HPTTL.spec.ts
 create mode 100644 packages/client/lib/commands/HPTTL.ts
 create mode 100644 packages/client/lib/commands/HTTL.spec.ts
 create mode 100644 packages/client/lib/commands/HTTL.ts

diff --git a/packages/client/lib/commands/HEXPIRE.spec.ts b/packages/client/lib/commands/HEXPIRE.spec.ts
new file mode 100644
index 00000000000..d5440b0796d
--- /dev/null
+++ b/packages/client/lib/commands/HEXPIRE.spec.ts
@@ -0,0 +1,40 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import HEXPIRE from './HEXPIRE';
+
+describe('HEXPIRE', () => {
+  testUtils.isVersionGreaterThanHook([7, 4]);
+  
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        HEXPIRE.transformArguments('key', 'field', 1),
+        ['HEXPIRE', 'key', '1', '1', 'field']
+      );
+    });
+
+    it('array', () => {
+      assert.deepEqual(
+        HEXPIRE.transformArguments('key', ['field1', 'field2'], 1),
+        ['HEXPIRE', 'key', '1', '2', 'field1', 'field2']
+      );
+    });
+
+    it('with set option', () => {
+      assert.deepEqual(
+        HEXPIRE.transformArguments('key', 'field1', 1, 'NX'),
+        ['HEXPIRE', 'key', '1', 'NX', '1', 'field1']
+      );
+    });
+  });
+
+  testUtils.testAll('hexpire', async client => {
+    assert.equal(
+      await client.hExpire('key', ['field1'], 0),
+      null,
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/HEXPIRE.ts b/packages/client/lib/commands/HEXPIRE.ts
new file mode 100644
index 00000000000..c33348c4e80
--- /dev/null
+++ b/packages/client/lib/commands/HEXPIRE.ts
@@ -0,0 +1,42 @@
+import { Command, NullReply, RedisArgument } from "../RESP/types";
+import { RedisVariadicArgument, pushVariadicArgument } from "./generic-transformers";
+
+/**
+ * @readonly
+ * @enum {number}
+ */
+export const HASH_EXPIRATION = {
+  /** @property {number} */
+  /** The field does not exist */
+  FieldNotExists: -2,
+  /** @property {number} */
+  /** Specified NX | XX | GT | LT condition not met */
+  ConditionNotMet: 0,
+  /** @property {number} */
+  /** Expiration time was set or updated */
+  Updated: 1,
+  /** @property {number} */
+  /** Field deleted because the specified expiration time is in the past */
+  Deleted: 2
+} as const;
+  
+export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION];
+
+export default {
+  FIRST_KEY_INDEX: 1,
+  transformArguments(
+    key: RedisArgument, 
+    fields: RedisVariadicArgument,
+    seconds: number,
+    mode?: 'NX' | 'XX' | 'GT' | 'LT',
+  ) {
+    const args = ['HEXPIRE', key, seconds.toString()];
+
+    if (mode) {
+      args.push(mode);
+    }
+
+    return pushVariadicArgument(args, fields);
+  },
+  transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
+} as const satisfies Command;
\ No newline at end of file
diff --git a/packages/client/lib/commands/HEXPIREAT.spec.ts b/packages/client/lib/commands/HEXPIREAT.spec.ts
new file mode 100644
index 00000000000..47bc66eafe6
--- /dev/null
+++ b/packages/client/lib/commands/HEXPIREAT.spec.ts
@@ -0,0 +1,48 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import HEXPIREAT from './HEXPIREAT';
+
+describe('HEXPIREAT', () => {
+  testUtils.isVersionGreaterThanHook([7, 4]);
+  
+  describe('transformArguments', () => {
+    it('string + number', () => {
+      assert.deepEqual(
+        HEXPIREAT.transformArguments('key', 'field', 1),
+        ['HEXPIREAT', 'key', '1', '1', 'field']
+      );
+    });
+
+    it('array + number', () => {
+      assert.deepEqual(
+        HEXPIREAT.transformArguments('key', ['field1', 'field2'], 1),
+        ['HEXPIREAT', 'key', '1', '2', 'field1', 'field2']
+      );
+    });
+
+    it('date', () => {
+      const d = new Date();
+      assert.deepEqual(
+        HEXPIREAT.transformArguments('key', ['field1'], d),
+        ['HEXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString(), '1', 'field1']
+      );
+    });
+
+    it('with set option', () => {
+      assert.deepEqual(
+        HEXPIREAT.transformArguments('key', 'field1', 1, 'GT'),
+        ['HEXPIREAT', 'key', '1', 'GT', 1, 'field1']
+      );
+    });
+  });
+
+  testUtils.testAll('expireAt', async client => {
+    assert.equal(
+      await client.hExpireAt('key', 'field1', 1),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/HEXPIREAT.ts b/packages/client/lib/commands/HEXPIREAT.ts
new file mode 100644
index 00000000000..eccc2a1978b
--- /dev/null
+++ b/packages/client/lib/commands/HEXPIREAT.ts
@@ -0,0 +1,23 @@
+import { RedisArgument, Command, NullReply } from '../RESP/types';
+import { HashExpiration } from './HEXPIRE';
+import { RedisVariadicArgument, pushVariadicArgument, transformEXAT } from './generic-transformers';
+
+export default {
+  FIRST_KEY_INDEX: 1,
+  IS_READ_ONLY: true,
+  transformArguments(
+    key: RedisArgument,
+    fields: RedisVariadicArgument,
+    timestamp: number | Date,
+    mode?: 'NX' | 'XX' | 'GT' | 'LT'
+  ) {
+    const args = ['HEXPIREAT', key, transformEXAT(timestamp)];
+
+    if (mode) {
+      args.push(mode);
+    }
+
+    return pushVariadicArgument(args, fields);
+  },
+  transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HEXPIRETIME.spec.ts b/packages/client/lib/commands/HEXPIRETIME.spec.ts
new file mode 100644
index 00000000000..9bfb88f4dda
--- /dev/null
+++ b/packages/client/lib/commands/HEXPIRETIME.spec.ts
@@ -0,0 +1,33 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import HEXPIRETIME from './HEXPIRETIME';
+
+describe('HEXPIRETIME', () => {
+  testUtils.isVersionGreaterThanHook([7, 4]);
+
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        HEXPIRETIME.transformArguments('key', 'field'),
+        ['HEXPIRETIME', 'key', '1', 'field']
+      );
+    });
+
+    it('array', () => {
+      assert.deepEqual(
+        HEXPIRETIME.transformArguments('key', ['field1', 'field2']),
+        ['HEXPIRETIME', 'key', '2', 'field1', 'field2']
+      );
+    });
+  })
+
+  testUtils.testAll('hExpireTime', async client => {
+    assert.equal(
+      await client.hExpireTime('key', 'field1'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/HEXPIRETIME.ts b/packages/client/lib/commands/HEXPIRETIME.ts
new file mode 100644
index 00000000000..0184ebeaa8f
--- /dev/null
+++ b/packages/client/lib/commands/HEXPIRETIME.ts
@@ -0,0 +1,20 @@
+import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
+
+export const HASH_EXPIRATION_TIME = {
+  /** @property {number} */
+  /** The field does not exist */
+  FieldNotExists: -2,
+  /** @property {number} */
+  /** The field exists but has no associated expire */
+  NoExpiration: -1,
+} as const;
+    
+export default {
+  FIRST_KEY_INDEX: 1,
+  IS_READ_ONLY: true,
+  transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
+    return pushVariadicArgument(['HEXPIRETIME', key], fields);
+  },
+  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+} as const satisfies Command;
\ No newline at end of file
diff --git a/packages/client/lib/commands/HPERSIST.spec.ts b/packages/client/lib/commands/HPERSIST.spec.ts
new file mode 100644
index 00000000000..fb7f375c0b2
--- /dev/null
+++ b/packages/client/lib/commands/HPERSIST.spec.ts
@@ -0,0 +1,33 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import HPERSIST from './HPERSIST';
+
+describe('PERSIST', () => {
+  testUtils.isVersionGreaterThanHook([7, 4]);
+  
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        HPERSIST.transformArguments('key', 'field'),
+        ['PERSIST', 'key', '1', 'field']
+      );
+    });
+
+    it('array', () => {
+      assert.deepEqual(
+        HPERSIST.transformArguments('key', ['field1', 'field2']),
+        ['PERSIST', 'key', '2', 'field1', 'field2']
+      );
+    });
+  })
+
+  testUtils.testAll('hPersist', async client => {
+    assert.equal(
+      await client.hPersist('key', 'field1'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/HPERSIST.ts b/packages/client/lib/commands/HPERSIST.ts
new file mode 100644
index 00000000000..8f75e80aa96
--- /dev/null
+++ b/packages/client/lib/commands/HPERSIST.ts
@@ -0,0 +1,10 @@
+import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
+
+export default {
+  FIRST_KEY_INDEX: 1,
+  transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
+    return pushVariadicArgument(['PERSIST', key], fields);
+  },
+  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIRE.spec.ts b/packages/client/lib/commands/HPEXPIRE.spec.ts
new file mode 100644
index 00000000000..9dcea824cb8
--- /dev/null
+++ b/packages/client/lib/commands/HPEXPIRE.spec.ts
@@ -0,0 +1,40 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import HPEXPIRE from './HPEXPIRE';
+
+describe('HEXPIRE', () => {
+  testUtils.isVersionGreaterThanHook([7, 4]);
+  
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        HPEXPIRE.transformArguments('key', 'field', 1),
+        ['HPEXPIRE', 'key', '1', '1', 'field']
+      );
+    });
+
+    it('array', () => {
+      assert.deepEqual(
+        HPEXPIRE.transformArguments('key', ['field1', 'field2'], 1),
+        ['HPEXPIRE', 'key', '1', '2', 'field1', 'field2']
+      );
+    });
+
+    it('with set option', () => {
+      assert.deepEqual(
+        HPEXPIRE.transformArguments('key', ['field1'], 1, 'NX'),
+        ['HPEXPIRE', 'key', '1', 'NX', '1', 'field1']
+      );
+    });
+  });
+
+  testUtils.testAll('hexpire', async client => {
+    assert.equal(
+      await client.hpExpire('key', ['field1'], 0),
+      null,
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/HPEXPIRE.ts b/packages/client/lib/commands/HPEXPIRE.ts
new file mode 100644
index 00000000000..c20b1be4b34
--- /dev/null
+++ b/packages/client/lib/commands/HPEXPIRE.ts
@@ -0,0 +1,22 @@
+import { Command, NullReply, RedisArgument } from "../RESP/types";
+import { HashExpiration } from "./HEXPIRE";
+import { RedisVariadicArgument, pushVariadicArgument } from "./generic-transformers";
+
+export default {
+  FIRST_KEY_INDEX: 1,
+  transformArguments(
+    key: RedisArgument, 
+    fields: RedisVariadicArgument,
+    ms: number,
+    mode?: 'NX' | 'XX' | 'GT' | 'LT',
+  ) {
+    const args = ['HPEXPIRE', key, ms.toString()];
+
+    if (mode) {
+      args.push(mode);
+    }
+
+    return pushVariadicArgument(args, fields);
+  },
+  transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
+} as const satisfies Command;
\ No newline at end of file
diff --git a/packages/client/lib/commands/HPEXPIREAT.spec.ts b/packages/client/lib/commands/HPEXPIREAT.spec.ts
new file mode 100644
index 00000000000..1b48dbb3711
--- /dev/null
+++ b/packages/client/lib/commands/HPEXPIREAT.spec.ts
@@ -0,0 +1,48 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import HPEXPIREAT from './HPEXPIREAT';
+
+describe('HPEXPIREAT', () => {
+  testUtils.isVersionGreaterThanHook([7, 4]);
+  
+  describe('transformArguments', () => {
+    it('string + number', () => {
+      assert.deepEqual(
+        HPEXPIREAT.transformArguments('key', 'field', 1),
+        ['HPEXPIREAT', 'key', '1', '1', 'field']
+      );
+    });
+
+    it('array + number', () => {
+      assert.deepEqual(
+        HPEXPIREAT.transformArguments('key', ['field1', 'field2'], 1),
+        ['HPEXPIREAT', 'key', '1', '2', 'field1', 'field2']
+      );
+    });
+
+    it('date', () => {
+      const d = new Date();
+      assert.deepEqual(
+        HPEXPIREAT.transformArguments('key', ['field1'], d),
+        ['HPEXPIREAT', 'key', d.getTime().toString(), '1', 'field1']
+      );
+    });
+
+    it('with set option', () => {
+      assert.deepEqual(
+        HPEXPIREAT.transformArguments('key', ['field1'], 1, 'XX'),
+        ['HPEXPIREAT', 'key', '1', 'XX', '1', 'field1']
+      );
+    });
+  });
+
+  testUtils.testAll('hpExpireAt', async client => {
+    assert.equal(
+      await client.hpExpireAt('key', ['field1'], 1),
+      null,
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/HPEXPIREAT.ts b/packages/client/lib/commands/HPEXPIREAT.ts
new file mode 100644
index 00000000000..ca3cb95dfa4
--- /dev/null
+++ b/packages/client/lib/commands/HPEXPIREAT.ts
@@ -0,0 +1,23 @@
+import { RedisArgument, Command, NullReply } from '../RESP/types';
+import { HashExpiration } from './HEXPIRE';
+import { RedisVariadicArgument, pushVariadicArgument, transformEXAT } from './generic-transformers';
+
+export default {
+  FIRST_KEY_INDEX: 1,
+  IS_READ_ONLY: true,
+  transformArguments(
+    key: RedisArgument,
+    fields: RedisVariadicArgument,
+    timestamp: number | Date,
+    mode?: 'NX' | 'XX' | 'GT' | 'LT'
+  ) {
+    const args = ['HPEXPIREAT', key, transformEXAT(timestamp)];
+
+    if (mode) {
+      args.push(mode);
+    }
+
+    return pushVariadicArgument(args, fields);
+  },
+  transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIRETIME.spec.ts b/packages/client/lib/commands/HPEXPIRETIME.spec.ts
new file mode 100644
index 00000000000..e2b88b72802
--- /dev/null
+++ b/packages/client/lib/commands/HPEXPIRETIME.spec.ts
@@ -0,0 +1,33 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import HPEXPIRETIME from './HPEXPIRETIME';
+
+describe('HPEXPIRETIME', () => {
+  testUtils.isVersionGreaterThanHook([7, 4]);
+
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        HPEXPIRETIME.transformArguments('key', 'field'),
+        ['HPEXPIRETIME', 'key', '1', 'field']
+      );
+    });
+
+    it('array', () => {
+      assert.deepEqual(
+        HPEXPIRETIME.transformArguments('key', ['field1', 'field2']),
+        ['HPEXPIRETIME', 'key', '2', 'field1', 'field2']
+      );
+    });
+  });
+
+  testUtils.testAll('hpExpireTime', async client => {
+    assert.equal(
+      await client.hpExpireTime('key', 'field1'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts
new file mode 100644
index 00000000000..db0d30f3136
--- /dev/null
+++ b/packages/client/lib/commands/HPEXPIRETIME.ts
@@ -0,0 +1,11 @@
+import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
+   
+export default {
+  FIRST_KEY_INDEX: 1,
+  IS_READ_ONLY: true,
+  transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
+    return pushVariadicArgument(['HPEXPIRETIME', key], fields);
+  },
+  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+} as const satisfies Command;
\ No newline at end of file
diff --git a/packages/client/lib/commands/HPTTL.spec.ts b/packages/client/lib/commands/HPTTL.spec.ts
new file mode 100644
index 00000000000..30439aebe6a
--- /dev/null
+++ b/packages/client/lib/commands/HPTTL.spec.ts
@@ -0,0 +1,33 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import HPTTL from './HPTTL';
+
+describe('HPTTL', () => {
+  testUtils.isVersionGreaterThanHook([7, 4]);
+  
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        HPTTL.transformArguments('key', 'field'),
+        ['PTTL', 'key', '1', 'field']
+      );
+    });
+
+    it('array', () => {
+      assert.deepEqual(
+        HPTTL.transformArguments('key', ['field1', 'field2']),
+        ['PTTL', 'key', '2', 'field1', 'field2']
+      );
+    });
+  });
+
+  testUtils.testAll('hpTTL', async client => {
+    assert.equal(
+      await client.hpTTL('key', 'field1'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/HPTTL.ts b/packages/client/lib/commands/HPTTL.ts
new file mode 100644
index 00000000000..8f09bf5ba6d
--- /dev/null
+++ b/packages/client/lib/commands/HPTTL.ts
@@ -0,0 +1,11 @@
+import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
+
+export default {
+  FIRST_KEY_INDEX: 1,
+  IS_READ_ONLY: true,
+  transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
+    return pushVariadicArgument(['HPTTL', key], fields);
+  },
+  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HTTL.spec.ts b/packages/client/lib/commands/HTTL.spec.ts
new file mode 100644
index 00000000000..454e98d9287
--- /dev/null
+++ b/packages/client/lib/commands/HTTL.spec.ts
@@ -0,0 +1,34 @@
+import { strict as assert } from 'node:assert';
+import testUtils, { GLOBAL } from '../test-utils';
+import HTTL from './HTTL';
+
+describe('HTTL', () => {
+  testUtils.isVersionGreaterThanHook([7, 4]);
+  
+  describe('transformArguments', () => {
+    it('string', () => {
+      assert.deepEqual(
+        HTTL.transformArguments('key', 'field'),
+        ['TTL', 'key', '1', 'field']
+      );
+    });
+
+    it('array', () => {
+      assert.deepEqual(
+        HTTL.transformArguments('key', ['field1', 'field2']),
+        ['TTL', 'key', '2', 'field1', 'field2']
+      );
+    });
+  
+  });
+
+  testUtils.testAll('hTTL', async client => {
+    assert.equal(
+      await client.hTTL('key', 'field1'),
+      null
+    );
+  }, {
+    client: GLOBAL.SERVERS.OPEN,
+    cluster: GLOBAL.CLUSTERS.OPEN
+  });
+});
diff --git a/packages/client/lib/commands/HTTL.ts b/packages/client/lib/commands/HTTL.ts
new file mode 100644
index 00000000000..a1602729415
--- /dev/null
+++ b/packages/client/lib/commands/HTTL.ts
@@ -0,0 +1,11 @@
+import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
+
+export default {
+  FIRST_KEY_INDEX: 1,
+  IS_READ_ONLY: true,
+  transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
+    return pushVariadicArgument(['TTL', key], fields);
+  },
+  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts
index b2898988386..71455e34e53 100644
--- a/packages/client/lib/commands/index.ts
+++ b/packages/client/lib/commands/index.ts
@@ -133,6 +133,9 @@ import FUNCTION_STATS from './FUNCTION_STATS';
 import HDEL from './HDEL';
 import HELLO from './HELLO';
 import HEXISTS from './HEXISTS';
+import HEXPIRE from './HEXPIRE';
+import HEXPIREAT from './HEXPIREAT';
+import HEXPIRETIME from './HEXPIRETIME';
 import HGET from './HGET';
 import HGETALL from './HGETALL';
 import HINCRBY from './HINCRBY';
@@ -140,6 +143,12 @@ import HINCRBYFLOAT from './HINCRBYFLOAT';
 import HKEYS from './HKEYS';
 import HLEN from './HLEN';
 import HMGET from './HMGET';
+import HPERSIST from './HPERSIST';
+import HPEXPIRE from './HPEXPIRE';
+import HPEXPIREAT from './HPEXPIREAT';
+import HPEXPIRETIME from './HPEXPIRETIME';
+import HPTTL from './HPTTL';
+import HTTL from './HTTL';
 import HRANDFIELD_COUNT_WITHVALUES from './HRANDFIELD_COUNT_WITHVALUES';
 import HRANDFIELD_COUNT from './HRANDFIELD_COUNT';
 import HRANDFIELD from './HRANDFIELD';
@@ -601,6 +610,12 @@ export default {
   hello: HELLO,
   HEXISTS,
   hExists: HEXISTS,
+  HEXPIRE,
+  hExpire: HEXPIRE,
+  HEXPIREAT,
+  hExpireAt: HEXPIREAT,
+  HEXPIRETIME,
+  hExpireTime: HEXPIRETIME,
   HGET,
   hGet: HGET,
   HGETALL,
@@ -615,6 +630,18 @@ export default {
   hLen: HLEN,
   HMGET,
   hmGet: HMGET,
+  HPERSIST,
+  hPersist: HPERSIST,
+  HPEXPIRE,
+  hpExpire: HPEXPIRE,
+  HPEXPIREAT,
+  hpExpireAt: HPEXPIRE,
+  HPEXPIRETIME,
+  hpExpireTime: HPEXPIRETIME,
+  HPTTL,
+  hpTTL: HPTTL,
+  HTTL,
+  hTTL: HTTL,
   HRANDFIELD_COUNT_WITHVALUES,
   hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES,
   HRANDFIELD_COUNT,

From 0a872340a8c5bc5186bc47519e9e93f17cb2e906 Mon Sep 17 00:00:00 2001
From: Leibale Eidelman <me@leibale.com>
Date: Wed, 8 May 2024 12:13:06 -0400
Subject: [PATCH 2/7] Update HEXPIRE.spec.ts

---
 packages/client/lib/commands/HEXPIRE.spec.ts | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/client/lib/commands/HEXPIRE.spec.ts b/packages/client/lib/commands/HEXPIRE.spec.ts
index d5440b0796d..15edf724851 100644
--- a/packages/client/lib/commands/HEXPIRE.spec.ts
+++ b/packages/client/lib/commands/HEXPIRE.spec.ts
@@ -28,10 +28,10 @@ describe('HEXPIRE', () => {
     });
   });
 
-  testUtils.testAll('hexpire', async client => {
+  testUtils.testAll('hExpire', async client => {
     assert.equal(
       await client.hExpire('key', ['field1'], 0),
-      null,
+      null
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,

From 69be96fe010bd6622318815c07d4e214373db017 Mon Sep 17 00:00:00 2001
From: Leibale Eidelman <me@leibale.com>
Date: Wed, 8 May 2024 12:15:29 -0400
Subject: [PATCH 3/7] Update HEXPIRE.ts

---
 packages/client/lib/commands/HEXPIRE.ts | 18 +++++-------------
 1 file changed, 5 insertions(+), 13 deletions(-)

diff --git a/packages/client/lib/commands/HEXPIRE.ts b/packages/client/lib/commands/HEXPIRE.ts
index c33348c4e80..570213d14ac 100644
--- a/packages/client/lib/commands/HEXPIRE.ts
+++ b/packages/client/lib/commands/HEXPIRE.ts
@@ -1,23 +1,15 @@
 import { Command, NullReply, RedisArgument } from "../RESP/types";
 import { RedisVariadicArgument, pushVariadicArgument } from "./generic-transformers";
 
-/**
- * @readonly
- * @enum {number}
- */
 export const HASH_EXPIRATION = {
-  /** @property {number} */
   /** The field does not exist */
-  FieldNotExists: -2,
-  /** @property {number} */
+  FIELD_NOT_EXISTS: -2,
   /** Specified NX | XX | GT | LT condition not met */
-  ConditionNotMet: 0,
-  /** @property {number} */
+  CONDITION_NOT_MET: 0,
   /** Expiration time was set or updated */
-  Updated: 1,
-  /** @property {number} */
+  UPDATED: 1,
   /** Field deleted because the specified expiration time is in the past */
-  Deleted: 2
+  DELETED: 2
 } as const;
   
 export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION];
@@ -39,4 +31,4 @@ export default {
     return pushVariadicArgument(args, fields);
   },
   transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
-} as const satisfies Command;
\ No newline at end of file
+} as const satisfies Command;

From 5ceb5f5ad389a392eece11c8332e7eddb50696a9 Mon Sep 17 00:00:00 2001
From: Leibale Eidelman <me@leibale.com>
Date: Wed, 8 May 2024 17:34:12 -0400
Subject: [PATCH 4/7] some cleanups

---
 packages/client/lib/commands/HEXPIRETIME.spec.ts  |  2 +-
 packages/client/lib/commands/HEXPIRETIME.ts       | 11 +----------
 packages/client/lib/commands/HPERSIST.spec.ts     |  6 +++---
 packages/client/lib/commands/HPERSIST.ts          |  2 +-
 packages/client/lib/commands/HPEXPIRE.spec.ts     |  8 ++++----
 packages/client/lib/commands/HPEXPIRE.ts          |  2 +-
 packages/client/lib/commands/HPEXPIRETIME.spec.ts |  2 +-
 packages/client/lib/commands/HPEXPIRETIME.ts      |  2 +-
 packages/client/lib/commands/HPTTL.spec.ts        |  6 +++---
 packages/client/lib/commands/HTTL.spec.ts         |  7 +++----
 packages/client/lib/commands/HTTL.ts              |  2 +-
 11 files changed, 20 insertions(+), 30 deletions(-)

diff --git a/packages/client/lib/commands/HEXPIRETIME.spec.ts b/packages/client/lib/commands/HEXPIRETIME.spec.ts
index 9bfb88f4dda..2e3396460f0 100644
--- a/packages/client/lib/commands/HEXPIRETIME.spec.ts
+++ b/packages/client/lib/commands/HEXPIRETIME.spec.ts
@@ -23,7 +23,7 @@ describe('HEXPIRETIME', () => {
 
   testUtils.testAll('hExpireTime', async client => {
     assert.equal(
-      await client.hExpireTime('key', 'field1'),
+      await client.hExpireTime('key', 'field'),
       null
     );
   }, {
diff --git a/packages/client/lib/commands/HEXPIRETIME.ts b/packages/client/lib/commands/HEXPIRETIME.ts
index 0184ebeaa8f..f612a58d99c 100644
--- a/packages/client/lib/commands/HEXPIRETIME.ts
+++ b/packages/client/lib/commands/HEXPIRETIME.ts
@@ -1,14 +1,5 @@
 import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
 import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
-
-export const HASH_EXPIRATION_TIME = {
-  /** @property {number} */
-  /** The field does not exist */
-  FieldNotExists: -2,
-  /** @property {number} */
-  /** The field exists but has no associated expire */
-  NoExpiration: -1,
-} as const;
     
 export default {
   FIRST_KEY_INDEX: 1,
@@ -17,4 +8,4 @@ export default {
     return pushVariadicArgument(['HEXPIRETIME', key], fields);
   },
   transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
-} as const satisfies Command;
\ No newline at end of file
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPERSIST.spec.ts b/packages/client/lib/commands/HPERSIST.spec.ts
index fb7f375c0b2..61e8f21e675 100644
--- a/packages/client/lib/commands/HPERSIST.spec.ts
+++ b/packages/client/lib/commands/HPERSIST.spec.ts
@@ -2,21 +2,21 @@ import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
 import HPERSIST from './HPERSIST';
 
-describe('PERSIST', () => {
+describe('HPERSIST', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
   
   describe('transformArguments', () => {
     it('string', () => {
       assert.deepEqual(
         HPERSIST.transformArguments('key', 'field'),
-        ['PERSIST', 'key', '1', 'field']
+        ['HPERSIST', 'key', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
         HPERSIST.transformArguments('key', ['field1', 'field2']),
-        ['PERSIST', 'key', '2', 'field1', 'field2']
+        ['HPERSIST', 'key', '2', 'field1', 'field2']
       );
     });
   })
diff --git a/packages/client/lib/commands/HPERSIST.ts b/packages/client/lib/commands/HPERSIST.ts
index 8f75e80aa96..955142c40e4 100644
--- a/packages/client/lib/commands/HPERSIST.ts
+++ b/packages/client/lib/commands/HPERSIST.ts
@@ -4,7 +4,7 @@ import { RedisVariadicArgument, pushVariadicArgument } from './generic-transform
 export default {
   FIRST_KEY_INDEX: 1,
   transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
-    return pushVariadicArgument(['PERSIST', key], fields);
+    return pushVariadicArgument(['HPERSIST', key], fields);
   },
   transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
 } as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIRE.spec.ts b/packages/client/lib/commands/HPEXPIRE.spec.ts
index 9dcea824cb8..9fcb0e31f93 100644
--- a/packages/client/lib/commands/HPEXPIRE.spec.ts
+++ b/packages/client/lib/commands/HPEXPIRE.spec.ts
@@ -2,7 +2,7 @@ import { strict as assert } from 'node:assert';
 import testUtils, { GLOBAL } from '../test-utils';
 import HPEXPIRE from './HPEXPIRE';
 
-describe('HEXPIRE', () => {
+describe('HPEXPIRE', () => {
   testUtils.isVersionGreaterThanHook([7, 4]);
   
   describe('transformArguments', () => {
@@ -28,10 +28,10 @@ describe('HEXPIRE', () => {
     });
   });
 
-  testUtils.testAll('hexpire', async client => {
+  testUtils.testAll('hpRxpire', async client => {
     assert.equal(
-      await client.hpExpire('key', ['field1'], 0),
-      null,
+      await client.hpExpire('key', ['field'], 0),
+      null
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HPEXPIRE.ts b/packages/client/lib/commands/HPEXPIRE.ts
index c20b1be4b34..e205f32f866 100644
--- a/packages/client/lib/commands/HPEXPIRE.ts
+++ b/packages/client/lib/commands/HPEXPIRE.ts
@@ -19,4 +19,4 @@ export default {
     return pushVariadicArgument(args, fields);
   },
   transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
-} as const satisfies Command;
\ No newline at end of file
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIRETIME.spec.ts b/packages/client/lib/commands/HPEXPIRETIME.spec.ts
index e2b88b72802..de966d479be 100644
--- a/packages/client/lib/commands/HPEXPIRETIME.spec.ts
+++ b/packages/client/lib/commands/HPEXPIRETIME.spec.ts
@@ -23,7 +23,7 @@ describe('HPEXPIRETIME', () => {
 
   testUtils.testAll('hpExpireTime', async client => {
     assert.equal(
-      await client.hpExpireTime('key', 'field1'),
+      await client.hpExpireTime('key', 'field'),
       null
     );
   }, {
diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts
index db0d30f3136..a79acf6b176 100644
--- a/packages/client/lib/commands/HPEXPIRETIME.ts
+++ b/packages/client/lib/commands/HPEXPIRETIME.ts
@@ -8,4 +8,4 @@ export default {
     return pushVariadicArgument(['HPEXPIRETIME', key], fields);
   },
   transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
-} as const satisfies Command;
\ No newline at end of file
+} as const satisfies Command;
diff --git a/packages/client/lib/commands/HPTTL.spec.ts b/packages/client/lib/commands/HPTTL.spec.ts
index 30439aebe6a..05e4713ed78 100644
--- a/packages/client/lib/commands/HPTTL.spec.ts
+++ b/packages/client/lib/commands/HPTTL.spec.ts
@@ -9,21 +9,21 @@ describe('HPTTL', () => {
     it('string', () => {
       assert.deepEqual(
         HPTTL.transformArguments('key', 'field'),
-        ['PTTL', 'key', '1', 'field']
+        ['HPTTL', 'key', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
         HPTTL.transformArguments('key', ['field1', 'field2']),
-        ['PTTL', 'key', '2', 'field1', 'field2']
+        ['HPTTL', 'key', '2', 'field1', 'field2']
       );
     });
   });
 
   testUtils.testAll('hpTTL', async client => {
     assert.equal(
-      await client.hpTTL('key', 'field1'),
+      await client.hpTTL('key', 'field'),
       null
     );
   }, {
diff --git a/packages/client/lib/commands/HTTL.spec.ts b/packages/client/lib/commands/HTTL.spec.ts
index 454e98d9287..9f6cf99e93a 100644
--- a/packages/client/lib/commands/HTTL.spec.ts
+++ b/packages/client/lib/commands/HTTL.spec.ts
@@ -9,22 +9,21 @@ describe('HTTL', () => {
     it('string', () => {
       assert.deepEqual(
         HTTL.transformArguments('key', 'field'),
-        ['TTL', 'key', '1', 'field']
+        ['HTTL', 'key', '1', 'field']
       );
     });
 
     it('array', () => {
       assert.deepEqual(
         HTTL.transformArguments('key', ['field1', 'field2']),
-        ['TTL', 'key', '2', 'field1', 'field2']
+        ['HTTL', 'key', '2', 'field1', 'field2']
       );
     });
-  
   });
 
   testUtils.testAll('hTTL', async client => {
     assert.equal(
-      await client.hTTL('key', 'field1'),
+      await client.hTTL('key', 'field'),
       null
     );
   }, {
diff --git a/packages/client/lib/commands/HTTL.ts b/packages/client/lib/commands/HTTL.ts
index a1602729415..5964a7aa19a 100644
--- a/packages/client/lib/commands/HTTL.ts
+++ b/packages/client/lib/commands/HTTL.ts
@@ -5,7 +5,7 @@ export default {
   FIRST_KEY_INDEX: 1,
   IS_READ_ONLY: true,
   transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
-    return pushVariadicArgument(['TTL', key], fields);
+    return pushVariadicArgument(['HTTL', key], fields);
   },
   transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
 } as const satisfies Command;

From 6a2c58bf081c787f56a4273827539165db3c0547 Mon Sep 17 00:00:00 2001
From: Leibale Eidelman <me@leibale.com>
Date: Thu, 6 Jun 2024 17:06:57 -0400
Subject: [PATCH 5/7] some api changes

---
 packages/client/lib/commands/HEXPIRE.spec.ts      | 4 ++--
 packages/client/lib/commands/HEXPIRE.ts           | 6 ++++--
 packages/client/lib/commands/HEXPIREAT.spec.ts    | 4 ++--
 packages/client/lib/commands/HEXPIREAT.ts         | 6 +++---
 packages/client/lib/commands/HEXPIRETIME.spec.ts  | 2 +-
 packages/client/lib/commands/HEXPIRETIME.ts       | 4 ++--
 packages/client/lib/commands/HPERSIST.spec.ts     | 4 ++--
 packages/client/lib/commands/HPERSIST.ts          | 4 ++--
 packages/client/lib/commands/HPEXPIRE.spec.ts     | 4 ++--
 packages/client/lib/commands/HPEXPIRE.ts          | 8 ++++----
 packages/client/lib/commands/HPEXPIREAT.spec.ts   | 4 ++--
 packages/client/lib/commands/HPEXPIREAT.ts        | 6 +++---
 packages/client/lib/commands/HPEXPIRETIME.spec.ts | 2 +-
 packages/client/lib/commands/HPEXPIRETIME.ts      | 4 ++--
 packages/client/lib/commands/HPTTL.spec.ts        | 2 +-
 packages/client/lib/commands/HPTTL.ts             | 4 ++--
 packages/client/lib/commands/HTTL.spec.ts         | 2 +-
 packages/client/lib/commands/HTTL.ts              | 4 ++--
 18 files changed, 38 insertions(+), 36 deletions(-)

diff --git a/packages/client/lib/commands/HEXPIRE.spec.ts b/packages/client/lib/commands/HEXPIRE.spec.ts
index 15edf724851..83afa14f3b6 100644
--- a/packages/client/lib/commands/HEXPIRE.spec.ts
+++ b/packages/client/lib/commands/HEXPIRE.spec.ts
@@ -30,8 +30,8 @@ describe('HEXPIRE', () => {
 
   testUtils.testAll('hExpire', async client => {
     assert.equal(
-      await client.hExpire('key', ['field1'], 0),
-      null
+      await client.hExpire('key', 'field', 0),
+      [-2]
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HEXPIRE.ts b/packages/client/lib/commands/HEXPIRE.ts
index 570213d14ac..228b7931520 100644
--- a/packages/client/lib/commands/HEXPIRE.ts
+++ b/packages/client/lib/commands/HEXPIRE.ts
@@ -1,4 +1,4 @@
-import { Command, NullReply, RedisArgument } from "../RESP/types";
+import { Command, NumberReply, ArrayReply, RedisArgument } from "../RESP/types";
 import { RedisVariadicArgument, pushVariadicArgument } from "./generic-transformers";
 
 export const HASH_EXPIRATION = {
@@ -14,6 +14,8 @@ export const HASH_EXPIRATION = {
   
 export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION];
 
+export type HashExpirationReply = NumberReply<HashExpiration>;
+
 export default {
   FIRST_KEY_INDEX: 1,
   transformArguments(
@@ -30,5 +32,5 @@ export default {
 
     return pushVariadicArgument(args, fields);
   },
-  transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
+  transformReply: undefined as unknown as () => ArrayReply<HashExpirationReply>
 } as const satisfies Command;
diff --git a/packages/client/lib/commands/HEXPIREAT.spec.ts b/packages/client/lib/commands/HEXPIREAT.spec.ts
index 47bc66eafe6..8fc5ebb4b15 100644
--- a/packages/client/lib/commands/HEXPIREAT.spec.ts
+++ b/packages/client/lib/commands/HEXPIREAT.spec.ts
@@ -38,8 +38,8 @@ describe('HEXPIREAT', () => {
 
   testUtils.testAll('expireAt', async client => {
     assert.equal(
-      await client.hExpireAt('key', 'field1', 1),
-      null
+      await client.hExpireAt('key', 'field', 1),
+      [-2]
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HEXPIREAT.ts b/packages/client/lib/commands/HEXPIREAT.ts
index eccc2a1978b..e4b062ae8b3 100644
--- a/packages/client/lib/commands/HEXPIREAT.ts
+++ b/packages/client/lib/commands/HEXPIREAT.ts
@@ -1,5 +1,5 @@
-import { RedisArgument, Command, NullReply } from '../RESP/types';
-import { HashExpiration } from './HEXPIRE';
+import { RedisArgument, ArrayReply, Command } from '../RESP/types';
+import { HashExpirationReply } from './HEXPIRE';
 import { RedisVariadicArgument, pushVariadicArgument, transformEXAT } from './generic-transformers';
 
 export default {
@@ -19,5 +19,5 @@ export default {
 
     return pushVariadicArgument(args, fields);
   },
-  transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
+  transformReply: undefined as unknown as () => ArrayReply<HashExpirationReply>
 } as const satisfies Command;
diff --git a/packages/client/lib/commands/HEXPIRETIME.spec.ts b/packages/client/lib/commands/HEXPIRETIME.spec.ts
index 2e3396460f0..ffed3232a91 100644
--- a/packages/client/lib/commands/HEXPIRETIME.spec.ts
+++ b/packages/client/lib/commands/HEXPIRETIME.spec.ts
@@ -24,7 +24,7 @@ describe('HEXPIRETIME', () => {
   testUtils.testAll('hExpireTime', async client => {
     assert.equal(
       await client.hExpireTime('key', 'field'),
-      null
+      [-2]
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HEXPIRETIME.ts b/packages/client/lib/commands/HEXPIRETIME.ts
index f612a58d99c..c17cc12d17a 100644
--- a/packages/client/lib/commands/HEXPIRETIME.ts
+++ b/packages/client/lib/commands/HEXPIRETIME.ts
@@ -1,4 +1,4 @@
-import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
 import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
     
 export default {
@@ -7,5 +7,5 @@ export default {
   transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
     return pushVariadicArgument(['HEXPIRETIME', key], fields);
   },
-  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
 } as const satisfies Command;
diff --git a/packages/client/lib/commands/HPERSIST.spec.ts b/packages/client/lib/commands/HPERSIST.spec.ts
index 61e8f21e675..a7c968ffaad 100644
--- a/packages/client/lib/commands/HPERSIST.spec.ts
+++ b/packages/client/lib/commands/HPERSIST.spec.ts
@@ -23,8 +23,8 @@ describe('HPERSIST', () => {
 
   testUtils.testAll('hPersist', async client => {
     assert.equal(
-      await client.hPersist('key', 'field1'),
-      null
+      await client.hPersist('key', 'field'),
+      [-2]
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HPERSIST.ts b/packages/client/lib/commands/HPERSIST.ts
index 955142c40e4..cbcf7259782 100644
--- a/packages/client/lib/commands/HPERSIST.ts
+++ b/packages/client/lib/commands/HPERSIST.ts
@@ -1,4 +1,4 @@
-import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
 import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
 
 export default {
@@ -6,5 +6,5 @@ export default {
   transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
     return pushVariadicArgument(['HPERSIST', key], fields);
   },
-  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
 } as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIRE.spec.ts b/packages/client/lib/commands/HPEXPIRE.spec.ts
index 9fcb0e31f93..4a60813b436 100644
--- a/packages/client/lib/commands/HPEXPIRE.spec.ts
+++ b/packages/client/lib/commands/HPEXPIRE.spec.ts
@@ -30,8 +30,8 @@ describe('HPEXPIRE', () => {
 
   testUtils.testAll('hpRxpire', async client => {
     assert.equal(
-      await client.hpExpire('key', ['field'], 0),
-      null
+      await client.hpExpire('key', 'field', 0),
+      [-2]
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HPEXPIRE.ts b/packages/client/lib/commands/HPEXPIRE.ts
index e205f32f866..47cea066844 100644
--- a/packages/client/lib/commands/HPEXPIRE.ts
+++ b/packages/client/lib/commands/HPEXPIRE.ts
@@ -1,6 +1,6 @@
-import { Command, NullReply, RedisArgument } from "../RESP/types";
-import { HashExpiration } from "./HEXPIRE";
-import { RedisVariadicArgument, pushVariadicArgument } from "./generic-transformers";
+import { RedisArgument, ArrayReply, Command } from '../RESP/types';
+import { HashExpirationReply } from './HEXPIRE';
+import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
 
 export default {
   FIRST_KEY_INDEX: 1,
@@ -18,5 +18,5 @@ export default {
 
     return pushVariadicArgument(args, fields);
   },
-  transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
+  transformReply: undefined as unknown as () => ArrayReply<HashExpirationReply>
 } as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIREAT.spec.ts b/packages/client/lib/commands/HPEXPIREAT.spec.ts
index 1b48dbb3711..bdf916bd3de 100644
--- a/packages/client/lib/commands/HPEXPIREAT.spec.ts
+++ b/packages/client/lib/commands/HPEXPIREAT.spec.ts
@@ -38,8 +38,8 @@ describe('HPEXPIREAT', () => {
 
   testUtils.testAll('hpExpireAt', async client => {
     assert.equal(
-      await client.hpExpireAt('key', ['field1'], 1),
-      null,
+      await client.hpExpireAt('key', 'field', 1),
+      [-2]
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HPEXPIREAT.ts b/packages/client/lib/commands/HPEXPIREAT.ts
index ca3cb95dfa4..f05a0fc2f53 100644
--- a/packages/client/lib/commands/HPEXPIREAT.ts
+++ b/packages/client/lib/commands/HPEXPIREAT.ts
@@ -1,5 +1,5 @@
-import { RedisArgument, Command, NullReply } from '../RESP/types';
-import { HashExpiration } from './HEXPIRE';
+import { RedisArgument, ArrayReply, Command } from '../RESP/types';
+import { HashExpirationReply } from './HEXPIRE';
 import { RedisVariadicArgument, pushVariadicArgument, transformEXAT } from './generic-transformers';
 
 export default {
@@ -19,5 +19,5 @@ export default {
 
     return pushVariadicArgument(args, fields);
   },
-  transformReply: undefined as unknown as () => NullReply | Array<HashExpiration>
+  transformReply: undefined as unknown as () => ArrayReply<HashExpirationReply>
 } as const satisfies Command;
diff --git a/packages/client/lib/commands/HPEXPIRETIME.spec.ts b/packages/client/lib/commands/HPEXPIRETIME.spec.ts
index de966d479be..4fbdb4b97b6 100644
--- a/packages/client/lib/commands/HPEXPIRETIME.spec.ts
+++ b/packages/client/lib/commands/HPEXPIRETIME.spec.ts
@@ -24,7 +24,7 @@ describe('HPEXPIRETIME', () => {
   testUtils.testAll('hpExpireTime', async client => {
     assert.equal(
       await client.hpExpireTime('key', 'field'),
-      null
+      [-2]
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts
index a79acf6b176..cec13e786fc 100644
--- a/packages/client/lib/commands/HPEXPIRETIME.ts
+++ b/packages/client/lib/commands/HPEXPIRETIME.ts
@@ -1,4 +1,4 @@
-import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
 import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
    
 export default {
@@ -7,5 +7,5 @@ export default {
   transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
     return pushVariadicArgument(['HPEXPIRETIME', key], fields);
   },
-  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
 } as const satisfies Command;
diff --git a/packages/client/lib/commands/HPTTL.spec.ts b/packages/client/lib/commands/HPTTL.spec.ts
index 05e4713ed78..949802091ca 100644
--- a/packages/client/lib/commands/HPTTL.spec.ts
+++ b/packages/client/lib/commands/HPTTL.spec.ts
@@ -24,7 +24,7 @@ describe('HPTTL', () => {
   testUtils.testAll('hpTTL', async client => {
     assert.equal(
       await client.hpTTL('key', 'field'),
-      null
+      [-2]
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HPTTL.ts b/packages/client/lib/commands/HPTTL.ts
index 8f09bf5ba6d..584c7e75b91 100644
--- a/packages/client/lib/commands/HPTTL.ts
+++ b/packages/client/lib/commands/HPTTL.ts
@@ -1,4 +1,4 @@
-import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
 import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
 
 export default {
@@ -7,5 +7,5 @@ export default {
   transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
     return pushVariadicArgument(['HPTTL', key], fields);
   },
-  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
 } as const satisfies Command;
diff --git a/packages/client/lib/commands/HTTL.spec.ts b/packages/client/lib/commands/HTTL.spec.ts
index 9f6cf99e93a..5b3dbfa0916 100644
--- a/packages/client/lib/commands/HTTL.spec.ts
+++ b/packages/client/lib/commands/HTTL.spec.ts
@@ -24,7 +24,7 @@ describe('HTTL', () => {
   testUtils.testAll('hTTL', async client => {
     assert.equal(
       await client.hTTL('key', 'field'),
-      null
+      [-2]
     );
   }, {
     client: GLOBAL.SERVERS.OPEN,
diff --git a/packages/client/lib/commands/HTTL.ts b/packages/client/lib/commands/HTTL.ts
index 5964a7aa19a..c59ad4ceccf 100644
--- a/packages/client/lib/commands/HTTL.ts
+++ b/packages/client/lib/commands/HTTL.ts
@@ -1,4 +1,4 @@
-import { RedisArgument, NumberReply, Command, NullReply, ArrayReply } from '../RESP/types';
+import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
 import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
 
 export default {
@@ -7,5 +7,5 @@ export default {
   transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
     return pushVariadicArgument(['HTTL', key], fields);
   },
-  transformReply: undefined as unknown as () => NullReply | ArrayReply<NumberReply>
+  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
 } as const satisfies Command;

From 986bd848e4e02af6a23b635f695cb8eddac44803 Mon Sep 17 00:00:00 2001
From: Shaya Potter <shaya@redislabs.com>
Date: Fri, 7 Jun 2024 00:51:37 +0300
Subject: [PATCH 6/7] add enum for HPERSIST reply

---
 packages/client/lib/commands/HPERSIST.ts | 15 ++++++++++++++-
 1 file changed, 14 insertions(+), 1 deletion(-)

diff --git a/packages/client/lib/commands/HPERSIST.ts b/packages/client/lib/commands/HPERSIST.ts
index cbcf7259782..97fd8b6c0dd 100644
--- a/packages/client/lib/commands/HPERSIST.ts
+++ b/packages/client/lib/commands/HPERSIST.ts
@@ -1,10 +1,23 @@
 import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
 import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
 
+export const HASH_FIELD_PERSIST = {
+  /** The expiration time was removed */
+  EXPIRATION_REMOVED: 1,
+  /** The field has no expiration time */
+  NO_EXPIRATION_TIME: -1,
+  /** The field does not exist */
+  FIELD_NOT_EXISTS: -2,
+} as const;
+
+export type HashFieldPersist = typeof HASH_FIELD_PERSIST[keyof typeof HASH_FIELD_PERSIST];
+
+export type HashFieldPersistReply = NumberReply<HashFieldPersist>;
+
 export default {
   FIRST_KEY_INDEX: 1,
   transformArguments(key: RedisArgument, fields: RedisVariadicArgument) {
     return pushVariadicArgument(['HPERSIST', key], fields);
   },
-  transformReply: undefined as unknown as () => ArrayReply<NumberReply>
+  transformReply: undefined as unknown as () => ArrayReply<HashFieldPersistReply>
 } as const satisfies Command;

From 9252bf271fe88f0e96654dfc1d099e013da565f8 Mon Sep 17 00:00:00 2001
From: Shaya Potter <shaya@redislabs.com>
Date: Tue, 11 Jun 2024 10:38:08 +0300
Subject: [PATCH 7/7] add enum for "error" codes for HPEXPIRETIME, but unsure
 how to expose to user

---
 packages/client/lib/commands/HPEXPIRETIME.ts | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts
index cec13e786fc..93b6f5a7ee6 100644
--- a/packages/client/lib/commands/HPEXPIRETIME.ts
+++ b/packages/client/lib/commands/HPEXPIRETIME.ts
@@ -1,6 +1,13 @@
 import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types';
 import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers';
-   
+
+export const HASH_FIELD_EXPIRETIME = {
+  /** The field does not exist */
+  FIELD_NOT_EXISTS: -2,
+  /** The field has no expiration time */
+  NO_EXPIRATION_TIME: -1,
+} as const;
+
 export default {
   FIRST_KEY_INDEX: 1,
   IS_READ_ONLY: true,