`Shared Worker` في TypeScript

`Shared Worker` في TypeScript

تشرح هذه المقالة Shared Worker في TypeScript۔

سنشرح بالتفصيل كيفية عمل Shared Workers وكيفية استخدامها عمليًا، مع أمثلة كود TypeScript۔

YouTube Video

Shared Worker في TypeScript

Shared Worker هو عملية عامل واحدة مشتركة عبر عدّة علامات تبويب ونوافذ وiframes ضمن نفس الأصل۔ باستخدام ذلك، يمكنك التعامل مع الحالة والموارد المشتركة عبر عدّة علامات تبويب في المتصفح۔

على سبيل المثال، يمكنك تنفيذ اتصال WebSocket مشترك بكفاءة، وتخزينًا مؤقتًا ومعالجة طوابير متزامنة عبر علامات التبويب، وقفلًا متبادلًا۔

على عكس Dedicated Worker، يتلقى Shared Worker عدة MessagePort عبر حدث onconnect ويمكنه تعدد الإرسال للتواصل مع عدة عملاء۔

حالات يُفضّل فيها اختيار Shared Worker

في الحالات التالية، يكون استخدام Shared Worker مناسبًا۔

  • عندما تحتاج إلى حالة مشتركة أو قفل متبادل عبر علامات التبويب
  • عندما تريد مشاركة اتصال WebSocket واحد أو الوصول إلى IndexedDB
  • عندما تحتاج إلى إشعار جميع علامات التبويب (بث)
  • عندما تريد تركيز المعالجة الثقيلة للحفاظ على الموارد

وعلى العكس، في الحالات التالية تكون أساليب أخرى أكثر ملاءمة۔

  • عندما تحتاج إلى التحكم في التخزين المؤقت أو دعم العمل دون اتصال، يمكنك استخدام Service Worker۔
  • للمعالجة الثقيلة المحصورة في علامة تبويب واحدة، يمكنك استخدام Dedicated Worker۔

خطوات التنفيذ لـ Shared Worker

سننفّذ هنا ما يلي خطوة بخطوة باستخدام TypeScript۔

  • بروتوكول رسائل آمن الأنواع
  • طلب/استجابة مبنيان على Promise (استدعاء إجراءات عن بُعد RPC)
  • بثّ إلى جميع علامات التبويب
  • آلية النبض (Heartbeat) وتنظيف العملاء

إعداد البيئة

أنشئ الإعدادات اللازمة لترجمة كل ملف مصدر يستخدم Shared Worker۔

 1{
 2  "compilerOptions": {
 3    "target": "ES2020",
 4    "module": "ES2020",
 5    "lib": ["ES2020", "dom", "WebWorker"],
 6    "moduleResolution": "Bundler",
 7    "strict": true,
 8    "noEmitOnError": true,
 9    "outDir": "out",
10    "skipLibCheck": true
11  },
12  "include": ["src"]
13}
  • في tsconfig-src.json، فعّل تعريفات الأنواع الخاصة بـ DOM وWeb Worker حتى يمكن ترجمة الشيفرة بأمان۔

تعريف بروتوكول الرسائل

أساس الاتصال هو عقد رسائل مُقيّد بالأنواع۔ تعريف هذا مسبقًا يجعل الاتصالات اللاحقة آمنة وسهلة التوسعة۔

 1// worker-protocol.ts
 2// Define discriminated unions for structured messages
 3
 4export type RequestAction =
 5  | { type: 'ping' }
 6  | { type: 'echo'; payload: string }
 7  | { type: 'getTime' }
 8  | { type: 'set'; key: string; value: unknown }
 9  | { type: 'get'; key: string }
10  | { type: 'broadcast'; channel: string; payload: unknown }
11  | { type: 'lock.acquire'; key: string; timeoutMs?: number }
12  | { type: 'lock.release'; key: string };
  • RequestAction هو اتحاد مميَّز (اتحاد موسوم) يمثّل أنواع الطلبات المُرسلة إلى العامل ويعرّف عمليات مثل ping وget وbroadcast۔
1export interface RequestMessage {
2  kind: 'request';
3  id: string;
4  from: string;
5  action: RequestAction;
6}
  • RequestMessage يعرّف بنية رسائل الطلب المرسلة من العميل إلى العامل۔
1export interface ResponseMessage {
2  kind: 'response';
3  id: string;
4  ok: boolean;
5  result?: unknown;
6  error?: string;
7}
  • ResponseMessage يعرّف بنية رسائل الاستجابة المُعادة من العامل إلى العميل۔
1export interface BroadcastMessage {
2  kind: 'broadcast';
3  channel: string;
4  payload: unknown;
5  from: string;
6}
  • BroadcastMessage يعرّف بنية رسائل البث التي يرسلها العامل إلى العملاء الآخرين۔
1export type WorkerInMessage =
2  | RequestMessage
3  | { kind: 'heartbeat'; from: string }
4  | { kind: 'bye'; from: string };
  • WorkerInMessage هو نوع يمثّل جميع الرسائل التي يتلقّاها العامل، مثل الطلبات ونبضات القلب وإشعارات قطع الاتصال۔
1export type WorkerOutMessage = ResponseMessage | BroadcastMessage;
  • WorkerOutMessage هو نوع يمثّل رسائل الاستجابة أو البث التي يرسلها العامل إلى العميل۔
1export const randomId = () => Math.random().toString(36).slice(2);
  • randomId هي دالّة تُولّد سلسلة عشوائية من أحرف وأرقام لاستخدامها لمعرّفات الرسائل وما شابه۔

تنفيذ Shared Worker

في shared-worker.ts، سجّل علامات التبويب التي تتصل عبر حدث onconnect وتعامل مع الرسائل۔

1// shared-worker.ts
2/// <reference lib="webworker" />
  • هذه التعليمة تطلب من TypeScript تحميل تعريفات الأنواع الخاصة بـ Web Workers۔
1import {
2  WorkerInMessage,
3  WorkerOutMessage,
4  RequestMessage,
5  ResponseMessage,
6} from './worker-protocol.js';
  • يستورد تعريفات الأنواع المستخدمة للتواصل مع العامل۔
1export default {};
2declare const self: SharedWorkerGlobalScope;
  • يصرّح صراحةً بأن self هو النطاق العام لـ Shared Worker۔
1type Client = {
2  id: string;
3  port: MessagePort;
4  lastBeat: number;
5};
  • Client هو نوع يمثّل معرّف كل عميل، ومنفذ الاتصال، والطابع الزمني لآخر نبضة قلب۔
1const clients = new Map<string, Client>();
2const kv = new Map<string, unknown>();
3const locks = new Map<string, string>();
4const HEARTBEAT_TIMEOUT = 30_000;
  • يدير قائمة العملاء المتصلين، ومخزن مفتاح-قيمة، وحالة القفل، وفترات انتهاء المهلة۔
1function send(port: MessagePort, msg: WorkerOutMessage) {
2  port.postMessage(msg);
3}
  • send هي دالّة مساعدة ترسل رسالة إلى المنفذ المحدد۔
1function respond(req: RequestMessage, ok: boolean, result?: unknown, error?: string): ResponseMessage {
2  return { kind: 'response', id: req.id, ok, result, error };
3}
  • respond تُنشئ رسالة استجابة لطلب معيّن۔
1function broadcast(from: string, channel: string, payload: unknown) {
2  for (const [id, c] of clients) {
3    send(c.port, { kind: 'broadcast', channel, payload, from });
4  }
5}
  • broadcast ترسل رسالة على قناة محددة إلى جميع العملاء۔
1function handleRequest(clientId: string, port: MessagePort, req: RequestMessage) {
  • handleRequest تعالج الطلبات الواردة حسب النوع وتعيد النتائج إلى العميل۔
 1  const { action } = req;
 2  try {
 3    switch (action.type) {
 4      case 'ping':
 5        send(port, respond(req, true, 'pong'));
 6        break;
 7      case 'echo':
 8        send(port, respond(req, true, action.payload));
 9        break;
10      case 'getTime':
11        send(port, respond(req, true, new Date().toISOString()));
12        break;
13      case 'set':
14        kv.set(action.key, action.value);
15        send(port, respond(req, true, true));
16        break;
17      case 'get':
18        send(port, respond(req, true, kv.get(action.key)));
19        break;
20      case 'broadcast':
21        broadcast(clientId, action.channel, action.payload);
22        send(port, respond(req, true, true));
23        break;
  • في هذا الكود، وبناءً على نوع الطلب المُستلم، يتعامل مع إرسال واستقبال الرسائل، وجلب البيانات وحفظها، والبث۔
 1      case 'lock.acquire': {
 2        const owner = locks.get(action.key);
 3        if (!owner) {
 4          locks.set(action.key, clientId);
 5          send(port, respond(req, true, { owner: clientId }));
 6        } else if (owner === clientId) {
 7          send(port, respond(req, true, { owner }));
 8        } else {
 9          const start = Date.now();
10          const tryWait = () => {
11            const current = locks.get(action.key);
12            if (!current) {
13              locks.set(action.key, clientId);
14              send(port, respond(req, true, { owner: clientId }));
15            } else if ((action.timeoutMs ?? 5000) < Date.now() - start) {
16              send(port, respond(req, false, undefined, 'lock-timeout'));
17            } else {
18              setTimeout(tryWait, 25);
19            }
20          };
21          tryWait();
22        }
23        break;
24      }
  • ينفّذ هذا الكود عملية حصول العميل على قفل للمفتاح المحدَّد۔ إذا لم يكن القفل محجوزًا مسبقًا، فسيتم الحصول عليه فورًا؛ وإذا طلبه العميل نفسه مرة أخرى، فسيُعامَل الطلب كذلك على أنه ناجح۔ إذا كان عميل آخر يحتفظ بالقفل بالفعل، فستتم إعادة المحاولة كل 25 مللي ثانية حتى يتم تحرير القفل، وإذا تم تجاوز مهلة الانتظار المحددة (الافتراضي 5 ثوانٍ) فسيكون الردّ خطأً۔
 1      case 'lock.release':
 2        if (locks.get(action.key) === clientId) {
 3          locks.delete(action.key);
 4          send(port, respond(req, true, true));
 5        } else {
 6          send(port, respond(req, false, undefined, 'not-owner'));
 7        }
 8        break;
 9      default:
10        send(port, respond(req, false, undefined, 'unknown-action'));
11    }
12  } catch (e: any) {
13    send(port, respond(req, false, undefined, e?.message ?? 'error'));
14  }
15}
  • يفك هذا الكود القفل الذي يحتفظ به العميل ويعيد استجابة خطأ إذا كان العميل يفتقر إلى الصلاحية أو كان الإجراء غير معروفًا۔
1function handleInMessage(c: Client, msg: WorkerInMessage) {
2  if (msg.kind === 'heartbeat') {
3    c.lastBeat = Date.now();
4  } else if (msg.kind === 'bye') {
5    cleanupClient(msg.from);
6  } else if (msg.kind === 'request') {
7    handleRequest(c.id, c.port, msg);
8  }
9}
  • handleInMessage تحلّل الرسائل الواردة من العملاء وتتولى التعامل مع الطلبات ونبضات القلب۔
1function cleanupClient(clientId: string) {
2  for (const [key, owner] of locks) {
3    if (owner === clientId) locks.delete(key);
4  }
5  clients.delete(clientId);
6}
  • cleanupClient تزيل العملاء المنفصلين من السجلّ وحالة القفل۔
1setInterval(() => {
2  const now = Date.now();
3  for (const [id, c] of clients) {
4    if (now - c.lastBeat > HEARTBEAT_TIMEOUT) cleanupClient(id);
5  }
6}, 10_000);
  • تستخدم setInterval للتحقق دوريًا من نبضات قلب جميع العملاء وتنظيف الاتصالات التي انتهت مهلتها۔
 1self.onconnect = (e: MessageEvent) => {
 2  const port = (e.ports && e.ports[0]) as MessagePort;
 3  const clientId = crypto.randomUUID?.() ?? Math.random().toString(36).slice(2);
 4  const client: Client = { id: clientId, port, lastBeat: Date.now() };
 5  clients.set(clientId, client);
 6
 7  port.addEventListener('message', (ev) => handleInMessage(client, ev.data as WorkerInMessage));
 8  send(port, { kind: 'broadcast', channel: 'system', payload: { hello: true, clientId }, from: 'worker' });
 9  port.start();
10};
  • تُستدعى onconnect عند اتصال علامة تبويب أو صفحة جديدة بـ Shared Worker، فتسجّل العميل وتبدأ الاتصال۔

  • عبر هذا الملف، تُطبَّق الآليات الأساسية لـ Shared Worker التي تمكّن من إدارة الحالة المشتركة والتواصل عبر عدة علامات تبويب في المتصفح۔

غلاف العميل (RPC)

بعد ذلك، أنشئ عميل RPC مبنيًا على Promise۔

1// shared-worker-client.ts
2import {
3  RequestAction,
4  RequestMessage,
5  WorkerOutMessage,
6  randomId
7} from './worker-protocol.js';
  • يستورد تعريفات الأنواع والدوال المساعدة المستخدمة للتواصل مع العامل۔
1export type BroadcastHandler = (msg: {
2  channel: string;
3  payload: unknown;
4  from: string
5}) => void;
  • هنا نعرّف نوع دالّة الاسترجاع التي تعمل عند تلقي رسالة بث۔
1export class SharedWorkerClient {
  • SharedWorkerClient هي فئة عميل تتواصل مع Shared Worker، فترسل الطلبات وتتولّى معالجة الاستجابات۔
1  private worker: SharedWorker;
2  private port: MessagePort;
3  private pending = new Map<string, {
4    resolve: (v: any) => void;
5    reject: (e: any) => void
6  }>();
  • هذه المتغيّرات هي مثيل العامل، ومنفذ الاتصال مع العامل، وخريطة تتعقّب الطلبات المنتظرة للاستجابة۔
1  private clientId = randomId();
2  private heartbeatTimer?: number;
3  private onBroadcast?: BroadcastHandler;
  • تحتفظ هذه المتغيّرات بمعرّف العميل، ومؤقّت إرسال نبضات القلب، ومعالج استقبال البث۔
1  constructor(url: URL, name = 'app-shared', onBroadcast?: BroadcastHandler) {
2    this.worker = new SharedWorker(url, { name, type: 'module' as any });
3    this.port = this.worker.port;
4    this.onBroadcast = onBroadcast;
  • في الباني (constructor)، يهيّئ الاتصال بـ Shared Worker ويعد مستمعي الرسائل وإرسال نبضات القلب۔
 1    this.port.addEventListener('message', (ev) => {
 2      const msg = ev.data as WorkerOutMessage;
 3      if (msg.kind === 'response') {
 4        const p = this.pending.get(msg.id);
 5        if (p) {
 6          this.pending.delete(msg.id);
 7          msg.ok ? p.resolve(msg.result) : p.reject(new Error(msg.error || 'error'));
 8        }
 9      } else if (msg.kind === 'broadcast') {
10        this.onBroadcast?.(msg);
11      }
12    });
13    this.port.start();
  • هنا يتلقّى الرسائل من العامل ويتعامل مع الاستجابات أو رسائل البث۔
1    this.heartbeatTimer = window.setInterval(() => {
2      this.port.postMessage({ kind: 'heartbeat', from: this.clientId });
3    }, 10_000);
  • يرسل رسائل نبضات القلب دوريًا للحفاظ على الاتصال نشطًا۔
1    window.addEventListener('beforeunload', () => {
2      try {
3        this.port.postMessage({ kind: 'bye', from: this.clientId });
4      } catch {}
5      if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
6    });
7  }
  • يرسل إشعار قطع الاتصال إلى العامل قبل إغلاق النافذة۔
1  request<T = unknown>(action: RequestAction): Promise<T> {
2    const id = randomId();
3    return new Promise<T>((resolve, reject) => {
4      this.pending.set(id, { resolve, reject });
5      this.port.postMessage({ kind: 'request', id, from: this.clientId, action });
6    });
7  }
  • طريقة request ترسل الإجراء المحدّد إلى العامل وتتلقّى النتيجة على شكل Promise۔
1  ping() {
2    return this.request<string>({ type: 'ping' });
3  }
4  echo(payload: string) {
5    return this.request<string>({ type: 'echo', payload });
6  }
7  getTime() {
8    return this.request<string>({ type: 'getTime' });
9  }
  • هذه أساليب مساعدة لاختبارات الاتصال الأساسية والحصول على الوقت الحالي۔
1  set(key: string, value: unknown) {
2    return this.request<boolean>({ type: 'set', key, value });
3  }
4  get<T = unknown>(key: string) {
5    return this.request<T>({ type: 'get', key });
6  }
  • هذه أساليب لتخزين أزواج المفتاح-القيمة واسترجاعها۔
1  broadcast(channel: string, payload: unknown) {
2    return this.request<boolean>({ type: 'broadcast', channel, payload });
3  }
  • هذه طريقة ترسل رسائل بث إلى العملاء الآخرين عبر العامل۔
1  lockAcquire(key: string, timeoutMs?: number) {
2    return this.request<{ owner: string }>({ type: 'lock.acquire', key, timeoutMs });
3  }
4  lockRelease(key: string) {
5    return this.request<boolean>({ type: 'lock.release', key });
6  }
7}
  • هذه أساليب لاكتساب الأقفال وإطلاقها لتحقيق الاستبعاد المتبادل على الموارد المشتركة۔
  • عبر هذا الملف، تُنفَّذ واجهة برمجة تطبيقات للعميل تتيح تواصلًا آمنًا وغير متزامن من كل علامة تبويب في المتصفح إلى الـ Shared Worker۔

مثال على الاستخدام

في demo.ts نستخدم الفئة SharedWorkerClient التي أُنشئت سابقًا ونتحقق من سلوكها۔ ينفّذ بالتتابع سلسلة من الدوال بما في ذلك اختبارات الاتصال، وقراءة وكتابة البيانات، والبث، والتعامل مع الأقفال۔

 1// demo.ts
 2import { SharedWorkerClient } from './shared-worker-client.js';
 3
 4const client = new SharedWorkerClient(new URL('./shared-worker.js', import.meta.url), 'app-shared', (b) => {
 5  console.log('[BROADCAST]', b.channel, JSON.stringify(b.payload));
 6});
 7
 8async function demo() {
 9  console.log('ping:', await client.ping());
10  console.log('echo:', await client.echo('hello'));
11  console.log('time:', await client.getTime());
12
13  // Counter update
14  await client.set('counter', (await client.get<number>('counter')) ?? 0);
15  const c1 = await client.get<number>('counter');
16  await client.set('counter', (c1 ?? 0) + 1);
17  console.log('counter:', await client.get<number>('counter'));
18
19  // Broadcast test
20  await client.broadcast('notify', { msg: 'Counter updated' });
21
22  // Lock test
23  const key = 'critical';
24  console.log(`[lock] Trying to acquire lock: "${key}"`);
25  const lockResult = await client.lockAcquire(key, 2000);
26  console.log(`[lock] Lock acquired for key "${key}":`, lockResult);
27
28  try {
29    console.log(`[lock] Simulating critical section for key "${key}"...`);
30    await new Promise((r) => setTimeout(r, 250));
31    console.log(`[lock] Critical section completed for key "${key}"`);
32  } finally {
33    console.log(`[lock] Releasing lock: "${key}"`);
34    const releaseResult = await client.lockRelease(key);
35    console.log(`[lock] Lock released for key "${key}":`, releaseResult);
36  }
37}
38
39demo().catch(console.error);
  • هذا الكود عرضٌ توضيحي يستخدم Shared Worker لمشاركة البيانات والحالة ومزامنتهما عبر علامات تبويب متعدّدة۔ باستخدام تواصل قائم على الرسائل، يمكنك تبادل رسائل غير متزامنة بأمان مع اقتران ضعيف، مما يسهل إدارة الاتصال بين السياقات المختلفة۔ بالإضافة إلى ذلك، باستخدام RPC، يُجرّد الاتصال بالعامل بأسلوب بديهي يشبه استدعاء الدوال، مما يحسّن قابلية الصيانة وقابلية القراءة۔

الاختبار في HTML

 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4  <meta charset="UTF-8" />
 5  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 6  <title>Shared Worker Demo</title>
 7</head>
 8<body>
 9  <h1>SharedWorker Demo</h1>
10  <p>Open the browser console to see the output.</p>
11
12  <script type="module">
13    import './demo.js';
14  </script>
15</body>
16</html>

اعتبارات التصميم والتشغيل

عند التصميم والتشغيل، فإن مراعاة النقاط التالية سيساعدك على بناء نظام أكثر متانة وقابلية للتوسع۔

  • يمكنك اعتماد اتحاد مميَّز (اتحاد موسوم) يتيح التفرّع بناءً على kind أو type۔
  • استخدم معرّف الترابط لمطابقة الطلبات مع الاستجابات بشكل صحيح۔
  • النبضات والتنظيف التلقائي يمكنهما منع الأقفال المتروكة۔
  • طبّق إدارة الإصدارات لاستيعاب تغييرات البروتوكول المستقبلية بمرونة۔
  • إن تعريف رموز أخطاء واضحة يجعل المعالجة على جانب واجهة المستخدم وتصحيح الأخطاء أسهل۔

الملخص

Shared Worker هو آلية أساسية لمشاركة البيانات والحالة عبر علامات تبويب متعددة في المتصفح۔

يوفّر الهيكل المقدَّم هنا اتصال RPC آمنًا من ناحية الأنواع، ومراقبة الحيوية عبر نبضات القلب، وآلية قفل، مما يجعله تصميمًا متينًا صالحًا للاستخدام كما هو في بيئات الإنتاج۔

فوق هذه الآلية، يمكنك أيضًا تنفيذ التطبيقات التالية۔

  • تسلسل عمليات الوصول إلى IndexedDB
  • دمج ومشاركة اتصالات WebSocket
  • بناء طابور مهام عبر علامات تبويب متعددة
  • التقييد وتسليم إشعارات التقدّم

كما ترى، يتيح الاستفادة من Shared Worker مشاركة البيانات والمعالجة بأمان وكفاءة عبر علامات تبويب متعددة۔

يمكنك متابعة المقالة أعلاه باستخدام Visual Studio Code على قناتنا على YouTube.۔ يرجى التحقق من القناة على YouTube أيضًا.۔

YouTube Video