Valkey مع Node.js و TypeScript: دليل إعداد
١٦ يونيو ٢٠٢٦
للاتصال بـ Valkey من تطبيق Node.js و TypeScript، قم بتثبيت عميل iovalkey، وأنشئ مثيلاً (instance) بـ new Valkey({ host, port })، وأرفق دائماً مستمعاً للخطأ error حتى لا يتسبب انقطاع الاتصال في تعطل عمليتك. يبني هذا الدليل ذلك العميل من البداية إلى النهاية مقابل نسخة Valkey تعمل على Docker.
ملخص
يربط هذا الدليل العملي عميل Valkey جاهز للإنتاج بمشروع Node.js و TypeScript باستخدام iovalkey 0.3.31، وهو عميل Valkey المتوافق مع ioredis والمصمم خصيصاً لـ TypeScript. ستقوم بتشغيل Valkey 8.1.82 محلياً باستخدام Docker Compose، وبناء وحدة عميل مكتوبة (typed) مع استراتيجية إعادة محاولة ومستمع الخطأ الإلزامي، ثم ممارسة التخزين المؤقت المكتوب مع TTLs، والعمليات المتسلسلة (pipelines)، والمعاملات الذرية، و pub/sub، وفحص الحالة (health check)، والإغلاق الآمن. التقنيات المستخدمة: iovalkey 0.3.3، TypeScript 6.0.33، tsx 4.22.44، @types/node 25.9.3 على Node.js 22. تم فحص كل مثال TypeScript هنا تحت الوضع الصارم (strict mode)، وتم تشغيل العرض التوضيحي الكامل من البداية للنهاية مقابل Valkey 8.1.8 حي في 16 يونيو 2026. الميزانية الزمنية حوالي 30-40 دقيقة.
ما ستتعلمه
- كيفية اختيار عميل Valkey لـ Node.js: iovalkey مقابل ioredis مقابل valkey-glide
- كيفية تشغيل Valkey محلياً باستخدام Docker Compose، مع تثبيت الإصدار وضمان استمرارية البيانات
- كيفية بناء وحدة عميل TypeScript للإنتاج مع استراتيجية إعادة محاولة ومستمع الخطأ الذي يمنع Valkey من تعطيل عمليتك
- كيفية إجراء تخزين مؤقت آمن النوع (type-safe) باستخدام
SET/GETو TTLs - كيفية تنفيذ الأوامر دفعة واحدة باستخدام pipelines وتشغيل المعاملات الذرية (
MULTI/EXEC) - كيفية استخدام pub/sub على اتصال مخصص
- كيفية إضافة فحص حالة وإغلاق آمن، واستكشاف أخطاء الاتصال الشائعة وإصلاحها
اختيار عميل Valkey: iovalkey مقابل ioredis مقابل valkey-glide
Valkey هو تفرع (fork) من مؤسسة Linux لـ Redis، تم إنشاؤه بعد أن قامت شركة Redis Ltd. بتغيير ترخيصها بعيداً عن BSD في مارس 2024؛ بدأ Valkey من Redis 7.2.4، وهو آخر إصدار مرخص بـ BSD.5 ولأن Valkey حافظ على نفس بروتوكول RESP ومجموعة الأوامر، فإن أي عميل Redis يعمل مع Valkey — ولكن في عام 2026 لديك ثلاثة خيارات منطقية، ويعتمد الخيار الصحيح على كودك الحالي.
| العميل | الحزمة | اللغة | الترخيص | الأفضل لـ |
|---|---|---|---|---|
| iovalkey | iovalkey | TypeScript | MIT | الفرق التي تستخدم بالفعل ioredis — فهو تفرع صديق بنفس الـ API1 |
| valkey-glide | @valkey/valkey-glide | نواة Rust + روابط Node | Apache-2.0 | التطبيقات الجديدة التي تريد العميل الرسمي المصمم للموثوقية والتوافر العالي (HA)6 |
| node-Redis | Redis | JavaScript | MIT | الفرق التي تستخدم بالفعل عميل Redis الرسمي وتريد أقل قدر من التغيير7 |
iovalkey هو تفرع مباشر لـ ioredis تم أخذه مباشرة بعد التزام (commit) محدد في المصدر، لذا فإن الـ API الخاص به يعكس ioredis: نفس المنشئ (constructor)، نفس الأحداث، ونفس بناة pipeline() و multi().1 إذا كنت تستخدم بالفعل ioredis، فإن الهجرة عادة ما تكون مجرد تبديل سطر واحد في التبعيات. إنه مكتوب بالكامل بـ TypeScript ويشحن تعريفات الأنواع الخاصة به، لذا لا توجد حزمة @types منفصلة لتثبيتها.1
valkey-glide هو عميل Valkey الرسمي. يستخدم نواة Rust مشتركة مع روابط لغوية وهو مصمم حول الموثوقية والتوافر العالي، لكن الـ API الخاص به يختلف عن ioredis — على سبيل المثال، تأخذ أوامر المفاتيح المتعددة مصفوفات بدلاً من وسيطات موزعة.6 وهذا يجعله مناسباً للكود الجديد أكثر من كونه هجرة سريعة. يستخدم هذا الدليل iovalkey لأنه يوفر أفضل تجربة TypeScript لأكبر مجموعة من القراء: أي شخص لديه خبرة سابقة مع ioredis.
المتطلبات الأساسية
- Node.js 22+ (يتطلب iovalkey إصدار Node 18.12.0 أو أحدث1)
- Docker مع Compose v2 (أمر
Docker compose، المضمن في Docker Desktop و Docker Engine الحاليين) - محرر أكواد ومحطة طرفية (terminal)؛ معرفة أساسية بـ
async/await
قم بتثبيت إصداراتك بدقة ليكون هذا البرنامج التعليمي قابلاً لإعادة التطبيق:
iovalkey 0.3.3
TypeScript 6.0.3
tsx 4.22.4
@types/node 25.9.3
Valkey image valkey/valkey:8.1.8-alpine
الخطوة 1 — تشغيل Valkey محلياً باستخدام Docker Compose
قم بتشغيل Valkey محلياً حتى يكون لتطبيقك شيء يتصل به. أنشئ مجلداً للمشروع وملف Docker-compose.yml يثبت الصورة ويفعل استمرارية البيانات:
# Docker-compose.yml
services:
valkey:
image: valkey/valkey:8.1.8-alpine
container_name: valkey
ports:
- "6379:6379"
# Enable both snapshotting (RDB) and the append-only file (AOF).
command: ["valkey-server", "--save", "60", "1", "--appendonly", "yes"]
volumes:
- valkey-data:/data
healthcheck:
test: ["CMD", "valkey-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
valkey-data:
ابدأ تشغيله وتأكد من أنه يعمل بشكل سليم:
Docker compose up -d
Docker compose ps
يجب أن ترى خدمة valkey بحالة running (healthy) بمجرد اجتياز فحص الحالة. تشحن صورة valkey/valkey الرسمية ملف valkey-cli الثنائي، لذا فإن فحص الحالة يقوم فقط بتشغيل valkey-cli ping داخل الحاوية. اختبره مباشرة من جهازك المضيف:
Docker compose exec valkey valkey-cli ping
# PONG
صورة valkey/valkey:8.1.8-alpine هي أحدث تصحيح لسلسلة 8.1، تم إصدارها في 2 يونيو 2026.2 إصدار Valkey 9.1.0 (19 مايو 2026) هو الأحدث؛ الخطوات في هذا الدليل متطابقة معه — فقط يتغير وسم الصورة (image tag).2
الخطوة 2 — الاتصال من TypeScript باستخدام عميل جاهز للإنتاج
يستغرق الاتصال ثلاثة أسطر، لكن اتصال الإنتاج يحتاج إلى استراتيجية إعادة محاولة و — بشكل حاسم — مستمع للخطأ error، وإلا فإن انقطاع اتصال واحد سيؤدي إلى تعطل عملية Node الخاصة بك. ابدأ المشروع:
npm init -y
npm pkg set type=module
npm install iovalkey@0.3.3
npm install -D TypeScript@6.0.3 tsx@4.22.4 @types/node@25.9.3
أنشئ ملف tsconfig.json. استخدام moduleResolution: "bundler" والتشغيل باستخدام tsx يتجنب احتكاك التوافق مع CommonJS الذي تواجهه عند استيراد iovalkey (حزمة CommonJS) تحت nodenext:
{
"compilerOptions": {
"target": "es2022",
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"]
}
الآن أنشئ src/valkey.ts، وهو المكان الوحيد الذي يستورد منه تطبيقك بالكامل العميل الخاص به:
// src/valkey.ts
import Valkey, { type RedisOptions } from "iovalkey";
const options: RedisOptions = {
host: process.env.VALKEY_HOST ?? "127.0.0.1",
port: Number(process.env.VALKEY_PORT ?? 6379),
password: process.env.VALKEY_PASSWORD || undefined,
// فشل الأمر بعد 3 محاولات بدلاً من وضعه في قائمة الانتظار للأبد.
maxRetriesPerRequest: 3,
// استراتيجية تراجع مخصصة: 50ms، 100ms، 150ms ... بحد أقصى 2s. إرجاع رقم
// يجدول إعادة الاتصال التالية؛ إرجاع null يوقف المحاولات تماماً.
retryStrategy(times: number) {
return Math.min(times * 50, 2000);
},
};
export const valkey = new Valkey(options);
// مطلوب. iovalkey هو EventEmitter؛ أي حدث "error" يتم إطلاقه بدون مستمع
// يعتبر حدث خطأ غير معالج، ويقوم Node بإنهاء العملية بالكامل.
valkey.on("error", (err: Error) => {
console.error("[valkey] client error:", err.message);
});
valkey.on("connect", () => console.log("[valkey] TCP connected"));
valkey.on("ready", () => console.log("[valkey] ready to accept commands"));
valkey.on("reconnecting", (ms: number) =>
console.log(`[valkey] reconnecting in ${msms`),
);
يقوم المنشئ (constructor) بالاتصال فوراً (لا يوجد await)، ثم يطلق connect عند فتح المقبس (socket) و ready عندما يكون Valkey مستعداً لاستقبال الأوامر. مستمع error ليس مجرد لباقة اختيارية — بل هو الفرق بين تسجيل خلل عابر في الشبكة وبين تعطل خدمتك بالكامل. سنقوم بإعادة إنتاج هذا التعطل في قسم استكشاف الأخطاء وإصلاحها حتى تتمكن من التعرف عليه.
الخطوة 3 — التخزين المؤقت الآمن للأنواع باستخدام SET و GET و TTLs
تعد حالة استخدام Valkey للتخزين المؤقت (caching) شائعة جداً، وهي المكان الذي تظهر فيه فائدة TypeScript: قم بتغليف get/set في دوال مساعدة محددة الأنواع حتى لا يضطر المستدعون للتعامل مع JSON يدوياً. أنشئ src/cache.ts:
// src/cache.ts
import { valkey } from "./valkey";
// تخزين أي قيمة قابلة للتسلسل مع TTL صريح بالثواني.
export async function setJSON<T>(
key: string,
value: T,
ttlSeconds: number,
): Promise<void> {
await valkey.set(key, JSON.stringify(value), "EX", ttlSeconds);
}
// يعيد null في حالة عدم وجود القيمة في الذاكرة المؤقتة؛ وإلا يعيد القيمة المحللة والمحددة النوع.
export async function getJSON<T>(key: string): Promise<T | null> {
const raw = await valkey.get(key);
return raw === null ? null : (JSON.parse(raw) as T);
}
يتم تعيين معامل "EX" مباشرة إلى صيغة Valkey SET key value EX seconds، بحيث يتم تخزين المفتاح وإعطاؤه وقتاً للصلاحية (TTL) في رحلة ذهاب وإياب واحدة. ولأن get تعيد string | null، فإن الدالة المساعدة تجعل حالة فقدان البيانات في الذاكرة المؤقتة (cache-miss) صريحة في نظام الأنواع: يجب على المستدعين التعامل مع null.
الخطوة 4 — خطوط الأنابيب (Pipelines) والمعاملات الذرية (Atomic Transactions)
عندما تحتاج إلى تشغيل عدة أوامر، لا تستخدم await لكل منها على حدة — فكل await هي رحلة ذهاب وإياب منفصلة عبر الشبكة. تقوم خطوط الأنابيب (Pipelines) بإرسال مجموعة من الأوامر في رحلة واحدة؛ أما المعاملات (Transactions) (MULTI/EXEC) فتقوم بالإضافة إلى ذلك بتشغيل المجموعة بشكل ذري دون تداخل أوامر أي عميل آخر.8 إليك كلاهما في src/demo.ts:
// src/demo.ts
import { valkey } from "./valkey";
import { setJSON, getJSON } from "./cache";
interface Session {
userId: string;
role: "admin" | "member";
}
async function main() {
// رحلة ذهاب وإياب محددة النوع مع TTL لمدة 60 ثانية.
await setJSON<Session>("session:42", { userId: "42", role: "admin" }, 60);
const session = await getJSON<Session>("session:42");
console.log("session:", session);
console.log("ttl:", await valkey.ttl("session:42"));
// خط أنابيب: أربعة أوامر، رحلة ذهاب وإياب واحدة. يتم حل exec() إلى مصفوفة من
// أزواج [error, result] بترتيب الأوامر.
const results = await valkey
.pipeline()
.set("page:home:views", 0)
.incr("page:home:views")
.incrby("page:home:views", 9)
.get("page:home:views")
.exec();
console.log("pipeline replies:", results?.map(([err, val]) => err ?? val));
// معاملة: نفس الـ API، لكن MULTI/EXEC تجعلها ذرية.
const tx = await valkey
.multi()
.set("balance", 100)
.decrby("balance", 30)
.get("balance")
.exec();
console.log("tx replies:", tx?.map(([err, val]) => err ?? val));
}
كل من pipeline() و multi() يعيدان كائناً (builder) تقوم بربط الأوامر به، ثم تستدعي .exec(). الرد يكون مصفوفة من أزواج [error, result] — فحص خانة error لكل أمر هو الطريقة التي تكتشف بها، على سبيل المثال، فشل أمر INCR لأن القيمة لم تكن عدداً صحيحاً. عند تشغيل هذا في خطوة التحقق، ستكون ردود خط الأنابيب هي ['OK', 1, 10, '10'] (تعيين، ثم زيادة إلى 1، ثم إضافة 9 للوصول إلى 10، ثم قراءتها مرة أخرى) وردود المعاملة هي ['OK', 70, '70'].
الخطوة 5 — النشر/الاشتراك (Pub/sub) على اتصال مخصص
يسمح نظام النشر/الاشتراك لجزء واحد من نظامك ببث رسالة إلى العديد من المشتركين — وهو مفيد لإبطال الذاكرة المؤقتة (cache invalidation) أو إشعارات التوزيع (fan-out).9 القاعدة الوحيدة التي يقع فيها الناس: الاتصال في وضع المشترك لا يمكنه تشغيل أوامر عادية، لذا تحتاج إلى اتصال ثانٍ. تقوم وظيفة duplicate() في iovalkey بنسخ العميل الذي قمت بتكوينه في اتصال جديد تماماً لهذا الغرض. أضف هذا إلى main() في src/demo.ts، قبل نهاية الدالة:
// duplicate() تنسخ إعدادات الاتصال في اتصال جديد.
const subscriber = valkey.duplicate();
await subscriber.subscribe("notifications");
// حل الوعد (promise) في المرة الأولى التي تصل فيها رسالة.
const got = new Promise<string>((resolve) => {
subscriber.on("message", (_channel: string, message: string) =>
resolve(message),
);
});
// النشر على الاتصال الرئيسي؛ المشترك يستقبلها.
await valkey.publish("notifications", "deploy finished");
console.log("received pub/sub message:", await got);
await subscriber.quit();
}
main()
.then(() => valkey.quit())
.then(() => console.log("done, connection closed cleanly"))
.catch(async (err) => {
console.error("demo failed:", err);
await valkey.quit();
process.exit(1);
});
يتم النشر على اتصال valkey الأصلي بينما يستمع اتصال subscriber المنسوخ؛ يتم إطلاق حدث message مع اسم القناة والبيانات. لاحظ أن كل اتصال يتم إغلاقه باستخدام quit() الخاص به.
الخطوة 6 — فحوصات الحالة والإغلاق الآمن
في بيئة الإنتاج، تحتاج إلى شيئين إضافيين: طريقة ليعرف بها المنسق (orchestrator) أن العميل لا يزال حياً، وإغلاق نظيف حتى تنتهي الأوامر الجارية قبل خروج العملية. لفحص الحالة، يكفي استخدام PING — فهو يعيد "PONG" ويثبت أن رحلة الذهاب والإياب تعمل:
// src/health.ts
import { valkey } from "./valkey";
export async function checkValkey(): Promise<boolean> {
try {
return (await valkey.ping()) === "PONG";
} catch {
return false;
}
}
بالنسبة للإغلاق، يفضل استخدام quit() بدلاً من disconnect(). يرسل quit() أمر QUIT وينتظر الردود المعلقة قبل إغلاق المقبس، حتى لا تتخلى عن الأوامر في منتصف التنفيذ؛ أما disconnect() فيقطع المقبس فوراً. قم بربطه بالإشارات التي ترسلها منصتك:
// src/shutdown.ts
import { valkey } from "./valkey";
async function shutdown(signal: string) {
console.log(`[valkey] ${signal received, closing connection...`);
await valkey.quit();
process.exit(0);
}
process.on("SIGTERM", () => void shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));
التحقق
تأكد من أن Valkey يعمل (Docker compose up -d)، ثم قم بتشغيل العرض التوضيحي باستخدام tsx — لا حاجة لخطوة بناء:
npx tsx src/demo.ts
المخرجات المتوقعة، من البداية للنهاية:
[valkey] TCP connected
[valkey] ready to accept commands
session: { userId: '42', role: 'admin' }
ttl: 60
pipeline replies: [ 'OK', 1, 10, '10' ]
tx replies: [ 'OK', 70, '70' ]
received pub/sub message: deploy finished
done, connection closed cleanly
قم بفحص الأنواع للمشروع بالكامل للتأكد من صحة الأنواع الصارمة:
npx tsc --noEmit
# (لا توجد مخرجات = نجاح)
أخيراً، تأكد من أنك تتحدث بالفعل إلى Valkey وليس مجرد أي خادم RESP:
Docker compose exec valkey valkey-cli info server | grep -E 'redis_version|valkey_version'
# redis_version:7.2.4 <- توافقية يبلغ عنها Valkey
# valkey_version:8.1.8 <- المحرك الحقيقي
سطر الإصدار المزدوج متوقع: يعلن Valkey عن redis_version بقيمة 7.2.4 (نقطة التفرع) لتوافق العملاء بينما يبلغ عن valkey_version الحقيقي الخاص به.
الأخطاء الشائعة
MaxRetriesPerRequestError: Reached the max retries per request limit (which is 3). لم يتمكن أمرك من الوصول إلى Valkey خلال محاولات maxRetriesPerRequest. عادةً ما يكون Valkey غير مفعل أو أن المضيف/المنفذ خاطئ. تأكد من أن Docker compose ps يظهر الحالة healthy وأن VALKEY_PORT يطابق المنفذ المنشور (6379 هنا).
العملية تخرج وترى [ioredis] Unhandled error event: ... ECONNREFUSED. لقد نسيت مستمع error من الخطوة 2. لأن iovalkey هو EventEmitter، فإن حدث error الذي يتم إطلاقه بدون مستمع هو حدث خطأ غير معالج ويقوم Node بإنهاء العملية. (التحذير يبدأ بـ [ioredis] لأن iovalkey هو تفرع من ioredis.) الحل دائماً هو valkey.on("error", ...).
This expression is not constructable عند new Valkey(...) أثناء tsc. يظهر هذا إذا قمت بالترجمة تحت "module": "nodenext"، لأن iovalkey منشور كـ CommonJS ولا يتم اعتبار استيراده الافتراضي قابلاً للإنشاء. استخدم إعداد "moduleResolution": "bundler" من الخطوة 2 وقم بالتشغيل باستخدام tsx، أو استورد الفئة المسماة بدلاً من الاستيراد الافتراضي.
اتصال المشترك "لا يمكنه تنفيذ الأوامر" / الردود تتوقف. الاتصال في وضع المشترك يقبل فقط subscribe/unsubscribe ويستقبل الرسائل — لا يمكنه تشغيل GET أو SET وما إلى ذلك. استخدم valkey.duplicate() للمشترك (الخطوة 5) واحتفظ باتصالك الرئيسي للأوامر العادية.
القراءات تنجح ولكن البيانات تختفي بعد إعادة التشغيل. ميزة الاستمرارية (Persistence) معطلة. ملف Docker-compose.yml في الخطوة 1 يفعل كلاً من لقطات RDB (--save 60 1) وملف الإضافة فقط (AOF) (--appendonly yes) ويقوم بتركيب وحدة تخزين (volume) مسماة عند /data؛ بدون وحدة التخزين، تضيع بيانات الحاوية عند إعادة إنشائها.
الخطوات التالية ومزيد من القراءة
لديك الآن عميل Valkey مكتوب بأنواع البيانات (typed) وجاهز للإنتاج مع ميزات إعادة المحاولة، و pub/sub، وفحوصات الحالة، والإغلاق النظيف. من هنا:
- قم ببناء استراتيجيات تخزين مؤقت حقيقية فوق
setJSON/getJSON— راجع دليلنا حول أنماط التخزين المؤقت في Redis للأنظمة القابلة للتوسع، والتي تنطبق مباشرة على Valkey. - قم ببناء محدد معدل (sliding-window limiter) على نفس الاتصال باستخدام دروس تحديد المعدل في API مع Redis.
- قارن بين Valkey pub/sub وبديل أصلي لقواعد البيانات في Postgres LISTEN/NOTIFY كطابور مهام.
مراجع خارجية: مستودع iovalkey، و صفحة مكتبات عملاء Valkey، و قائمة إصدارات Valkey.
Footnotes
-
iovalkey — "A robust, performance-focused and full-featured Valkey client for Node.js. This is a friendly fork of ioredis." Supports Valkey >= 7.0.0; engines
node >= 18.12.0; 100% TypeScript with bundled type declarations; MIT licensed. Version 0.3.3 per npm. https://GitHub.com/valkey-io/iovalkey and https://www.npmjs.com/package/iovalkey (accessed 16 June 2026). ↩ ↩2 ↩3 ↩4 ↩5 ↩6 -
Valkey Releases — 8.1.8 released 2026-06-02; 9.1.0 released 2026-05-19; 9.0.4 released 2026-05-06. https://valkey.io/download/releases/ Official images are published as
valkey/valkey; the8.1.8,8.1.8-alpine, and8.1.8-alpine3.23tags (pushed 2026-06-02) are listed at https://hub.Docker.com/r/valkey/valkey/tags (both accessed 16 June 2026). ↩ ↩2 ↩3 -
TypeScript 6.0.3, latest stable per the npm registry, 16 June 2026. https://www.npmjs.com/package/TypeScript ↩
-
tsx 4.22.4, latest stable per the npm registry, 16 June 2026. https://www.npmjs.com/package/tsx ↩
-
Redis Ltd. changed the Redis license from BSD-3 to dual source-available terms (RSALv2 / SSPLv1) on 20 March 2024; the Linux Foundation launched Valkey days later as a fork of Redis 7.2.4, the last BSD-licensed release, backed by AWS, Google Cloud, and Oracle. https://valkey.io/ and https://www.devclass.com/development/2025/04/01/one-year-ago-Redis-changed-its-license-and-lost-most-of-its-external-contributors/1624450 ↩
-
Valkey GLIDE — the official Valkey client library, built on a Rust core with multi-language bindings (Node.js support added in GLIDE v1.1, September 2024), Apache-2.0 licensed. Its API differs from ioredis (e.g., multi-key commands take arrays). https://GitHub.com/valkey-io/valkey-glide ↩ ↩2
-
Valkey Client Libraries — official directory listing GLIDE, node-Redis, and community clients. https://valkey.io/clients/ ↩
-
Valkey — Pipelining batches multiple commands into a single round trip; transactions (MULTI/EXEC) execute a batch atomically. https://valkey.io/topics/pipelining/ ↩
-
Valkey — Pub/Sub. A connection subscribed to a channel cannot issue other commands. https://valkey.io/topics/pubsub/ ↩
-
As of Valkey 8.1, JSON and search are available through the
valkey-bundleimage, which packages the open-source modulesvalkey-json,valkey-search,valkey-bloom, andvalkey-ldap. Redis Ltd.'s proprietary RediSearch/RedisJSON modules are not redistributable with Valkey, so these community equivalents are used instead. https://valkey.io/blog/introducing-enhanced-json-capabilities-in-valkey/ ↩