@agorapete/wllama 3.5.1-q2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.gitmodules +3 -0
  2. package/.prettierignore +38 -0
  3. package/AGENTS.md +1 -0
  4. package/CMakeLists.txt +131 -0
  5. package/LICENCE +21 -0
  6. package/README-dev.md +178 -0
  7. package/README.md +225 -0
  8. package/README_banner.png +0 -0
  9. package/assets/screenshot_0.png +0 -0
  10. package/cpp/generate_glue_prototype.js +115 -0
  11. package/cpp/glue.hpp +664 -0
  12. package/cpp/test_glue.cpp +80 -0
  13. package/cpp/wllama-context.h +1172 -0
  14. package/cpp/wllama-fs.h +148 -0
  15. package/cpp/wllama.cpp +187 -0
  16. package/cpp/wllama.h +6 -0
  17. package/esm/cache-manager.d.ts +130 -0
  18. package/esm/debug.d.ts +28 -0
  19. package/esm/glue/glue.d.ts +22 -0
  20. package/esm/glue/messages.d.ts +146 -0
  21. package/esm/huggingface.d.ts +31 -0
  22. package/esm/index.cjs +3406 -0
  23. package/esm/index.d.ts +8 -0
  24. package/esm/index.js +3387 -0
  25. package/esm/index.min.js +1 -0
  26. package/esm/index.min.js.map +1 -0
  27. package/esm/model-manager.d.ts +136 -0
  28. package/esm/storage/cos.d.ts +36 -0
  29. package/esm/storage/index.d.ts +33 -0
  30. package/esm/storage/opfs.d.ts +12 -0
  31. package/esm/types/oai-compat.d.ts +278 -0
  32. package/esm/types/types.d.ts +112 -0
  33. package/esm/utils.d.ts +119 -0
  34. package/esm/wasm/source-map.d.ts +1 -0
  35. package/esm/wasm/wllama.wasm +0 -0
  36. package/esm/wasm-from-cdn.d.ts +8 -0
  37. package/esm/wllama.d.ts +397 -0
  38. package/esm/worker.d.ts +92 -0
  39. package/esm/workers-code/generated.d.ts +4 -0
  40. package/guides/intro-v2.md +132 -0
  41. package/guides/intro-v3.1.md +40 -0
  42. package/guides/intro-v3.md +230 -0
  43. package/index.ts +1 -0
  44. package/package.json +71 -0
  45. package/scripts/bisect_test.sh +33 -0
  46. package/scripts/build_hf_space.sh +26 -0
  47. package/scripts/build_source_map.js +269 -0
  48. package/scripts/build_wasm.sh +19 -0
  49. package/scripts/build_worker.sh +38 -0
  50. package/scripts/check_debug_build.js +30 -0
  51. package/scripts/check_package_size.js +25 -0
  52. package/scripts/docker-compose.yml +76 -0
  53. package/scripts/generate_wasm_from_cdn.js +24 -0
  54. package/scripts/http_server.js +44 -0
  55. package/scripts/post_build.sh +32 -0
  56. package/src/cache-manager.ts +358 -0
  57. package/src/debug.ts +111 -0
  58. package/src/glue/glue.ts +291 -0
  59. package/src/glue/messages.ts +773 -0
  60. package/src/huggingface.ts +151 -0
  61. package/src/index.ts +8 -0
  62. package/src/mjs.test.ts +44 -0
  63. package/src/model-manager.test.ts +200 -0
  64. package/src/model-manager.ts +359 -0
  65. package/src/storage/cos.test.ts +83 -0
  66. package/src/storage/cos.ts +171 -0
  67. package/src/storage/index.ts +40 -0
  68. package/src/storage/opfs.ts +119 -0
  69. package/src/types/oai-compat.ts +342 -0
  70. package/src/types/types.ts +133 -0
  71. package/src/utils.test.ts +231 -0
  72. package/src/utils.ts +403 -0
  73. package/src/wasm/source-map.ts +7 -0
  74. package/src/wasm/wllama.js +1 -0
  75. package/src/wasm/wllama.wasm +0 -0
  76. package/src/wasm-from-cdn.ts +13 -0
  77. package/src/wllama.test.ts +392 -0
  78. package/src/wllama.ts +1138 -0
  79. package/src/wllama.wgpu.test.ts +62 -0
  80. package/src/worker.ts +443 -0
  81. package/src/workers-code/generated.ts +11 -0
  82. package/src/workers-code/llama-cpp.js +511 -0
  83. package/src/workers-code/opfs-utils.js +150 -0
  84. package/tsconfig.build.json +34 -0
  85. package/tsup.config.ts +23 -0
  86. package/vitest.config.ts +61 -0
@@ -0,0 +1,511 @@
1
+ // Start the main llama.cpp
2
+ let wllamaMalloc;
3
+ let wllamaStart;
4
+ let wllamaAction;
5
+ let wllamaExit;
6
+ let wllamaDebug;
7
+
8
+ let Module = null;
9
+ let isCompat = false;
10
+ let lastStack = '';
11
+ let isAborted = false;
12
+ let hasMultithread = false;
13
+
14
+ //////////////////////////////////////////////////////////////
15
+ // UTILS
16
+ //////////////////////////////////////////////////////////////
17
+
18
+ // send message back to main thread
19
+ const msg = (data, transfer) => postMessage(data, transfer);
20
+
21
+ // Convert CPP log into JS log
22
+ const cppLogToJSLog = (line) => {
23
+ const matched = line.match(/@@(DEBUG|INFO|WARN|ERROR)@@(.*)/);
24
+ return !!matched
25
+ ? {
26
+ level: (matched[1] === 'INFO' ? 'debug' : matched[1]).toLowerCase(),
27
+ text: matched[2],
28
+ }
29
+ : { level: 'log', text: line };
30
+ };
31
+
32
+ const getHeapU8 = () => {
33
+ const buffer = Module.wasmMemory.buffer;
34
+ return new Uint8Array(buffer);
35
+ };
36
+
37
+ const toSizeT = (num) => {
38
+ return isCompat ? Number(num) : BigInt(num);
39
+ };
40
+
41
+ // Get module config that forwards stdout/err to main thread
42
+ const getWModuleConfig = (_argMainScriptBlob) => {
43
+ var pathConfig = RUN_OPTIONS.pathConfig;
44
+ var pthreadPoolSize = RUN_OPTIONS.nbThread;
45
+ var argMainScriptBlob = _argMainScriptBlob;
46
+
47
+ isCompat = RUN_OPTIONS.compat;
48
+ hasMultithread = pthreadPoolSize > 1;
49
+
50
+ msg({
51
+ verb: 'console.debug',
52
+ args: [
53
+ `Multithread enabled: ${hasMultithread}, pthreadPoolSize: ${pthreadPoolSize}`,
54
+ ],
55
+ });
56
+
57
+ if (!pathConfig['wllama.wasm']) {
58
+ throw new Error('"wllama.wasm" is missing in pathConfig');
59
+ }
60
+ return {
61
+ noInitialRun: true,
62
+ print: function (text) {
63
+ if (arguments.length > 1)
64
+ text = Array.prototype.slice.call(arguments).join(' ');
65
+ msg({ verb: 'console.log', args: [text] });
66
+ },
67
+ printErr: function (text) {
68
+ if (arguments.length > 1)
69
+ text = Array.prototype.slice.call(arguments).join(' ');
70
+ if (text.startsWith('@@STACK@@')) {
71
+ lastStack = text.slice('@@STACK@@'.length);
72
+ return;
73
+ }
74
+ const logLine = cppLogToJSLog(text);
75
+ msg({ verb: 'console.' + logLine.level, args: [logLine.text] });
76
+ },
77
+ locateFile: function (filename, basePath) {
78
+ const p = pathConfig[filename];
79
+ const truncate = (str) =>
80
+ str.length > 128 ? `${str.substr(0, 128)}...` : str;
81
+ if (filename.match(/wllama\.worker\.js/)) {
82
+ msg({
83
+ verb: 'console.error',
84
+ args: [
85
+ '"wllama.worker.js" is removed from v2.2.1. Hint: make sure to clear browser\'s cache.',
86
+ ],
87
+ });
88
+ } else {
89
+ msg({
90
+ verb: 'console.debug',
91
+ args: [`Loading "${filename}" from "${truncate(p)}"`],
92
+ });
93
+ return p;
94
+ }
95
+ },
96
+ mainScriptUrlOrBlob: hasMultithread
97
+ ? argMainScriptBlob
98
+ : 'throw new Error("Multithreading is not enabled")',
99
+ pthreadPoolSize: hasMultithread ? pthreadPoolSize : 0,
100
+ wasmMemory: hasMultithread ? getWasmMemory() : null,
101
+ onAbort: function (message) {
102
+ isAborted = true;
103
+ msg({ verb: 'signal.abort', args: ['abort', message, lastStack, null] });
104
+ },
105
+ onExit: function (code) {
106
+ isAborted = true;
107
+ const callstack = new Error().stack.toString();
108
+ msg({
109
+ verb: 'signal.abort',
110
+ args: ['abort', 'exit(' + code + ')', callstack, null],
111
+ });
112
+ },
113
+ };
114
+ };
115
+
116
+ // Get the memory to be used by wasm. (Only used in multi-thread mode)
117
+ // Because we have a weird OOM issue on iOS, we need to try some values
118
+ // See: https://github.com/emscripten-core/emscripten/issues/19144
119
+ // https://github.com/godotengine/godot/issues/70621
120
+ const getWasmMemory = () => {
121
+ let minBytes = 128 * 1024 * 1024;
122
+ let maxBytes = 4096 * 1024 * 1024;
123
+ let stepBytes = 128 * 1024 * 1024;
124
+ while (maxBytes > minBytes) {
125
+ try {
126
+ const wasmMemory = new WebAssembly.Memory({
127
+ initial: toSizeT(minBytes / 65536),
128
+ maximum: toSizeT(maxBytes / 65536),
129
+ shared: true,
130
+ address: isCompat ? undefined : 'i64',
131
+ });
132
+ return wasmMemory;
133
+ } catch (e) {
134
+ maxBytes -= stepBytes;
135
+ continue; // retry
136
+ }
137
+ }
138
+ throw new Error('Cannot allocate WebAssembly.Memory');
139
+ };
140
+
141
+ //////////////////////////////////////////////////////////////
142
+ // HEAPFS PATCH
143
+ //////////////////////////////////////////////////////////////
144
+
145
+ /**
146
+ * By default, emscripten uses memfs. The way it works is by
147
+ * allocating new Uint8Array in javascript heap. This is not good
148
+ * because it requires files to be copied to wasm heap each time
149
+ * a file is read.
150
+ *
151
+ * HeapFS is an alternative, which resolves this problem by
152
+ * allocating space for file directly inside wasm heap. This
153
+ * allows us to mmap without doing any copy.
154
+ *
155
+ * For llama.cpp, this is great because we use MAP_SHARED
156
+ *
157
+ * Ref: https://github.com/ngxson/wllama/pull/39
158
+ * Ref: https://github.com/emscripten-core/emscripten/blob/main/src/library_memfs.js
159
+ *
160
+ * Note 29/05/2024 @ngxson
161
+ * Due to ftell() being limited to MAX_LONG, we cannot load files bigger than 2^31 bytes (or 2GB)
162
+ * Ref: https://github.com/emscripten-core/emscripten/blob/main/system/lib/libc/musl/src/stdio/ftell.c
163
+ */
164
+
165
+ const fsNameToFile = {}; // map Name => File
166
+ const fsIdToFile = {}; // map ID => File
167
+ let currFileId = 0;
168
+
169
+ // Patch and redirect memfs calls to wllama
170
+ const patchHeapFS = () => {
171
+ const m = Module;
172
+ // save functions
173
+ m.MEMFS.stream_ops._read = m.MEMFS.stream_ops.read;
174
+ m.MEMFS.stream_ops._write = m.MEMFS.stream_ops.write;
175
+ m.MEMFS.stream_ops._llseek = m.MEMFS.stream_ops.llseek;
176
+ m.MEMFS.stream_ops._allocate = m.MEMFS.stream_ops.allocate;
177
+ m.MEMFS.stream_ops._mmap = m.MEMFS.stream_ops.mmap;
178
+ m.MEMFS.stream_ops._msync = m.MEMFS.stream_ops.msync;
179
+
180
+ const patchStream = (stream) => {
181
+ const name = stream.node.name;
182
+ if (fsNameToFile[name]) {
183
+ const f = fsNameToFile[name];
184
+ const ptr = Number(f.ptr);
185
+ stream.node.contents = getHeapU8().subarray(ptr, ptr + f.size);
186
+ stream.node.usedBytes = f.size;
187
+ }
188
+ };
189
+
190
+ // replace "read" functions
191
+ m.MEMFS.stream_ops.read = function (
192
+ stream,
193
+ buffer,
194
+ offset,
195
+ length,
196
+ position
197
+ ) {
198
+ patchStream(stream);
199
+ return m.MEMFS.stream_ops._read(stream, buffer, offset, length, position);
200
+ };
201
+ m.MEMFS.ops_table.file.stream.read = m.MEMFS.stream_ops.read;
202
+
203
+ // replace "llseek" functions
204
+ m.MEMFS.stream_ops.llseek = function (stream, offset, whence) {
205
+ patchStream(stream);
206
+ return m.MEMFS.stream_ops._llseek(stream, offset, whence);
207
+ };
208
+ m.MEMFS.ops_table.file.stream.llseek = m.MEMFS.stream_ops.llseek;
209
+
210
+ // replace "mmap" functions
211
+ m.MEMFS.stream_ops.mmap = function (stream, length, position, prot, flags) {
212
+ patchStream(stream);
213
+ const name = stream.node.name;
214
+ if (fsNameToFile[name]) {
215
+ const f = fsNameToFile[name];
216
+ const mmapPtr = f.ptr + toSizeT(position);
217
+ return {
218
+ ptr: mmapPtr,
219
+ allocated: false,
220
+ };
221
+ } else {
222
+ return m.MEMFS.stream_ops._mmap(stream, length, position, prot, flags);
223
+ }
224
+ };
225
+ m.MEMFS.ops_table.file.stream.mmap = m.MEMFS.stream_ops.mmap;
226
+
227
+ // mount FS
228
+ m.FS.mkdir('/models');
229
+ m.FS.mount(m.MEMFS, { root: '.' }, '/models');
230
+ };
231
+
232
+ // Allocate a new file in wllama heapfs, returns file ID
233
+ const heapfsAlloc = (name, size, allocBuffer) => {
234
+ if (size < 1) {
235
+ throw new Error('File size must be bigger than 0');
236
+ }
237
+ const m = Module;
238
+ const ptr = toSizeT(allocBuffer ? m.mmapAlloc(size) : 0);
239
+ const file = {
240
+ ptr: ptr,
241
+ size: size,
242
+ id: currFileId++,
243
+ };
244
+ fsIdToFile[file.id] = file;
245
+ fsNameToFile[name] = file;
246
+ return file.id;
247
+ };
248
+
249
+ // Add new file to wllama heapfs, return number of written bytes
250
+ const heapfsWrite = (id, buffer, offset) => {
251
+ if (fsIdToFile[id]) {
252
+ const { ptr, size } = fsIdToFile[id];
253
+ const afterWriteByte = offset + buffer.byteLength;
254
+ if (afterWriteByte > size) {
255
+ throw new Error(
256
+ `File ID ${id} write out of bound, afterWriteByte = ${afterWriteByte} while size = ${size}`
257
+ );
258
+ }
259
+ getHeapU8().set(buffer, Number(ptr) + offset);
260
+ return buffer.byteLength;
261
+ } else {
262
+ throw new Error(`File ID ${id} not found in heapfs`);
263
+ }
264
+ };
265
+
266
+ //////////////////////////////////////////////////////////////
267
+ // ASYNC FILE READ
268
+ //////////////////////////////////////////////////////////////
269
+
270
+ let isAwaitReading = false;
271
+ let pendingReadPromise = null;
272
+ let pendingReadResolve = null;
273
+ let pendingReadReject = null;
274
+
275
+ const _stripModelsPrefix = (path) => path.replace(/^\/?models\//, '');
276
+
277
+ // Called from EM_ASYNC_JS stub in wllama-fs.h (path is already a JS string)
278
+ const _wllama_js_file_read = async (path, offset, req_size, out_ptr) => {
279
+ const name = _stripModelsPrefix(path);
280
+
281
+ pendingReadPromise = new Promise((res, rej) => {
282
+ pendingReadResolve = res;
283
+ pendingReadReject = rej;
284
+ });
285
+ isAwaitReading = true;
286
+
287
+ postMessage({ verb: 'fs.read_req', args: [name, offset, req_size] });
288
+
289
+ let data;
290
+ try {
291
+ data = await pendingReadPromise;
292
+ } finally {
293
+ isAwaitReading = false;
294
+ pendingReadResolve = null;
295
+ pendingReadReject = null;
296
+ }
297
+
298
+ const bytes = new Uint8Array(data);
299
+ getHeapU8().set(bytes, out_ptr);
300
+ return toSizeT(bytes.length);
301
+ };
302
+
303
+ //////////////////////////////////////////////////////////////
304
+ // MAIN CODE
305
+ //////////////////////////////////////////////////////////////
306
+
307
+ const callWrapper = (name, ret, args, isAsync) => {
308
+ const fn = Module.cwrap(
309
+ name,
310
+ ret,
311
+ args,
312
+ isAsync ? { async: true } : undefined
313
+ );
314
+ return async (action, req) => {
315
+ // console.log(`Calling ${name} with action:`, action, 'and req:', req);
316
+ let result;
317
+ try {
318
+ if (args.length === 2) {
319
+ result = isAsync ? await fn(action, req) : fn(action, req);
320
+ } else {
321
+ result = fn();
322
+ }
323
+ } catch (ex) {
324
+ console.error(ex);
325
+ throw ex;
326
+ }
327
+ return result;
328
+ };
329
+ };
330
+
331
+ function handleError(err) {
332
+ // If WASM already aborted, onAbort already sent signal.abort; skip to avoid
333
+ // re-reporting the resulting WebAssembly.RuntimeError as a JS exception.
334
+ if (isAborted) return;
335
+
336
+ const message = err ? err.message || String(err) : 'Unknown error';
337
+ const stack = err ? err.stack || String(err) : '';
338
+ msg({
339
+ verb: 'signal.abort',
340
+ args: ['exception', message, stack, err],
341
+ });
342
+ }
343
+
344
+ onmessage = async (e) => {
345
+ if (!e.data) return;
346
+ const { verb, args, callbackId } = e.data;
347
+
348
+ // fs.read_res arrives while wasm is JSPI-suspended; resolve the pending promise.
349
+ if (verb === 'fs.read_res') {
350
+ if (pendingReadResolve) {
351
+ pendingReadResolve(args[0]);
352
+ }
353
+ return;
354
+ }
355
+
356
+ // Guard: while awaiting a file read, reject any other incoming task.
357
+ if (isAwaitReading) {
358
+ if (callbackId) {
359
+ msg({
360
+ callbackId,
361
+ err: 'Worker is suspended waiting for file data (JSPI)',
362
+ });
363
+ }
364
+ return;
365
+ }
366
+
367
+ if (!callbackId) {
368
+ msg({ verb: 'console.error', args: ['callbackId is required', e.data] });
369
+ return;
370
+ }
371
+
372
+ if (verb === 'module.init') {
373
+ const argMainScriptBlob = args[0];
374
+ const argUseAsyncFile = args[1];
375
+ try {
376
+ Module = getWModuleConfig(argMainScriptBlob);
377
+ Module.preRun = () => {
378
+ if (argUseAsyncFile) {
379
+ Module.ENV['USE_ASYNC_FILE'] = '1';
380
+ }
381
+ };
382
+ Module.onRuntimeInitialized = () => {
383
+ // async call once module is ready
384
+ // init FS
385
+ patchHeapFS();
386
+ // init cwrap
387
+ const pointer = isCompat ? 'number' : 'bigint';
388
+ // TODO: note sure why emscripten cannot bind if there is only 1 argument
389
+ wllamaMalloc = callWrapper('wllama_malloc', pointer, [
390
+ 'number',
391
+ pointer,
392
+ ]);
393
+ wllamaStart = callWrapper('wllama_start', 'string', [], true);
394
+ wllamaAction = callWrapper(
395
+ 'wllama_action',
396
+ pointer,
397
+ ['string', pointer],
398
+ true
399
+ );
400
+ wllamaExit = callWrapper('wllama_exit', 'string', []);
401
+ wllamaDebug = callWrapper('wllama_debug', 'string', []);
402
+ msg({ callbackId, result: null });
403
+ };
404
+ wModuleInit();
405
+ } catch (err) {
406
+ handleError(err);
407
+ }
408
+ return;
409
+ }
410
+
411
+ if (verb === 'fs.alloc') {
412
+ const argFilename = args[0];
413
+ const argSize = args[1];
414
+ const argAllocBuffer = args[2];
415
+ try {
416
+ // create blank file
417
+ const emptyBuffer = new ArrayBuffer(0);
418
+ Module['FS_createDataFile'](
419
+ '/models',
420
+ argFilename,
421
+ emptyBuffer,
422
+ true,
423
+ true,
424
+ true
425
+ );
426
+ // alloc data on heap
427
+ const fileId = heapfsAlloc(argFilename, argSize, argAllocBuffer);
428
+ msg({ callbackId, result: { fileId } });
429
+ } catch (err) {
430
+ handleError(err);
431
+ }
432
+ return;
433
+ }
434
+
435
+ if (verb === 'fs.write') {
436
+ const argFileId = args[0];
437
+ const argBuffer = args[1];
438
+ const argOffset = args[2];
439
+ try {
440
+ const writtenBytes = heapfsWrite(argFileId, argBuffer, argOffset);
441
+ msg({ callbackId, result: { writtenBytes } });
442
+ } catch (err) {
443
+ handleError(err);
444
+ }
445
+ return;
446
+ }
447
+
448
+ if (verb === 'wllama.start') {
449
+ try {
450
+ const result = await wllamaStart();
451
+ msg({ callbackId, result });
452
+ } catch (err) {
453
+ handleError(err);
454
+ }
455
+ return;
456
+ }
457
+
458
+ if (verb === 'wllama.action') {
459
+ const argAction = args[0];
460
+ const argEncodedMsg = args[1];
461
+ try {
462
+ const inputPtr = await wllamaMalloc(toSizeT(argEncodedMsg.byteLength), 0);
463
+ // copy data to wasm heap
464
+ const inputBuffer = new Uint8Array(
465
+ getHeapU8().buffer,
466
+ Number(inputPtr),
467
+ argEncodedMsg.byteLength
468
+ );
469
+ inputBuffer.set(argEncodedMsg, 0);
470
+ const outputPtr = await wllamaAction(argAction, inputPtr);
471
+ // length of output buffer is written at the first 4 bytes of input buffer
472
+ const outputLen = new Uint32Array(
473
+ getHeapU8().buffer,
474
+ Number(inputPtr),
475
+ 1
476
+ )[0];
477
+ // copy the output buffer to JS heap
478
+ const outputBuffer = new Uint8Array(outputLen);
479
+ const outputSrcView = new Uint8Array(
480
+ getHeapU8().buffer,
481
+ Number(outputPtr),
482
+ outputLen
483
+ );
484
+ outputBuffer.set(outputSrcView, 0); // copy it
485
+ msg({ callbackId, result: outputBuffer }, [outputBuffer.buffer]);
486
+ } catch (err) {
487
+ handleError(err);
488
+ }
489
+ return;
490
+ }
491
+
492
+ if (verb === 'wllama.exit') {
493
+ try {
494
+ const result = await wllamaExit();
495
+ msg({ callbackId, result });
496
+ } catch (err) {
497
+ handleError(err);
498
+ }
499
+ return;
500
+ }
501
+
502
+ if (verb === 'wllama.debug') {
503
+ try {
504
+ const result = await wllamaDebug();
505
+ msg({ callbackId, result });
506
+ } catch (err) {
507
+ handleError(err);
508
+ }
509
+ return;
510
+ }
511
+ };
@@ -0,0 +1,150 @@
1
+ let accessHandle;
2
+ let abortController = new AbortController();
3
+
4
+ async function openFile(filename) {
5
+ const opfsRoot = await navigator.storage.getDirectory();
6
+ const cacheDir = await opfsRoot.getDirectoryHandle('cache', { create: true });
7
+ const fileHandler = await cacheDir.getFileHandle(filename, { create: true });
8
+ accessHandle = await fileHandler.createSyncAccessHandle();
9
+ accessHandle.truncate(0); // clear file content
10
+ }
11
+
12
+ async function writeFile(buf) {
13
+ accessHandle.write(buf);
14
+ }
15
+
16
+ async function closeFile() {
17
+ accessHandle.flush();
18
+ accessHandle.close();
19
+ }
20
+
21
+ async function writeTextFile(filename, str) {
22
+ await openFile(filename);
23
+ await writeFile(new TextEncoder().encode(str));
24
+ await closeFile();
25
+ }
26
+
27
+ const throttled = (func, delay) => {
28
+ let lastRun = 0;
29
+ return (...args) => {
30
+ const now = Date.now();
31
+ if (now - lastRun > delay) {
32
+ lastRun = now;
33
+ func.apply(null, args);
34
+ }
35
+ };
36
+ };
37
+
38
+ const assertNonNull = (val) => {
39
+ if (val === null || val === undefined) {
40
+ throw new Error('OPFS Worker: Assertion failed');
41
+ }
42
+ };
43
+
44
+ // respond to main thread
45
+ const resOK = () => postMessage({ ok: true });
46
+ const resProgress = (loaded, total) =>
47
+ postMessage({ progress: { loaded, total } });
48
+ const resErr = (err) => postMessage({ err });
49
+
50
+ onmessage = async (e) => {
51
+ try {
52
+ if (!e.data) return;
53
+
54
+ /**
55
+ * @param {Object} e.data
56
+ *
57
+ * Fine-control FS actions:
58
+ * - { action: 'open', filename: 'string' }
59
+ * - { action: 'write', buf: ArrayBuffer }
60
+ * - { action: 'close' }
61
+ *
62
+ * Simple write API:
63
+ * - { action: 'write-simple', filename: 'string', buf: ArrayBuffer }
64
+ *
65
+ * Download API:
66
+ * - { action: 'download', url: 'string', filename: 'string', options: Object, metadataFileName: 'string' }
67
+ * - { action: 'download-abort' }
68
+ */
69
+ const {
70
+ action,
71
+ filename,
72
+ buf,
73
+ url,
74
+ options,
75
+ metadataFileName,
76
+ metadataAdditional,
77
+ } = e.data;
78
+
79
+ if (action === 'open') {
80
+ assertNonNull(filename);
81
+ await openFile(filename);
82
+ return resOK();
83
+ } else if (action === 'write') {
84
+ assertNonNull(buf);
85
+ await writeFile(buf);
86
+ return resOK();
87
+ } else if (action === 'close') {
88
+ await closeFile();
89
+ return resOK();
90
+ } else if (action === 'write-simple') {
91
+ assertNonNull(filename);
92
+ assertNonNull(buf);
93
+ await openFile(filename);
94
+ await writeFile(buf);
95
+ await closeFile();
96
+ return resOK();
97
+ } else if (action === 'download') {
98
+ assertNonNull(url);
99
+ assertNonNull(filename);
100
+ assertNonNull(metadataFileName);
101
+ assertNonNull(options);
102
+ assertNonNull(options.aborted);
103
+ abortController = new AbortController();
104
+ if (options.aborted) abortController.abort();
105
+ const response = await fetch(url, {
106
+ ...options,
107
+ signal: abortController.signal,
108
+ });
109
+ const contentLength = response.headers.get('content-length');
110
+ const etag = (response.headers.get('etag') || '').replace(
111
+ /[^A-Za-z0-9]/g,
112
+ ''
113
+ );
114
+ const total = parseInt(contentLength, 10);
115
+ const reader = response.body.getReader();
116
+ await openFile(filename);
117
+ let loaded = 0;
118
+ const throttledProgress = throttled(resProgress, 100);
119
+ while (true) {
120
+ const { done, value } = await reader.read();
121
+ if (done) break;
122
+ loaded += value.byteLength;
123
+ await writeFile(value);
124
+ throttledProgress(loaded, total);
125
+ }
126
+ resProgress(total, total); // 100% done
127
+ await closeFile();
128
+ // make sure this is in-sync with CacheEntryMetadata
129
+ await writeTextFile(
130
+ metadataFileName,
131
+ JSON.stringify({
132
+ originalURL: url,
133
+ originalSize: total,
134
+ etag,
135
+ ...metadataAdditional,
136
+ })
137
+ );
138
+ return resOK();
139
+ } else if (action === 'download-abort') {
140
+ if (abortController) {
141
+ abortController.abort();
142
+ }
143
+ return;
144
+ }
145
+
146
+ throw new Error('OPFS Worker: Invalid action', e.data);
147
+ } catch (err) {
148
+ return resErr(err);
149
+ }
150
+ };
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "useDefineForClassFields": true,
4
+ "target": "ESNext",
5
+ "lib": ["esnext", "dom"],
6
+ "allowJs": true,
7
+ "moduleResolution": "node",
8
+ "strict": true,
9
+ "noImplicitAny": true,
10
+ "noImplicitReturns": true,
11
+ "noImplicitThis": true,
12
+ "strictNullChecks": true,
13
+ "strictFunctionTypes": true,
14
+ "skipLibCheck": true,
15
+ "preserveConstEnums": true,
16
+ "sourceMap": true,
17
+ "declaration": true,
18
+ "downlevelIteration": true,
19
+ "resolveJsonModule": true,
20
+ "experimentalDecorators": true,
21
+ "exactOptionalPropertyTypes": true,
22
+ "noImplicitOverride": true,
23
+ "verbatimModuleSyntax": true,
24
+
25
+ "noEmit": false,
26
+ "module": "es2015",
27
+ "outDir": "esm",
28
+ "rootDir": "./src",
29
+ "baseUrl": ".",
30
+ "allowSyntheticDefaultImports": true
31
+ },
32
+ "include": [ "./src/**/*.ts" ],
33
+ "exclude": [ "./src/**/*.test.ts" ]
34
+ }