• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

naver / billboard.js / 30630758235

31 Jul 2026 12:28PM UTC coverage: 93.462% (-0.2%) from 93.703%
30630758235

Pull #4181

github

web-flow
Merge 401c13b2d into b4fe41a54
Pull Request #4181: fix(text): batch overlap class updates

11636 of 13015 branches covered (89.4%)

Branch coverage included in aggregate %.

257 of 289 new or added lines in 5 files covered. (88.93%)

14723 of 15188 relevant lines covered (96.94%)

28984.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.32
/src/module/worker.ts
1
/**
2
 * Copyright (c) 2017 ~ present NAVER Corp.
3
 * billboard.js project is licensed under the MIT license
4
 */
5
import {window} from "./browser";
6

7
// Store worker cache in memory
8
const cache: {
9
        [key: string]: {
10
                depsString: string,
11
                fnString: string,
12
                revoke: boolean,
13
                src: string,
14
                worker: Worker | null,
15
                workerUrl?: string
16
        }
17
} = {};
261✔
18
const disabledKeys = new Set<string>();
261✔
19
const verifiedKeys = new Set<string>();
261✔
20
const DEFAULT_WORKER_TIMEOUT = 5000;
261✔
21

22
// Correlation id for worker request/response matching
23
let messageId = 0;
261✔
24

25
type TWorkerOptions = {timeout?: number, workerUrl?: string};
26

27
/**
28
 * Get Web Worker related browser APIs when all required primitives are available.
29
 * @returns {object|null} Worker API handles
30
 * @private
31
 */
32
function getWorkerAPI():
33
        | {Blob: typeof Blob, Worker: typeof Worker, URL: typeof URL}
34
        | null {
35
        const {Blob, Worker, URL} = window;
1,110✔
36

37
        return Worker && Blob && URL?.createObjectURL && URL?.revokeObjectURL ?
1,110!
38
                {Blob, Worker, URL} :
39
                null;
40
}
41

42
/**
43
 * Get Worker constructor when available.
44
 * @returns {function|null} Worker constructor
45
 * @private
46
 */
47
function getWorkerConstructor(): typeof Worker | null {
48
        return window.Worker || null;
120!
49
}
50

51
/**
52
 * Normalize worker options while preserving the legacy numeric timeout argument.
53
 * @param {number|object} options Worker timeout or options
54
 * @returns {object} Normalized worker options
55
 * @private
56
 */
57
function normalizeWorkerOptions(options?: number | TWorkerOptions): Required<TWorkerOptions> {
58
        return typeof options === "number" ? {timeout: options, workerUrl: ""} : {
7,623✔
59
                timeout: options?.timeout ?? DEFAULT_WORKER_TIMEOUT,
15,240✔
60
                workerUrl: options?.workerUrl ?? ""
15,231✔
61
        };
62
}
63

64
/**
65
 * Generate a stable cache key from the worker source.
66
 * @param {string} str Worker source string
67
 * @returns {string} Cache key
68
 * @private
69
 */
70
function hashString(str: string): string {
71
        let hash = 2166136261;
60✔
72

73
        for (let i = 0, len = str.length; i < len; i++) {
60✔
74
                hash ^= str.charCodeAt(i);
35,385✔
75
                hash = Math.imul(hash, 16777619);
35,385✔
76
        }
77

78
        return `worker-${str.length}-${(hash >>> 0).toString(36)}`;
60✔
79
}
80

81
/**
82
 * Release cached worker resources and optionally disable this worker for the session.
83
 * @param {string} key Cache key
84
 * @param {boolean} disable Whether to disable future worker attempts
85
 * @private
86
 */
87
function releaseWorker(key: string, disable = false): void {
×
88
        const cached = cache[key];
15✔
89

90
        if (disable) {
15!
91
                disabledKeys.add(key);
15✔
92
        }
93

94
        if (cached) {
15!
95
                cached.worker?.terminate();
15✔
96
                cached.revoke && getWorkerAPI()?.URL.revokeObjectURL(cached.src);
15✔
97
                delete cache[key];
15✔
98
        }
99

100
        verifiedKeys.delete(key);
15✔
101
}
102

103
/**
104
 * Compare worker and main-thread results for the parity self-test.
105
 * @param {unknown} actual Worker result
106
 * @param {unknown} expected Main-thread result
107
 * @returns {boolean} Whether results match
108
 * @private
109
 */
110
function isSameResult(actual, expected): boolean {
111
        if (actual === expected) {
15✔
112
                return true;
12✔
113
        }
114

115
        try {
3✔
116
                return JSON.stringify(actual) === JSON.stringify(expected);
3✔
117
        } catch {
118
                // Structured-cloned worker results should normally be serializable. If a custom
119
                // worker returns an exotic value, don't disable the worker on an unverifiable result.
NEW
120
                return true;
×
121
        }
122
}
123

124
/**
125
 * Get or create cached worker resources (Object URL, Worker)
126
 * @param {function} fn Function to be executed in worker
127
 * @param {Array} depsFn Dependency functions to run given function(fn).
128
 * @param {string} workerUrl Custom worker script URL.
129
 * @returns {{key: string, src: string}} Cache key and Object URL
130
 * @private
131
 */
132
function getOrCreateWorkerResources(fn: Function, depsFn?: Function[], workerUrl = ""):
×
133
        | {key: string, src: string, depsString: string, fnString: string, workerUrl?: string}
134
        | null {
135
        const hasWorker = !!getWorkerConstructor();
60✔
136
        const api = workerUrl ? null : getWorkerAPI();
60✔
137
        const fnString = fn.toString();
60✔
138
        // Include depsFn in cache key to handle different dependencies
139
        const depsString = depsFn?.map(String).join(";") ?? "";
60✔
140
        const key = hashString(`${workerUrl}\n${fnString}\n${depsString}`);
60✔
141

142
        if (!hasWorker || (!workerUrl && !api) || disabledKeys.has(key)) {
60!
143
                return null;
×
144
        }
145

146
        if (!(key in cache)) {
60✔
147
                try {
36✔
148
                        if (workerUrl) {
36✔
149
                                cache[key] = {
3✔
150
                                        depsString,
151
                                        fnString,
152
                                        revoke: false,
153
                                        src: workerUrl,
154
                                        worker: null,
155
                                        workerUrl
156
                                };
157
                        } else if (api) {
33!
158
                                // Create Blob and Object URL for Web Worker
159
                                const blob = new api.Blob([
33✔
160
                                        `${depsString}
161

162
                                self.onmessage=function({data}) {
163
                                        try {
164
                                                const result = (${fnString}).apply(null, data.args);
165
                                                self.postMessage({id: data.id, result});
166
                                        } catch (error) {
167
                                                self.postMessage({
168
                                                        id: data.id,
169
                                                        error: error && (error.message || error.name) || String(error)
170
                                                });
171
                                        }
172
                                };`
173
                                ], {
174
                                        type: "text/javascript"
175
                                });
176

177
                                cache[key] = {
33✔
178
                                        depsString,
179
                                        fnString,
180
                                        revoke: true,
181
                                        src: api.URL.createObjectURL(blob),
182
                                        worker: null
183
                                };
184
                        }
185
                } catch {
186
                        return null;
3✔
187
                }
188
        }
189

190
        return {
57✔
191
                key,
192
                src: cache[key].src,
193
                depsString: cache[key].depsString,
194
                fnString: cache[key].fnString,
195
                workerUrl: cache[key].workerUrl
196
        };
197
}
198

199
/**
200
 * Get or create cached WebWorker instance
201
 * @param {string} key Cache key
202
 * @param {string} src URL object as string
203
 * @returns {Worker} WebWorker instance
204
 * @private
205
 */
206
export function getWorker(key: string, src: string): Worker | null {
207
        const cached = cache[key];
60✔
208
        const Worker = getWorkerConstructor();
60✔
209

210
        // Return null if cache entry doesn't exist
211
        if (!cached || !Worker || disabledKeys.has(key)) {
60✔
212
                return null;
3✔
213
        }
214

215
        if (!cached.worker) {
57✔
216
                try {
33✔
217
                        cached.worker = new Worker(src);
33✔
218
                } catch {
219
                        releaseWorker(key, true);
3✔
220
                        return null;
3✔
221
                }
222
        }
223

224
        return cached.worker;
54✔
225
}
226

227
/**
228
 * Create and run on Web Worker
229
 * @param {boolean} useWorker Use Web Worker
230
 * @param {function} fn Function to be executed in worker
231
 * @param {function} callback Callback function to receive result from worker
232
 * @param {Array} depsFn Dependency functions to run given function(fn).
233
 * @param {number|object} options Worker response timeout or options.
234
 * @returns {function}
235
 * @example
236
 *         const worker = runWorker(function(arg) {
237
 *                   // do some tasks...
238
 *                   console.log("param:", A(arg));
239
 *
240
 *                   return 1234;
241
 *            }, function(data) {
242
 *                   // callback after worker is done
243
 *                    console.log("result:", data);
244
 *            },
245
 *            [function A(){}]
246
 *         );
247
 *
248
 *         worker(11111);
249
 * @private
250
 */
251
export function runWorker(
252
        useWorker = true,
×
253
        fn: Function,
254
        callback: Function,
255
        depsFn?: Function[],
256
        options?: number | TWorkerOptions
257
): Function {
258
        const {timeout, workerUrl} = normalizeWorkerOptions(options);
7,623✔
259
        const runSync = function(...args: unknown[]) {
7,623✔
260
                const res = fn(...args);
7,578✔
261

262
                callback(res);
7,572✔
263
        };
264
        let runFn = runSync;
7,623✔
265

266
        if (useWorker) {
7,623✔
267
                const workerResources = getOrCreateWorkerResources(fn, depsFn, workerUrl);
60✔
268
                const worker = workerResources ? getWorker(workerResources.key, workerResources.src) : null;
60✔
269

270
                if (worker && workerResources) {
60✔
271
                        const {depsString, fnString, key} = workerResources;
54✔
272

273
                        runFn = function(...args: unknown[]) {
54✔
274
                                // workers are cached and shared: match the response by id so concurrent
275
                                // callers don't steal each other's result
276
                                const id = ++messageId;
54✔
277
                                let settled = false;
54✔
278

279
                                const fallback = () => {
54✔
280
                                        if (!settled) {
12✔
281
                                                settled = true;
9✔
282
                                                cleanup();
9✔
283
                                                releaseWorker(key, true);
9✔
284
                                                runFn = runSync;
9✔
285
                                                runSync(...args);
9✔
286
                                        }
287
                                };
288

289
                                const handler = function(e: MessageEvent) {
54✔
290
                                        if (e.data?.id === id) {
15!
291
                                                if (e.data.error) {
15!
292
                                                        fallback();
×
293
                                                        return;
×
294
                                                }
295

296
                                                settled = true;
15✔
297
                                                cleanup();
15✔
298

299
                                                if (!verifiedKeys.has(key)) {
15!
300
                                                        const expected = fn(...args);
15✔
301

302
                                                        if (!isSameResult(e.data.result, expected)) {
15✔
303
                                                                releaseWorker(key, true);
3✔
304
                                                                runFn = runSync;
3✔
305
                                                                callback(expected);
3✔
306
                                                                return;
3✔
307
                                                        }
308

309
                                                        verifiedKeys.add(key);
12✔
310
                                                }
311

312
                                                callback(e.data.result);
12✔
313
                                        }
314
                                };
315

316
                                const errorHandler = function() {
54✔
317
                                        fallback();
6✔
318
                                };
319

320
                                const timer = setTimeout(fallback, timeout);
54✔
321
                                const cleanup = () => {
54✔
322
                                        clearTimeout(timer);
24✔
323
                                        worker.removeEventListener("message", handler);
24✔
324
                                        worker.removeEventListener("error", errorHandler);
24✔
325
                                };
326

327
                                worker.addEventListener("message", handler);
54✔
328
                                worker.addEventListener("error", errorHandler);
54✔
329

330
                                try {
54✔
331
                                        worker.postMessage(workerUrl ? {id, args, deps: depsString, fn: fnString} : {
54✔
332
                                                id,
333
                                                args
334
                                        });
335
                                } catch {
336
                                        fallback();
3✔
337
                                }
338
                        };
339
                }
340
        }
341

342
        return runFn;
7,623✔
343
}
344

345
/**
346
 * Clean-up all cached workers and release resources
347
 * @private
348
 */
349
export function cleanupWorkers(): void {
350
        const api = getWorkerAPI();
1,038✔
351

352
        for (const key in cache) {
1,038✔
353
                const cached = cache[key];
15✔
354

355
                if (cached.worker) {
15!
356
                        cached.worker.terminate();
15✔
357
                }
358

359
                if (cached.src) {
15!
360
                        cached.revoke && api?.URL.revokeObjectURL(cached.src);
15✔
361
                }
362

363
                delete cache[key];
15✔
364
        }
365

366
        disabledKeys.clear();
1,038✔
367
        verifiedKeys.clear();
1,038✔
368
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc