Skip to content

Commit f6aac9b

Browse files
fix: worker yielding (#902)
1 parent dfae6e3 commit f6aac9b

1 file changed

Lines changed: 32 additions & 38 deletions

File tree

src/frontend/src/lib/functions/streams.ts

Lines changed: 32 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ import { WORKER_CONCURRENCY } from '#consts/concurrency';
22
import { HKDF_IV_STR, HKDF_SALT_STR } from '#consts/encryption';
33
import DecryptWorker from '#workers/decrypt.worker?worker';
44
import EncryptWorker from '#workers/encrypt.worker?worker';
5-
65
import { ZipWriter } from '@zip.js/zip.js';
7-
86
import {
97
CHUNK_SIZE,
108
argon2Derive,
@@ -16,17 +14,14 @@ import {
1614
} from './encryption';
1715

1816
const usedNames = new Map<string, number>();
19-
2017
const makeUnique = (name: string) => {
2118
if (!usedNames.has(name)) {
2219
usedNames.set(name, 1);
2320
return name;
2421
}
25-
2622
const count = usedNames.get(name) || 1;
2723
usedNames.set(name, count + 1);
2824

29-
// Preserve extension when adding suffix
3025
const lastDot = name.lastIndexOf('.');
3126
if (lastDot > 0) {
3227
const base = name.slice(0, lastDot);
@@ -37,24 +32,20 @@ const makeUnique = (name: string) => {
3732
};
3833

3934
async function deriveSecrets(ikm: Uint8Array, password?: string) {
40-
// Derive deterministic salt from IKM
4135
const enc = new TextEncoder();
4236
const derivedSalt = await crypto.subtle.digest(
4337
'SHA-256',
4438
new Uint8Array([...ikm, ...enc.encode(HKDF_SALT_STR)])
4539
);
46-
4740
let finalIKM = ikm;
4841

49-
// Mix in password if provided
5042
if (password && password.length > 0) {
5143
const saltBytes = new Uint8Array(derivedSalt).slice(0, 16);
5244
const passwordBytes = new TextEncoder().encode(password);
5345
const pb = await argon2Derive(passwordBytes, saltBytes, 32, 16384, 32, 1);
5446
finalIKM = xorBytes(ikm, pb);
5547
}
5648

57-
// Derive AES key and base IV
5849
const hkdfSalt = new Uint8Array(
5950
await crypto.subtle.digest('SHA-256', new Uint8Array([...finalIKM, ...enc.encode('aes-key')]))
6051
).slice(0, 16);
@@ -102,7 +93,6 @@ async function handleWorkerEncryptedMessage(ctx: EncryptionContext, data: any):
10293
ctx.processedTotal += sz;
10394
if (ctx.onProgress) ctx.onProgress(ctx.processedTotal, ctx.originalSize);
10495
}
105-
10696
if (ctx.streamEnded && ctx.pendingCount === 0) {
10797
if (ctx.allDoneResolve) ctx.allDoneResolve();
10898
if (ctx.onProgress) ctx.onProgress(ctx.originalSize ?? ctx.processedTotal, ctx.originalSize);
@@ -120,14 +110,28 @@ async function initializeEncryptionWorkers(
120110
): Promise<void> {
121111
try {
122112
const keyRaw = await crypto.subtle.exportKey('raw', aesKey);
113+
const readyPromises: Promise<void>[] = [];
114+
123115
for (let i = 0; i < concurrency; i++) {
124116
const w = new EncryptWorker();
125-
w.onmessage = (ev) => handleWorkerEncryptedMessage(ctx, ev.data);
117+
const readyPromise = new Promise<void>((resolve, reject) => {
118+
w.onmessage = (ev) => {
119+
if (ev.data?.type === 'ready') {
120+
resolve();
121+
} else {
122+
handleWorkerEncryptedMessage(ctx, ev.data);
123+
}
124+
};
125+
w.onerror = () => reject(new Error('Worker failed during initialization'));
126+
});
127+
readyPromises.push(readyPromise);
128+
126129
ctx.workers.push(w);
127130
const keyCopy = keyRaw.slice(0);
128131
const ivCopy = baseIv.buffer.slice(0);
129132
w.postMessage({ type: 'init', keyRaw: keyCopy, baseIv: ivCopy }, [keyCopy, ivCopy]);
130133
}
134+
await Promise.all(readyPromises);
131135
} catch (e) {
132136
ctx.workers.length = 0;
133137
await handleEncryptionError(ctx, e);
@@ -231,7 +235,6 @@ async function handleWorkerDecryptedMessage(ctx: DecryptionContext, data: any):
231235
ctx.decryptedMap.delete(ctx.nextToEnqueue);
232236
ctx.controllerRef!.enqueue(arr);
233237
ctx.nextToEnqueue++;
234-
235238
ctx.processedTotal += arr.byteLength;
236239
if (ctx.onProgress) ctx.onProgress(ctx.processedTotal, ctx.originalSize);
237240
}
@@ -254,14 +257,28 @@ async function initializeDecryptionWorkers(
254257
): Promise<void> {
255258
try {
256259
const keyRaw = await crypto.subtle.exportKey('raw', aesKey);
260+
const readyPromises: Promise<void>[] = [];
261+
257262
for (let i = 0; i < concurrency; i++) {
258263
const w = new DecryptWorker();
259-
w.onmessage = (ev) => handleWorkerDecryptedMessage(ctx, ev.data);
264+
const readyPromise = new Promise<void>((resolve, reject) => {
265+
w.onmessage = (ev) => {
266+
if (ev.data?.type === 'ready') {
267+
resolve();
268+
} else {
269+
handleWorkerDecryptedMessage(ctx, ev.data);
270+
}
271+
};
272+
w.onerror = () => reject(new Error('Worker failed during initialization'));
273+
});
274+
readyPromises.push(readyPromise);
275+
260276
ctx.workers.push(w);
261277
const keyCopy = keyRaw.slice(0);
262278
const ivCopy = baseIv.buffer.slice(0);
263279
w.postMessage({ type: 'init', keyRaw: keyCopy, baseIv: ivCopy }, [keyCopy, ivCopy]);
264280
}
281+
await Promise.all(readyPromises);
265282
} catch (e) {
266283
ctx.workers.length = 0;
267284
await handleDecryptionError(ctx, e);
@@ -303,7 +320,6 @@ async function decryptChunkFallback(
303320
ctx.decryptedMap.delete(ctx.nextToEnqueue);
304321
ctx.controllerRef.enqueue(arr);
305322
ctx.nextToEnqueue++;
306-
307323
ctx.processedTotal += arr.byteLength;
308324
if (ctx.onProgress) ctx.onProgress(ctx.processedTotal, ctx.originalSize);
309325
}
@@ -342,17 +358,14 @@ async function writeZipFiles(
342358
for (const file of files) {
343359
let filename = (file as any).relativePath || file.name;
344360
filename = makeUnique(filename);
345-
346361
try {
347362
await zipWriter.add(filename, file.stream(), {
348363
password: password?.length ? password : undefined,
349-
// prefer strongest WinZip AES (1..3 => 128,192,256). Use 3 for AES-256 compatibility.
350364
encryptionStrength: password?.length ? 3 : undefined,
351365
level: 9,
352366
signal
353367
});
354368
} catch (err: any) {
355-
// If the writer reports an existing file, try to generate another unique name and retry once
356369
const msg = String(err?.message || err || '');
357370
if (msg.includes('File already exists') || msg.includes('already exists')) {
358371
const altName = makeUnique((file as any).relativePath || file.name);
@@ -367,7 +380,6 @@ async function writeZipFiles(
367380
}
368381
}
369382
}
370-
371383
await zipWriter.close();
372384
} catch (error) {
373385
console.error('Error creating zip stream:', error);
@@ -389,11 +401,10 @@ export async function createZipStream(
389401
bufferedWrite: true,
390402
useCompressionStream: true
391403
});
392-
393404
writeZipFiles(zipWriter, writable, files, password, signal);
394-
395405
return readable;
396406
}
407+
397408
export async function createEncryptedStream(
398409
inputStream: ReadableStream<Uint8Array>,
399410
password?: string,
@@ -403,7 +414,6 @@ export async function createEncryptedStream(
403414
) {
404415
const ikm = ikm_override ?? crypto.getRandomValues(new Uint8Array(32));
405416
const { aesKey, baseIv } = await deriveSecrets(ikm, password);
406-
407417
const chunks: Uint8Array[] = [];
408418
let bufferedBytes = 0;
409419
let chunkIndex = 0;
@@ -432,31 +442,26 @@ export async function createEncryptedStream(
432442
while (offset < size) {
433443
const first = chunks[0];
434444
const take = Math.min(first.length, size - offset);
435-
436445
out.set(first.subarray(0, take), offset);
437446

438447
if (take === first.length) {
439448
chunks.shift();
440449
} else {
441450
chunks[0] = first.subarray(take);
442451
}
443-
444452
offset += take;
445453
bufferedBytes -= take;
446454
}
447-
448455
return out;
449456
}
450457

451458
const transformer = new TransformStream<Uint8Array, Uint8Array>({
452459
async start(controller) {
453460
ctx.controllerRef = controller;
454-
455461
allDonePromise = new Promise<void>((res, rej) => {
456462
ctx.allDoneResolve = res;
457463
ctx.allDoneReject = rej;
458464
});
459-
460465
await initializeEncryptionWorkers(ctx, aesKey, baseIv, WORKER_CONCURRENCY);
461466
},
462467

@@ -467,9 +472,7 @@ export async function createEncryptedStream(
467472
while (bufferedBytes >= CHUNK_SIZE) {
468473
const chunkData = readChunk(CHUNK_SIZE);
469474
const index = chunkIndex++;
470-
471475
ctx.chunkSizes.set(index, chunkData.byteLength);
472-
473476
await assignEncryptionChunk(ctx, index, chunkData, aesKey, baseIv);
474477
}
475478
},
@@ -478,18 +481,13 @@ export async function createEncryptedStream(
478481
if (bufferedBytes > 0 || chunkIndex === 0) {
479482
const chunkData = readChunk(bufferedBytes);
480483
const index = chunkIndex++;
481-
482484
ctx.chunkSizes.set(index, chunkData.byteLength);
483-
484485
await assignEncryptionChunk(ctx, index, chunkData, aesKey, baseIv);
485486
}
486-
487487
ctx.streamEnded = true;
488-
489488
if (ctx.pendingCount > 0) {
490489
await allDonePromise;
491490
}
492-
493491
try {
494492
for (const w of ctx.workers) w.terminate();
495493
} catch {}
@@ -511,7 +509,6 @@ export async function createDecryptedStream(
511509
) {
512510
const ikm = base64urlToBytes(keySecret);
513511
const { aesKey, baseIv } = await deriveSecrets(ikm, password);
514-
515512
const reader = inputStream.getReader();
516513
let buffer = new Uint8Array(0);
517514

@@ -584,7 +581,6 @@ export async function createDecryptedStream(
584581
ctx.decryptedMap.delete(ctx.nextToEnqueue);
585582
controller.enqueue(arr);
586583
ctx.nextToEnqueue++;
587-
588584
ctx.processedTotal += arr.byteLength;
589585
if (ctx.onProgress) ctx.onProgress(ctx.processedTotal, ctx.originalSize);
590586
}
@@ -619,8 +615,6 @@ export function createMultipartStream(
619615
fileStream: ReadableStream<Uint8Array>
620616
): ReadableStream<Uint8Array> {
621617
const encoder = new TextEncoder();
622-
623-
// Construct preamble
624618
const preAmbleParts: Uint8Array[] = [];
625619
for (const [key, value] of Object.entries(fields)) {
626620
preAmbleParts.push(encoder.encode(`--${boundary}\r\n`));
@@ -648,7 +642,7 @@ export function createMultipartStream(
648642
if (preambleIndex < preAmbleParts.length) {
649643
controller.enqueue(preAmbleParts[preambleIndex]);
650644
preambleIndex++;
651-
return; // Yield to event loop
645+
return;
652646
} else {
653647
state = 'file';
654648
fileReader = fileStream.getReader();

0 commit comments

Comments
 (0)