`Shared Worker` sa TypeScript

`Shared Worker` sa TypeScript

Ipinaliliwanag ng artikulong ito ang Shared Worker sa TypeScript.

Ipapaliwanag namin nang detalyado kung paano gumagana ang mga Shared Worker at kung paano ito gamitin sa praktika, na may mga halimbawa ng code sa TypeScript.

YouTube Video

Shared Worker sa TypeScript

Ang Shared Worker ay isang iisang proseso ng worker na ibinabahagi sa maraming tab, window, at iframe sa iisang origin. Sa paggamit nito, maaari mong pamahalaan ang pinagsasaluhang estado at mga resource sa maraming tab ng browser.

Halimbawa, maaari kang mahusay na magpatupad ng pinagsasaluhang koneksyong WebSocket, cache at queue processing na nakasabay sa mga tab, at mutwal na eksklusyon.

Hindi tulad ng isang Dedicated Worker, ang Shared Worker ay tumatanggap ng maraming MessagePort sa pamamagitan ng kaganapang onconnect at maaaring mag-multiplex ng komunikasyon sa maraming kliyente.

Mga pagkakataong dapat mong piliin ang Shared Worker

Sa mga sumusunod na kaso, ang paggamit ng Shared Worker ay angkop.

  • Kapag kailangan mo ng pinagsasaluhang estado o mutwal na eksklusyon sa iba’t ibang tab
  • Kapag nais mong magbahagi ng iisang koneksyong WebSocket o pag-access sa IndexedDB
  • Kapag kailangan mong mag-abiso sa lahat ng tab (broadcast)
  • Kapag nais mong isentro ang mabibigat na proseso upang makatipid ng mga resource

Sa kabaligtaran, sa mga sumusunod na kaso, mas angkop ang ibang mga pamamaraan.

  • Kapag kailangan mo ng cache control o offline na suporta, maaari mong gamitin ang Service Worker.
  • Para sa mabigat na pagproseso na nakapaloob sa iisang tab, maaari mong gamitin ang Dedicated Worker.

Mga hakbang sa implementasyon para sa Shared Worker

Dito, ipatutupad natin ang mga sumusunod nang sunud-sunod gamit ang TypeScript.

  • Type-safe na protocol ng mensahe
  • Request/response (RPC) na nakabatay sa Promise
  • Pag-broadcast sa lahat ng tab
  • Heartbeat at paglilinis ng mga kliyente

Pagsasaayos ng kapaligiran

Lumikha ng configuration para i-compile ang bawat source file na gumagamit ng 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}
  • Sa tsconfig-src.json, i-enable ang mga type definition ng DOM at Web Worker upang ang code ay ma-compile nang ligtas.

Pagpapakahulugan ng protocol ng mensahe

Ang pundasyon ng komunikasyon ay isang typed na kontrata ng mensahe. Ang pagtukoy nito nang maaga ay ginagawang ligtas at madaling palawakin ang mga susunod na komunikasyon.

 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 };
  • Ang RequestAction ay isang discriminated union (tagged union) na kumakatawan sa mga uri ng request na ipinapadala sa worker at tumutukoy sa mga operasyong gaya ng ping, get, at broadcast.
1export interface RequestMessage {
2  kind: 'request';
3  id: string;
4  from: string;
5  action: RequestAction;
6}
  • RequestMessage ang tumutukoy sa estruktura ng mga request message na ipinapadala mula sa client papunta sa worker.
1export interface ResponseMessage {
2  kind: 'response';
3  id: string;
4  ok: boolean;
5  result?: unknown;
6  error?: string;
7}
  • ResponseMessage ang tumutukoy sa estruktura ng mga response message na ibinabalik ng worker sa client.
1export interface BroadcastMessage {
2  kind: 'broadcast';
3  channel: string;
4  payload: unknown;
5  from: string;
6}
  • BroadcastMessage ang tumutukoy sa estruktura ng mga broadcast message na ipinapadala ng worker sa iba pang mga client.
1export type WorkerInMessage =
2  | RequestMessage
3  | { kind: 'heartbeat'; from: string }
4  | { kind: 'bye'; from: string };
  • WorkerInMessage ay isang type na kumakatawan sa lahat ng mensaheng natatanggap ng worker, tulad ng mga request, mga heartbeat, at mga abiso ng pag-disconnect.
1export type WorkerOutMessage = ResponseMessage | BroadcastMessage;
  • WorkerOutMessage ay isang type na kumakatawan sa mga response o broadcast message na ipinapadala ng worker sa client.
1export const randomId = () => Math.random().toString(36).slice(2);
  • randomId ay isang function na lumilikha ng random na alphanumeric na string para gamitin sa mga message ID at katulad nito.

Pagpapatupad ng Shared Worker

Sa shared-worker.ts, irehistro ang mga tab na kumokonekta sa pamamagitan ng kaganapang onconnect at hawakan ang mga mensahe.

1// shared-worker.ts
2/// <reference lib="webworker" />
  • Ang direktibang ito ay nagsasabi sa TypeScript na i-load ang mga type definition para sa Web Workers.
1import {
2  WorkerInMessage,
3  WorkerOutMessage,
4  RequestMessage,
5  ResponseMessage,
6} from './worker-protocol.js';
  • Ini-import ang mga type definition na ginagamit para sa komunikasyon ng worker.
1export default {};
2declare const self: SharedWorkerGlobalScope;
  • Hayagang idinedeklara na ang self ang global scope ng Shared Worker.
1type Client = {
2  id: string;
3  port: MessagePort;
4  lastBeat: number;
5};
  • Client ay isang type na kumakatawan sa identifier ng bawat client, communication port, at huling timestamp ng heartbeat.
1const clients = new Map<string, Client>();
2const kv = new Map<string, unknown>();
3const locks = new Map<string, string>();
4const HEARTBEAT_TIMEOUT = 30_000;
  • Pinamamahalaan ang listahan ng mga nakakonektang client, isang key-value store, estado ng lock, at mga tagal ng timeout.
1function send(port: MessagePort, msg: WorkerOutMessage) {
2  port.postMessage(msg);
3}
  • send ay isang utility function na nagpapadala ng mensahe sa tinukoy na port.
1function respond(req: RequestMessage, ok: boolean, result?: unknown, error?: string): ResponseMessage {
2  return { kind: 'response', id: req.id, ok, result, error };
3}
  • respond ay lumilikha ng response message para sa isang request.
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 ay nagpapadala ng mensahe sa isang tinukoy na channel sa lahat ng client.
1function handleRequest(clientId: string, port: MessagePort, req: RequestMessage) {
  • handleRequest ay nagpoproseso ng mga papasok na request ayon sa uri at ibinabalik ang mga resulta sa client.
 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;
  • Sa code na ito, depende sa uri ng kahilingang natanggap, pinangangasiwaan nito ang pagpapadala at pagtanggap ng mga mensahe, pagkuha at pag-iimbak ng data, at pagbo-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      }
  • Ipinapatupad ng code na ito ang proseso para makakuha ang isang client ng lock para sa tinukoy na key. Kapag hindi pa hawak ang lock, agad itong makukuha; kung ang parehong client ang muling humiling nito, ituturing ding matagumpay ang request. Kung may ibang client na may hawak na ng lock, susubukan muli bawat 25 millisecond hanggang sa ma-release ang lock, at kapag lumampas sa itinakdang timeout (default na 5 segundo), magbabalik ito ng 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}
  • Inaalis ng code na ito ang lock na hawak ng kliyente at nagbabalik ng error na tugon kung kulang ang pahintulot ng kliyente o hindi kilala ang aksyon.
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 ay nagpa-parse ng mga mensaheng natanggap mula sa mga client at humahawak ng mga request at mga 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 ay nag-aalis ng mga na-disconnect na client mula sa registry at estado ng lock.
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);
  • Gumagamit ng setInterval para pana-panahong suriin ang mga heartbeat ng lahat ng client at linisin ang mga koneksyong nag-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 ay tinatawag kapag may bagong tab o pahina na kumokonekta sa Shared Worker, nirehistro ang client at sinisimulan ang komunikasyon.

  • Sa kabuuan ng file na ito, ipinatupad ang mga pundamental na mekanismo ng Shared Worker na nagbibigay-daan sa shared state management at komunikasyon sa maraming browser tab.

Wrapper ng kliyente (RPC)

Susunod, gumawa ng RPC client na nakabatay sa Promise.

1// shared-worker-client.ts
2import {
3  RequestAction,
4  RequestMessage,
5  WorkerOutMessage,
6  randomId
7} from './worker-protocol.js';
  • Ini-import ang mga type definition at utility function na ginagamit para sa komunikasyon ng worker.
1export type BroadcastHandler = (msg: {
2  channel: string;
3  payload: unknown;
4  from: string
5}) => void;
  • Dito natin tinutukoy ang uri ng callback function na tumatakbo kapag may natanggap na broadcast na mensahe.
1export class SharedWorkerClient {
  • SharedWorkerClient ay isang client class na nakikipag-ugnayan sa isang Shared Worker, nagpapadala ng mga request at humahawak ng mga response.
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  }>();
  • Ang mga variable na ito ay ang instance ng worker, ang communication port sa worker, at isang mapa na sumusubaybay sa mga request na naghihintay ng mga sagot.
1  private clientId = randomId();
2  private heartbeatTimer?: number;
3  private onBroadcast?: BroadcastHandler;
  • Ang mga variable na ito ay naglalaman ng identifier ng client, ang timer para sa pagpapadala ng mga heartbeat, at ang handler para sa pagtanggap ng 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;
  • Sa constructor, ini-initialize nito ang koneksyon sa Shared Worker at sine-set up ang mga listener ng mensahe at ang pagpapadala ng 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();
  • Dito tumatanggap ito ng mga mensahe mula sa worker at humahawak ng mga response o broadcast.
1    this.heartbeatTimer = window.setInterval(() => {
2      this.port.postMessage({ kind: 'heartbeat', from: this.clientId });
3    }, 10_000);
  • Nagpapadala ng mga heartbeat message nang pana-panahon upang panatilihing buhay ang koneksyon.
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  }
  • Nagpapadala ng abiso ng disconnect sa worker bago magsara ang window.
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  }
  • Ang method na request ay nagpapadala ng tinukoy na action sa worker at tumatanggap ng resulta bilang isang 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  }
  • Ang mga ito ay mga utility method para sa pangunahing pagsubok ng komunikasyon at pagkuha ng kasalukuyang oras.
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  }
  • Ang mga ito ay mga method para sa pag-imbak at pagkuha ng mga key-value pair.
1  broadcast(channel: string, payload: unknown) {
2    return this.request<boolean>({ type: 'broadcast', channel, payload });
3  }
  • Ito ay isang method na nagpapadala ng mga broadcast message sa ibang mga client sa pamamagitan ng 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}
  • Ang mga ito ay mga method na kumukuha at nagpapakawala ng mga lock upang makamit ang mutual exclusion sa mga pinagsasaluhang resource.
  • Sa kabuuan ng file na ito, ipinatupad ang isang client API para sa ligtas, asynchronous na komunikasyon mula sa bawat browser tab patungo sa Shared Worker.

Halimbawang paggamit

Sa demo.ts, ginagamit natin ang klaseng SharedWorkerClient na ginawa kanina at sinusuri ang pag-uugali nito. Sunod-sunod nitong isinasagawa ang isang serye ng mga function, kabilang ang mga pagsubok sa komunikasyon, pagbasa at pagsulat ng data, pagbo-broadcast, at paghawak ng 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);
  • Ang code na ito ay isang demo na gumagamit ng Shared Worker upang magbahagi at mag-synchronize ng data at estado sa maraming browser tab. Sa paggamit ng message-based na komunikasyon, maaari kang magpalitan ng asynchronous na mga mensahe nang ligtas na may maluwag na pagkakaugnay (loose coupling), na nagpapadali sa pamamahala ng komunikasyon sa pagitan ng magkakaibang konteksto. Dagdag pa rito, sa paggamit ng RPC, ina-abstract nito ang komunikasyon sa worker sa isang intuitive, parang method call na estilo, na nagpapahusay sa maintainability at readability.

Pagsubok sa 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>

Mga konsiderasyon sa disenyo at operasyon

Sa pagdisenyo at pagpapatakbo, ang pagsasaalang-alang sa mga sumusunod na punto ay makatutulong sa iyo na bumuo ng mas matatag at mas napapalawak na sistema.

  • Maaari kang gumamit ng isang discriminated union (tagged union) na nagpapahintulot sa pag-branch batay sa kind o type.
  • Gumamit ng correlation ID upang maitugma nang tama ang mga request sa mga response.
  • Heartbeats at awtomatikong paglilinis ay maaaring pumigil sa mga naiwan na lock.
  • Ipatupad ang versioning upang madaling makaangkop sa mga pagbabagong pang-protokol sa hinaharap.
  • Ang pagtatakda ng malinaw na mga code ng error ay nagpapadali sa paghawak sa panig ng UI at sa pag-debug.

Buod

Ang Shared Worker ay isang pangunahing mekanismo para sa pagbabahagi ng data at estado sa maraming browser tab.

Ang estrukturang ipinakilala rito ay nagbibigay ng type-safe na komunikasyong RPC, liveness monitoring sa pamamagitan ng mga heartbeat, at isang locking mechanism, na ginagawa itong matatag na disenyo na maaaring gamitin nang as-is sa production.

Sa ibabaw ng mekanismong ito, maaari mo ring ipatupad ang mga sumusunod na aplikasyon.

  • Pagse-serialize ng pag-access sa IndexedDB
  • Pagsasama at pagbabahagi ng mga koneksyong WebSocket
  • Pagbuo ng pila ng mga gawain sa maraming tab
  • Throttling at paghahatid ng mga abiso ng progreso

Gaya ng nakikita mo, ang paggamit sa Shared Worker ay nagpapahintulot na magbahagi ng data at pagproseso nang ligtas at mahusay sa maraming tab.

Maaari mong sundan ang artikulo sa itaas gamit ang Visual Studio Code sa aming YouTube channel. Paki-check din ang aming YouTube channel.

YouTube Video