`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 sepertiping
,get
, danbroadcast
.
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 dariShared 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 keShared 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 denganShared 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 sebagaiPromise
.
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
atautype
. - 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.