TypeScript'te `Shared Worker`

TypeScript'te `Shared Worker`

Bu makale, TypeScript'te Shared Worker konusunu açıklar.

Shared Worker'ların nasıl çalıştığını ve pratikte nasıl kullanılacağını TypeScript kod örnekleriyle ayrıntılı olarak açıklayacağız.

YouTube Video

TypeScript'te Shared Worker

Shared Worker, aynı origin üzerindeki birden çok sekme, pencere ve iframe arasında paylaşılan tek bir worker sürecidir. Bunu kullanarak, birden fazla tarayıcı sekmesi arasında paylaşılan durumları ve kaynakları yönetebilirsiniz.

Örneğin, paylaşılan bir WebSocket bağlantısını, sekmeler arası senkronize önbellek ve kuyruk işlemesini ve karşılıklı dışlamayı (mutex) verimli biçimde uygulayabilirsiniz.

Dedicated Worker'ın aksine, Shared Worker onconnect olayı aracılığıyla birden fazla MessagePort alır ve birden çok istemciyle iletişimi çoklayabilir.

Shared Worker'ı seçmeniz gereken durumlar

Aşağıdaki durumlarda Shared Worker kullanmak uygundur.

  • Sekmeler arasında paylaşılan duruma veya karşılıklı dışlamaya ihtiyaç duyduğunuzda
  • Tek bir WebSocket bağlantısını veya IndexedDB erişimini paylaşmak istediğinizde
  • Tüm sekmeleri bilgilendirmeniz (broadcast) gerektiğinde
  • Kaynakları korumak için ağır işlemleri merkezileştirmek istediğinizde

Buna karşılık, aşağıdaki durumlarda diğer yaklaşımlar daha uygundur.

  • Önbellek denetimine veya çevrimdışı desteğe ihtiyaç duyduğunuzda bir Service Worker kullanabilirsiniz.
  • Yalnızca tek bir sekmeyle sınırlı ağır işlemler için bir Dedicated Worker kullanabilirsiniz.

Shared Worker için uygulama adımları

Burada, TypeScript kullanarak aşağıdakileri adım adım uygulayacağız.

  • Tip güvenli mesaj protokolü
  • Promise tabanlı istek/yanıt (RPC)
  • Tüm sekmelere yayın (broadcast)
  • Heartbeat ve istemci temizliği

Ortam kurulumu

Shared Worker kullanan her kaynak dosyayı derlemek için yapılandırmayı oluşturun.

 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 içinde, kodun güvenle derlenebilmesi için DOM ve Web Worker tür tanımlarını etkinleştirin.

Mesaj protokolünü tanımlama

İletişimin temeli tip tanımlı bir mesaj sözleşmesidir. Bunu en baştan tanımlamak, sonraki iletişimi güvenli ve genişletmesi kolay hale getirir.

 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, worker iş parçacığına gönderilen istek türlerini temsil eden ve ping, get ve broadcast gibi işlemleri tanımlayan ayırt edilebilir bir birliktir (etiketli birlik).
1export interface RequestMessage {
2  kind: 'request';
3  id: string;
4  from: string;
5  action: RequestAction;
6}
  • RequestMessage, istemciden işçiye gönderilen istek iletilerinin yapısını tanımlar.
1export interface ResponseMessage {
2  kind: 'response';
3  id: string;
4  ok: boolean;
5  result?: unknown;
6  error?: string;
7}
  • ResponseMessage, işçiden istemciye döndürülen yanıt iletilerinin yapısını tanımlar.
1export interface BroadcastMessage {
2  kind: 'broadcast';
3  channel: string;
4  payload: unknown;
5  from: string;
6}
  • BroadcastMessage, işçinin diğer istemcilere gönderdiği yayın iletilerinin yapısını tanımlar.
1export type WorkerInMessage =
2  | RequestMessage
3  | { kind: 'heartbeat'; from: string }
4  | { kind: 'bye'; from: string };
  • WorkerInMessage, istekler, kalp atışları ve bağlantı kesme bildirimleri gibi işçinin aldığı tüm iletileri temsil eden bir türdür.
1export type WorkerOutMessage = ResponseMessage | BroadcastMessage;
  • WorkerOutMessage, işçinin istemciye gönderdiği yanıt veya yayın iletilerini temsil eden bir türdür.
1export const randomId = () => Math.random().toString(36).slice(2);
  • randomId, ileti kimlikleri vb. için kullanılacak rastgele alfasayısal bir dize üreten bir işlevidir.

Shared Worker'ın uygulanması

shared-worker.ts içinde, onconnect olayıyla bağlanan sekmeleri kaydedin ve mesajları işleyin.

1// shared-worker.ts
2/// <reference lib="webworker" />
  • Bu yönerge, TypeScript'e Web Worker'lar için tür tanımlarını yüklemesini söyler.
1import {
2  WorkerInMessage,
3  WorkerOutMessage,
4  RequestMessage,
5  ResponseMessage,
6} from './worker-protocol.js';
  • İşçi iletişimi için kullanılan tür tanımlarını içe aktarır.
1export default {};
2declare const self: SharedWorkerGlobalScope;
  • self'in Shared Workerın genel kapsamı (global scope) olduğunu açıkça bildirir.
1type Client = {
2  id: string;
3  port: MessagePort;
4  lastBeat: number;
5};
  • Client, her istemcinin tanımlayıcısını, iletişim portunu ve son kalp atışı zaman damgasını temsil eden bir türdür.
1const clients = new Map<string, Client>();
2const kv = new Map<string, unknown>();
3const locks = new Map<string, string>();
4const HEARTBEAT_TIMEOUT = 30_000;
  • Bağlı istemciler listesini, bir anahtar-değer deposunu, kilit durumunu ve zaman aşımı sürelerini yönetir.
1function send(port: MessagePort, msg: WorkerOutMessage) {
2  port.postMessage(msg);
3}
  • send, belirtilen porta bir ileti gönderen yardımcı bir işlevidir.
1function respond(req: RequestMessage, ok: boolean, result?: unknown, error?: string): ResponseMessage {
2  return { kind: 'response', id: req.id, ok, result, error };
3}
  • respond, bir istek için bir yanıt iletisi üretir.
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, belirli bir kanalda tüm istemcilere bir ileti gönderir.
1function handleRequest(clientId: string, port: MessagePort, req: RequestMessage) {
  • handleRequest, gelen istekleri türüne göre işler ve sonuçları istemciye döndürür.
 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;
  • Bu kodda, alınan isteğin türüne bağlı olarak mesaj gönderme ve alma, veriyi getirme ve kaydetme ve yayınlama işlemleri gerçekleştirilir.
 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      }
  • Bu kod, bir istemcinin belirtilen anahtar için kilit edinme sürecini uygular. Kilit halihazırda tutulmuyorsa, derhal edinilir; aynı istemci onu tekrar talep ederse, istek de başarılı kabul edilir. Başka bir istemci kilidi zaten tutuyorsa, kilit serbest bırakılana kadar her 25 milisaniyede bir yeniden dener; belirtilen zaman aşımı (varsayılan 5 saniye) aşılırsa bir hatayla yanıt verir.
 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}
  • Bu kod, istemcinin elindeki kilidi serbest bırakır ve istemcinin yetkisi yoksa veya işlem bilinmiyorsa bir hata yanıtı döndürür.
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, istemcilerden alınan iletileri ayrıştırır ve istekleri ile kalp atışlarını işler.
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, bağlantısı kesilmiş istemcileri kayıt listesinden ve kilit durumundan kaldırır.
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);
  • Tüm istemcilerin kalp atışlarını periyodik olarak kontrol etmek ve zaman aşımına uğramış bağlantıları temizlemek için setInterval kullanır.
 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};
  • Yeni bir sekme veya sayfa Shared Workera bağlandığında onconnect çağrılır; istemciyi kaydeder ve iletişimi başlatır.

  • Bu dosya boyunca, birden çok tarayıcı sekmesi arasında paylaşılan durum yönetimini ve iletişimi mümkün kılan bir Shared Workerın temel mekanizmaları uygulanmıştır.

İstemci sarmalayıcı (RPC)

Ardından, Promise tabanlı bir RPC istemcisi oluşturun.

1// shared-worker-client.ts
2import {
3  RequestAction,
4  RequestMessage,
5  WorkerOutMessage,
6  randomId
7} from './worker-protocol.js';
  • İşçi iletişimi için kullanılan tür tanımlarını ve yardımcı işlevleri içe aktarır.
1export type BroadcastHandler = (msg: {
2  channel: string;
3  payload: unknown;
4  from: string
5}) => void;
  • Burada, bir yayın iletisi alındığında çalışan geri çağırma işlevinin türünü tanımlarız.
1export class SharedWorkerClient {
  • SharedWorkerClient, bir Shared Worker ile iletişim kuran, istek gönderen ve yanıtları işleyen bir istemci sınıfıdır.
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  }>();
  • Bu değişkenler, worker örneğini, worker ile iletişim portunu ve yanıt bekleyen istekleri izleyen bir eşlemeyi (map) tutar.
1  private clientId = randomId();
2  private heartbeatTimer?: number;
3  private onBroadcast?: BroadcastHandler;
  • Bu değişkenler, istemci tanımlayıcısını, kalp atışlarını göndermek için zamanlayıcıyı ve yayın alma işleyicisini tutar.
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;
  • Kurucu içinde, Shared Workera bağlantıyı başlatır ve ileti dinleyicilerini ile kalp atışı göndermeyi ayarlar.
 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();
  • Burada worker'dan iletiler alır ve yanıtları veya yayınları işler.
1    this.heartbeatTimer = window.setInterval(() => {
2      this.port.postMessage({ kind: 'heartbeat', from: this.clientId });
3    }, 10_000);
  • Bağlantıyı canlı tutmak için kalp atışı iletilerini periyodik olarak gönderir.
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  }
  • Pencere kapanmadan önce worker'a bir bağlantı kesme bildirimi gönderir.
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 yöntemi, belirtilen eylemi worker'a gönderir ve sonucu bir Promise olarak alır.
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  }
  • Bunlar, temel iletişim testleri ve mevcut zamanı alma için yardımcı yöntemlerdir.
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  }
  • Bunlar, anahtar-değer çiftlerini depolama ve geri alma yöntemleridir.
1  broadcast(channel: string, payload: unknown) {
2    return this.request<boolean>({ type: 'broadcast', channel, payload });
3  }
  • Bu, worker aracılığıyla diğer istemcilere yayın iletileri gönderen bir yöntemdir.
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}
  • Bunlar, paylaşılan kaynaklar üzerinde karşılıklı dışlamayı sağlamak için kilit edinip serbest bırakan yöntemlerdir.
  • Bu dosya boyunca, her tarayıcı sekmesinden Shared Worker'a güvenli, eşzamansız iletişim için bir istemci API'si uygulanmıştır.

Örnek kullanım

demo.ts içinde, daha önce oluşturulan SharedWorkerClient sınıfını kullanır ve davranışını doğrularız. İletişim testleri, veri okuma ve yazma, yayınlama ve kilit yönetimi dahil bir dizi işlevi sırasıyla yürütür.

 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);
  • Bu kod, bir Shared Worker kullanarak birden çok tarayıcı sekmesi arasında veri ve durumu paylaşan ve eşzamanlayan bir demodur. Mesaj tabanlı iletişim kullanarak, gevşek bağlılıkla eşzamansız iletileri güvenle değiş tokuş edebilir, farklı bağlamlar arasındaki iletişimi yönetmeyi kolaylaştırabilirsiniz. Ayrıca, RPC kullanarak worker ile iletişimi sezgisel, yöntem çağrısı benzeri bir üslupla soyutlar; bakım yapılabilirliği ve okunabilirliği artırır.

HTML'de test

 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>

Tasarım ve işletimle ilgili değerlendirmeler

Tasarlarken ve işletirken, aşağıdaki noktaları akılda tutmak daha sağlam ve genişletilebilir bir sistem kurmanıza yardımcı olacaktır.

  • kind veya type üzerinde dallanmayı sağlayan bir ayırt edilebilir birlik (etiketli birlik) benimseyebilirsiniz.
  • İstekleri yanıtlarla doğru şekilde eşleştirmek için bir ilişkilendirme kimliği kullanın.
  • Kalp atışları ve otomatik temizlik, sahipsiz kalmış kilitleri önleyebilir.
  • Gelecekteki protokol değişikliklerine esnek biçimde uyum sağlamak için sürümleme uygulayın.
  • Açık hata kodları tanımlamak, UI tarafında ele almayı ve hata ayıklamayı kolaylaştırır.

Özet

Shared Worker, birden çok tarayıcı sekmesi arasında veri ve durum paylaşımını sağlayan temel bir mekanizmadır.

Burada tanıtılan yapı, tür güvenli RPC iletişimi, kalp atışları aracılığıyla canlılık izleme ve bir kilitleme mekanizması sağlar; bu da üretimde olduğu gibi kullanılabilecek sağlam bir tasarım sunar.

Bu mekanizmanın üzerine ayrıca aşağıdaki uygulamaları da gerçekleştirebilirsiniz.

  • IndexedDB erişimini sıralamak
  • WebSocket bağlantılarının bütünleştirilmesi ve paylaşılması.
  • Birden çok sekme arasında bir iş kuyruğu oluşturma
  • Oran sınırlama (throttling) ve ilerleme bildirimlerinin iletilmesi.

Gördüğünüz gibi, bir Shared Workerdan yararlanmak, verileri ve işlemleri birden çok sekme arasında güvenli ve verimli şekilde paylaşmayı mümkün kılar.

Yukarıdaki makaleyi, YouTube kanalımızda Visual Studio Code'u kullanarak takip edebilirsiniz. Lütfen YouTube kanalını da kontrol edin.

YouTube Video