`Shared Worker` di TypeScript

`Shared Worker` di TypeScript

Artikel ini menjelaskan Shared Worker di TypeScript.

Kami akan menjelaskan secara detail cara kerja Shared Worker dan cara menggunakannya dalam praktik, disertai contoh kode TypeScript.

YouTube Video

Shared Worker di TypeScript

Shared Worker adalah satu proses worker tunggal yang dibagikan ke banyak tab, jendela, dan iframe pada origin yang sama. Dengan ini, Anda dapat menangani state dan sumber daya bersama di banyak tab browser.

Misalnya, Anda dapat secara efisien mengimplementasikan koneksi WebSocket bersama, cache dan pemrosesan antrean yang tersinkron antar tab, serta eksklusi mutual.

Tidak seperti Dedicated Worker, Shared Worker menerima beberapa MessagePort melalui event onconnect dan dapat melakukan multiplexing komunikasi dengan banyak klien.

Kasus ketika Anda sebaiknya memilih Shared Worker

Pada kasus berikut, menggunakan Shared Worker adalah pilihan yang tepat.

  • Saat Anda memerlukan state bersama atau eksklusi mutual antar tab
  • Saat Anda ingin membagi satu koneksi WebSocket atau akses IndexedDB
  • Saat Anda perlu memberi tahu semua tab (broadcast)
  • Saat Anda ingin memusatkan pemrosesan berat untuk menghemat sumber daya

Sebaliknya, pada kasus berikut, pendekatan lain lebih cocok.

  • Saat Anda memerlukan kontrol cache atau dukungan offline, Anda dapat menggunakan Service Worker.
  • Untuk pemrosesan berat yang terbatas pada satu tab, Anda dapat menggunakan Dedicated Worker.

Langkah-langkah implementasi untuk Shared Worker

Di sini, kita akan mengimplementasikan hal-hal berikut selangkah demi selangkah dengan TypeScript.

  • Protokol pesan yang type-safe
  • Permintaan/respons berbasis Promise (RPC)
  • Broadcast ke semua tab
  • Heartbeat dan pembersihan klien

Penyiapan lingkungan

Buat konfigurasi untuk mengompilasi setiap berkas sumber yang menggunakan 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}
  • Dalam tsconfig-src.json, aktifkan definisi tipe DOM dan Web Worker agar kode dapat dikompilasi dengan aman.

Mendefinisikan protokol pesan

Dasar komunikasi adalah kontrak pesan bertipe. Mendefinisikannya sejak awal membuat komunikasi selanjutnya aman dan mudah diperluas.

 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 adalah discriminated union (tagged union) yang mewakili tipe permintaan yang dikirimkan ke worker dan mendefinisikan operasi seperti ping, get, dan broadcast.
1export interface RequestMessage {
2  kind: 'request';
3  id: string;
4  from: string;
5  action: RequestAction;
6}
  • RequestMessage mendefinisikan struktur pesan permintaan yang dikirim dari klien ke worker.
1export interface ResponseMessage {
2  kind: 'response';
3  id: string;
4  ok: boolean;
5  result?: unknown;
6  error?: string;
7}
  • ResponseMessage mendefinisikan struktur pesan respons yang dikembalikan dari worker ke klien.
1export interface BroadcastMessage {
2  kind: 'broadcast';
3  channel: string;
4  payload: unknown;
5  from: string;
6}
  • BroadcastMessage mendefinisikan struktur pesan broadcast yang dikirim worker ke klien lain.
1export type WorkerInMessage =
2  | RequestMessage
3  | { kind: 'heartbeat'; from: string }
4  | { kind: 'bye'; from: string };
  • WorkerInMessage adalah tipe yang merepresentasikan semua pesan yang diterima worker, seperti permintaan, heartbeat, dan notifikasi pemutusan koneksi.
1export type WorkerOutMessage = ResponseMessage | BroadcastMessage;
  • WorkerOutMessage adalah tipe yang merepresentasikan pesan respons atau broadcast yang dikirim worker ke klien.
1export const randomId = () => Math.random().toString(36).slice(2);
  • randomId adalah fungsi yang menghasilkan string alfanumerik acak untuk digunakan sebagai ID pesan dan sejenisnya.

Mengimplementasikan Shared Worker

Di shared-worker.ts, daftarkan tab yang terhubung melalui event onconnect dan tangani pesan.

1// shared-worker.ts
2/// <reference lib="webworker" />
  • Direktif ini memberi tahu TypeScript untuk memuat definisi tipe untuk Web Worker.
1import {
2  WorkerInMessage,
3  WorkerOutMessage,
4  RequestMessage,
5  ResponseMessage,
6} from './worker-protocol.js';
  • Mengimpor definisi tipe yang digunakan untuk komunikasi worker.
1export default {};
2declare const self: SharedWorkerGlobalScope;
  • Secara eksplisit menyatakan bahwa self adalah lingkup global dari Shared Worker.
1type Client = {
2  id: string;
3  port: MessagePort;
4  lastBeat: number;
5};
  • Client adalah tipe yang merepresentasikan pengenal tiap klien, port komunikasi, dan stempel waktu heartbeat terakhir.
1const clients = new Map<string, Client>();
2const kv = new Map<string, unknown>();
3const locks = new Map<string, string>();
4const HEARTBEAT_TIMEOUT = 30_000;
  • Mengelola daftar klien yang terhubung, penyimpanan kunci-nilai, status penguncian, dan durasi batas waktu (timeout).
1function send(port: MessagePort, msg: WorkerOutMessage) {
2  port.postMessage(msg);
3}
  • send adalah fungsi utilitas yang mengirim pesan ke port yang ditentukan.
1function respond(req: RequestMessage, ok: boolean, result?: unknown, error?: string): ResponseMessage {
2  return { kind: 'response', id: req.id, ok, result, error };
3}
  • respond menghasilkan pesan respons untuk sebuah permintaan.
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 mengirim pesan pada kanal yang ditentukan ke semua klien.
1function handleRequest(clientId: string, port: MessagePort, req: RequestMessage) {
  • handleRequest memproses permintaan masuk berdasarkan jenisnya dan mengembalikan hasil ke klien.
 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;
  • Dalam kode ini, tergantung pada jenis permintaan yang diterima, dilakukan penanganan pengiriman dan penerimaan pesan, pengambilan dan penyimpanan data, serta broadcast.
 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      }
  • Kode ini mengimplementasikan proses bagi klien untuk memperoleh lock atas key yang ditentukan. Jika lock belum dipegang, lock akan segera diperoleh; jika klien yang sama memintanya lagi, permintaan tersebut juga dianggap berhasil. Jika lock sudah dipegang oleh klien lain, sistem akan mencoba ulang setiap 25 milidetik hingga lock dilepas, dan jika batas waktu yang ditentukan (bawaan 5 detik) terlampaui, akan merespons dengan error.
 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}
  • Kode ini melepaskan lock yang dipegang oleh klien dan mengembalikan respons kesalahan jika klien tidak memiliki izin atau tindakannya tidak dikenal.
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 mengurai pesan yang diterima dari klien serta menangani permintaan dan heartbeat.
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 menghapus klien yang terputus dari registri dan status penguncian.
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);
  • Menggunakan setInterval untuk memeriksa heartbeat semua klien secara berkala dan membersihkan koneksi yang kehabisan waktu (timeout).
 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 dipanggil saat tab atau halaman baru tersambung ke Shared Worker, mendaftarkan klien dan memulai komunikasi.

  • Di seluruh berkas ini, mekanisme mendasar Shared Worker yang memungkinkan pengelolaan state bersama dan komunikasi lintas banyak tab peramban diimplementasikan.

Pembungkus klien (RPC)

Selanjutnya, buat klien RPC berbasis Promise.

1// shared-worker-client.ts
2import {
3  RequestAction,
4  RequestMessage,
5  WorkerOutMessage,
6  randomId
7} from './worker-protocol.js';
  • Mengimpor definisi tipe dan fungsi utilitas yang digunakan untuk komunikasi worker.
1export type BroadcastHandler = (msg: {
2  channel: string;
3  payload: unknown;
4  from: string
5}) => void;
  • Di sini kita mendefinisikan tipe fungsi callback yang dijalankan saat pesan broadcast diterima.
1export class SharedWorkerClient {
  • SharedWorkerClient adalah kelas klien yang berkomunikasi dengan Shared Worker, mengirim permintaan dan menangani respons.
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  }>();
  • Variabel-variabel ini adalah instance worker, port komunikasi dengan worker, dan map yang melacak permintaan yang menunggu respons.
1  private clientId = randomId();
2  private heartbeatTimer?: number;
3  private onBroadcast?: BroadcastHandler;
  • Variabel-variabel ini menyimpan pengenal klien, timer untuk mengirim heartbeat, dan handler penerimaan broadcast.
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;
  • Di konstruktor, ia menginisialisasi koneksi ke Shared Worker serta menyiapkan pendengar pesan dan pengiriman heartbeat.
 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();
  • Di sini ia menerima pesan dari worker dan menangani respons atau broadcast.
1    this.heartbeatTimer = window.setInterval(() => {
2      this.port.postMessage({ kind: 'heartbeat', from: this.clientId });
3    }, 10_000);
  • Mengirim pesan heartbeat secara berkala untuk menjaga koneksi tetap hidup.
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  }
  • Mengirim notifikasi pemutusan koneksi ke worker sebelum jendela ditutup.
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  }
  • Metode request mengirim aksi yang ditentukan ke worker dan menerima hasilnya sebagai 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  }
  • Ini adalah metode utilitas untuk uji komunikasi dasar dan mendapatkan waktu saat ini.
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  }
  • Ini adalah metode untuk menyimpan dan mengambil pasangan kunci-nilai.
1  broadcast(channel: string, payload: unknown) {
2    return this.request<boolean>({ type: 'broadcast', channel, payload });
3  }
  • Ini adalah metode yang mengirim pesan broadcast ke klien lain melalui worker.
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}
  • Ini adalah metode yang memperoleh dan melepaskan kunci untuk mencapai eksklusi mutual atas sumber daya bersama.
  • Di seluruh berkas ini, sebuah API klien diimplementasikan untuk komunikasi yang aman dan asinkron dari setiap tab peramban ke Shared Worker.

Contoh penggunaan

Di demo.ts, kami menggunakan kelas SharedWorkerClient yang dibuat sebelumnya dan memverifikasi perilakunya. Kode tersebut mengeksekusi secara berurutan serangkaian fungsi termasuk pengujian komunikasi, pembacaan dan penulisan data, broadcast, dan penanganan lock.

 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);
  • Kode ini adalah demo yang menggunakan Shared Worker untuk berbagi dan menyinkronkan data serta state di banyak tab peramban. Dengan menggunakan komunikasi berbasis pesan, Anda dapat bertukar pesan asinkron secara aman dengan keterikatan longgar (loose coupling), sehingga memudahkan pengelolaan komunikasi antar konteks yang berbeda. Selain itu, dengan menggunakan RPC, komunikasi dengan worker diabstraksikan dalam gaya yang intuitif seperti pemanggilan metode, sehingga meningkatkan kemudahan pemeliharaan dan keterbacaan.

Pengujian di 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>

Pertimbangan desain dan operasional

Saat merancang dan mengoperasikan, mengingat poin-poin berikut akan membantu Anda membangun sistem yang lebih tangguh dan dapat diperluas.

  • Anda dapat mengadopsi discriminated union (tagged union) yang memungkinkan percabangan berdasarkan kind atau type.
  • Gunakan correlation ID untuk mencocokkan permintaan dengan respons secara benar.
  • Heartbeat dan pembersihan otomatis dapat mencegah kunci yang terbengkalai.
  • Implementasikan versioning untuk secara fleksibel mengakomodasi perubahan protokol di masa depan.
  • Mendefinisikan kode kesalahan yang jelas memudahkan penanganan di sisi UI dan proses debugging.

Ringkasan

Shared Worker adalah mekanisme inti untuk berbagi data dan state di berbagai tab peramban.

Struktur yang diperkenalkan di sini menyediakan komunikasi RPC yang aman terhadap tipe (type-safe), pemantauan liveness melalui heartbeat, dan mekanisme penguncian, menjadikannya desain tangguh yang dapat digunakan apa adanya di produksi.

Di atas mekanisme ini, Anda juga dapat mengimplementasikan aplikasi berikut.

  • Menserialisasi akses IndexedDB
  • Integrasi dan berbagi koneksi WebSocket
  • Membangun antrean tugas lintas beberapa tab
  • Pembatasan laju dan pengiriman notifikasi progres

Seperti yang Anda lihat, memanfaatkan Shared Worker memungkinkan berbagi data dan pemrosesan secara aman dan efisien di banyak tab.

Anda dapat mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Silakan periksa juga saluran YouTube kami.

YouTube Video