@volter/editor-blender 0.1.17 → 0.1.18
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.
- package/dist-node/serving.mjs +73 -12
- package/package.json +6 -6
- package/serving/blender-routes.ts +29 -3
- package/serving/blender-wasm-artifact.ts +79 -4
package/dist-node/serving.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// serving/blender-routes.ts
|
|
2
2
|
import { createReadStream as createReadStream2 } from "node:fs";
|
|
3
|
-
import { mkdir, readFile as readFile2, rm, stat as stat2, unlink, writeFile } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
3
|
+
import { mkdir as mkdir2, readFile as readFile2, rm, stat as stat2, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
4
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
5
5
|
import { dirname as dirname2, join as join2 } from "node:path";
|
|
6
6
|
import { createBrotliDecompress } from "node:zlib";
|
|
7
7
|
|
|
@@ -40,10 +40,11 @@ function isContainedRelativePath(path) {
|
|
|
40
40
|
// serving/blender-wasm-artifact.ts
|
|
41
41
|
import { createHash } from "node:crypto";
|
|
42
42
|
import { createReadStream, existsSync } from "node:fs";
|
|
43
|
-
import { readdir, readFile, stat } from "node:fs/promises";
|
|
43
|
+
import { mkdir, readdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
44
|
+
import { tmpdir } from "node:os";
|
|
44
45
|
import { dirname, isAbsolute, join, posix, relative } from "node:path";
|
|
45
46
|
import { fileURLToPath } from "node:url";
|
|
46
|
-
import { brotliDecompressSync } from "node:zlib";
|
|
47
|
+
import { brotliCompress, brotliDecompressSync, constants as zlibConstants } from "node:zlib";
|
|
47
48
|
var BLENDER_WASM_FILES = [
|
|
48
49
|
"blender_browser.js",
|
|
49
50
|
"blender_browser.wasm",
|
|
@@ -53,7 +54,7 @@ var BLENDER_WASM_FILES = [
|
|
|
53
54
|
];
|
|
54
55
|
var BLENDER_WALI_ARTIFACT = "blender.wasm";
|
|
55
56
|
var BLENDER_WALI_RUNTIME = "runtime";
|
|
56
|
-
var BLENDER_WALI_WORKERS = { pool: 16, blender:
|
|
57
|
+
var BLENDER_WALI_WORKERS = { pool: 16, blender: 1 };
|
|
57
58
|
var BLENDER_WALI_SUBSTRATE = [
|
|
58
59
|
{ segment: "browser-wali", specifier: "@volter/browser-wali" },
|
|
59
60
|
{ segment: "browser-runtime", specifier: "@volter/browser-runtime" }
|
|
@@ -211,6 +212,7 @@ async function runtimeIndex(dir) {
|
|
|
211
212
|
if (cachedRuntimeIndex?.dir === dir) return cachedRuntimeIndex.index;
|
|
212
213
|
const root = join(dir, BLENDER_WALI_RUNTIME);
|
|
213
214
|
const files = [];
|
|
215
|
+
let newest = 0;
|
|
214
216
|
const walk = async (where) => {
|
|
215
217
|
for (const entry of (await readdir(where, { withFileTypes: true })).sort(
|
|
216
218
|
(left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0
|
|
@@ -227,14 +229,19 @@ async function runtimeIndex(dir) {
|
|
|
227
229
|
size: info.size,
|
|
228
230
|
mode: info.mode & 4095
|
|
229
231
|
});
|
|
232
|
+
newest = Math.max(newest, info.mtimeMs);
|
|
230
233
|
}
|
|
231
234
|
};
|
|
232
235
|
await walk(root);
|
|
233
236
|
if (files.length === 0) throw new Error(`${root} is empty`);
|
|
234
|
-
const index = { files };
|
|
237
|
+
const index = { files, newestMtimeMs: newest };
|
|
235
238
|
cachedRuntimeIndex = { dir, index };
|
|
236
239
|
return index;
|
|
237
240
|
}
|
|
241
|
+
function runtimeTag(index) {
|
|
242
|
+
const total = index.files.reduce((sum, file) => sum + file.size, 0);
|
|
243
|
+
return `"runtime-${index.files.length}-${total}-${Math.floor(index.newestMtimeMs ?? 0)}"`;
|
|
244
|
+
}
|
|
238
245
|
async function writeRuntimeBlob(dir, write) {
|
|
239
246
|
const root = join(dir, BLENDER_WALI_RUNTIME);
|
|
240
247
|
for (const file of (await runtimeIndex(dir)).files) write(await readFile(join(root, file.path)));
|
|
@@ -264,6 +271,41 @@ async function substrateModule(segment, file) {
|
|
|
264
271
|
);
|
|
265
272
|
return { text };
|
|
266
273
|
}
|
|
274
|
+
var compressing = /* @__PURE__ */ new Map();
|
|
275
|
+
async function brotliCopy(key, produce) {
|
|
276
|
+
const path = join(tmpdir(), "volter-blender-brotli", `${key.replace(/[^\w.-]/g, "")}.br`);
|
|
277
|
+
try {
|
|
278
|
+
if ((await stat(path)).isFile()) return path;
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
if (!compressing.has(path)) {
|
|
282
|
+
const making = (async () => {
|
|
283
|
+
const source = await produce();
|
|
284
|
+
const compressed = await new Promise((resolve, reject) => brotliCompress(source, {
|
|
285
|
+
params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 9, [zlibConstants.BROTLI_PARAM_SIZE_HINT]: source.byteLength }
|
|
286
|
+
}, (error, result) => error ? reject(error) : resolve(result)));
|
|
287
|
+
await mkdir(dirname(path), { recursive: true });
|
|
288
|
+
const part = `${path}.${process.pid}.part`;
|
|
289
|
+
await writeFile(part, compressed);
|
|
290
|
+
await rename(part, path);
|
|
291
|
+
})().catch(() => void 0).finally(() => compressing.delete(path));
|
|
292
|
+
compressing.set(path, making);
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
async function runtimeBlobBrotli(dir, index) {
|
|
297
|
+
return brotliCopy(`runtime-${runtimeTag(index)}`, async () => {
|
|
298
|
+
const chunks = [];
|
|
299
|
+
await writeRuntimeBlob(dir, (chunk) => void chunks.push(chunk));
|
|
300
|
+
return Buffer.concat(chunks);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async function brotliArtifact(found) {
|
|
304
|
+
if (found.encoding !== null) return found;
|
|
305
|
+
const info = await stat(found.path);
|
|
306
|
+
const path = await brotliCopy(`artifact-${info.size}-${Math.floor(info.mtimeMs)}`, () => readFile(found.path));
|
|
307
|
+
return path ? { path, size: (await stat(path)).size, encoding: "br" } : found;
|
|
308
|
+
}
|
|
267
309
|
|
|
268
310
|
// serving/blender-routes.ts
|
|
269
311
|
function json(res, body, status = 200) {
|
|
@@ -277,7 +319,7 @@ async function readBody(req) {
|
|
|
277
319
|
return Buffer.concat(chunks);
|
|
278
320
|
}
|
|
279
321
|
function blenderRoutesPlugin(services) {
|
|
280
|
-
const transferRoot = join2(
|
|
322
|
+
const transferRoot = join2(tmpdir2(), "volter-blender-transfer");
|
|
281
323
|
const transferPath = (id) => join2(transferRoot, id);
|
|
282
324
|
void rm(transferRoot, { recursive: true, force: true });
|
|
283
325
|
const transferId = (value) => value !== null && /^[0-9a-f-]{36}$/.test(value) ? value : null;
|
|
@@ -320,13 +362,31 @@ function blenderRoutesPlugin(services) {
|
|
|
320
362
|
{
|
|
321
363
|
method: "GET",
|
|
322
364
|
match: /^\/__editor\/blender-wasm\/runtime\.bin$/,
|
|
323
|
-
handle: async (
|
|
365
|
+
handle: async (req, res) => {
|
|
324
366
|
const status = await blenderWasmStatus();
|
|
325
367
|
if (status.skew !== "wali" || status.dir === null) {
|
|
326
368
|
json(res, { error: "This editor does not serve the substrate skew of Blender." }, 404);
|
|
327
369
|
return;
|
|
328
370
|
}
|
|
371
|
+
const index = await runtimeIndex(status.dir);
|
|
372
|
+
const accepted = String(req.headers["accept-encoding"] ?? "").toLowerCase().includes("br");
|
|
373
|
+
const compressed = accepted ? await runtimeBlobBrotli(status.dir, index) : null;
|
|
374
|
+
const tag = runtimeTag(index).replace(/"$/, compressed ? '-br"' : '-identity"');
|
|
375
|
+
res.setHeader("cache-control", "no-cache");
|
|
376
|
+
res.setHeader("etag", tag);
|
|
377
|
+
res.setHeader("vary", "accept-encoding");
|
|
378
|
+
if (req.headers["if-none-match"] === tag) {
|
|
379
|
+
res.statusCode = 304;
|
|
380
|
+
res.end();
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
329
383
|
res.setHeader("content-type", "application/octet-stream");
|
|
384
|
+
if (compressed) {
|
|
385
|
+
res.setHeader("content-encoding", "br");
|
|
386
|
+
res.setHeader("content-length", String((await stat2(compressed)).size));
|
|
387
|
+
createReadStream2(compressed).pipe(res);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
330
390
|
await writeRuntimeBlob(status.dir, (chunk) => void res.write(chunk));
|
|
331
391
|
res.end();
|
|
332
392
|
}
|
|
@@ -376,11 +436,12 @@ function blenderRoutesPlugin(services) {
|
|
|
376
436
|
res.end(await blenderGlueText(status.dir));
|
|
377
437
|
return;
|
|
378
438
|
}
|
|
379
|
-
const
|
|
380
|
-
if (!
|
|
439
|
+
const onDisk = await blenderWasmOnDisk(status.dir, file);
|
|
440
|
+
if (!onDisk) {
|
|
381
441
|
json(res, { error: `${file} vanished from ${status.dir}.` }, 404);
|
|
382
442
|
return;
|
|
383
443
|
}
|
|
444
|
+
const found = file === BLENDER_WALI_ARTIFACT ? await brotliArtifact(onDisk) : onDisk;
|
|
384
445
|
const info = await stat2(found.path);
|
|
385
446
|
const accepted = String(req.headers["accept-encoding"] ?? "").toLowerCase();
|
|
386
447
|
const sendEncoded = found.encoding !== null && accepted.includes(found.encoding);
|
|
@@ -415,9 +476,9 @@ function blenderRoutesPlugin(services) {
|
|
|
415
476
|
return;
|
|
416
477
|
}
|
|
417
478
|
const path = transferPath(id);
|
|
418
|
-
await
|
|
479
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
419
480
|
const body = await readBody(req);
|
|
420
|
-
await
|
|
481
|
+
await writeFile2(path, new Uint8Array(body));
|
|
421
482
|
json(res, { bytes: body.byteLength });
|
|
422
483
|
}
|
|
423
484
|
},
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@volter/editor-blender",
|
|
3
3
|
"author": "Volter AI, Inc.",
|
|
4
4
|
"license": "AGPL-3.0-only AND GPL-3.0-or-later",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.18",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -69,14 +69,14 @@
|
|
|
69
69
|
},
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
72
|
-
"@volter/blender-engine": "0.1.
|
|
73
|
-
"@volter/editor-threejs": "0.5.
|
|
72
|
+
"@volter/blender-engine": "0.1.16",
|
|
73
|
+
"@volter/editor-threejs": "0.5.78",
|
|
74
74
|
"zod": "^4.3.6"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@volter/editor-sdk": "0.5.
|
|
78
|
-
"@volter/editor-live": "0.5.
|
|
79
|
-
"@volter/editor-project": "0.5.
|
|
77
|
+
"@volter/editor-sdk": "0.5.78",
|
|
78
|
+
"@volter/editor-live": "0.5.78",
|
|
79
|
+
"@volter/editor-project": "0.5.78",
|
|
80
80
|
"react": "^19.0.0",
|
|
81
81
|
"three": "^0.180.0",
|
|
82
82
|
"vite": "^6.4.3"
|
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
blenderWasmReadStream,
|
|
28
28
|
blenderWasmStatus,
|
|
29
29
|
runtimeIndex,
|
|
30
|
+
runtimeTag,
|
|
31
|
+
runtimeBlobBrotli,
|
|
32
|
+
brotliArtifact,
|
|
30
33
|
substrateModule,
|
|
31
34
|
writeRuntimeBlob,
|
|
32
35
|
} from './blender-wasm-artifact';
|
|
@@ -104,13 +107,34 @@ export function blenderRoutesPlugin(services: ProjectServingServices): Plugin {
|
|
|
104
107
|
{
|
|
105
108
|
method: 'GET',
|
|
106
109
|
match: /^\/__editor\/blender-wasm\/runtime\.bin$/,
|
|
107
|
-
handle: async (
|
|
110
|
+
handle: async (req, res) => {
|
|
108
111
|
const status = await blenderWasmStatus();
|
|
109
112
|
if (status.skew !== 'wali' || status.dir === null) {
|
|
110
113
|
json(res, { error: 'This editor does not serve the substrate skew of Blender.' }, 404);
|
|
111
114
|
return;
|
|
112
115
|
}
|
|
116
|
+
// A validator, as the engine's own files have, and brotli once it is
|
|
117
|
+
// made, as the standalone skew's payload goes out: small enough for
|
|
118
|
+
// the HTTP cache, so a warm boot is a 304 (`runtimeBlobBrotli`).
|
|
119
|
+
const index = await runtimeIndex(status.dir);
|
|
120
|
+
const accepted = String(req.headers['accept-encoding'] ?? '').toLowerCase().includes('br');
|
|
121
|
+
const compressed = accepted ? await runtimeBlobBrotli(status.dir, index) : null;
|
|
122
|
+
const tag = runtimeTag(index).replace(/"$/, compressed ? '-br"' : '-identity"');
|
|
123
|
+
res.setHeader('cache-control', 'no-cache');
|
|
124
|
+
res.setHeader('etag', tag);
|
|
125
|
+
res.setHeader('vary', 'accept-encoding');
|
|
126
|
+
if (req.headers['if-none-match'] === tag) {
|
|
127
|
+
res.statusCode = 304;
|
|
128
|
+
res.end();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
113
131
|
res.setHeader('content-type', 'application/octet-stream');
|
|
132
|
+
if (compressed) {
|
|
133
|
+
res.setHeader('content-encoding', 'br');
|
|
134
|
+
res.setHeader('content-length', String((await stat(compressed)).size));
|
|
135
|
+
createReadStream(compressed).pipe(res);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
114
138
|
await writeRuntimeBlob(status.dir, (chunk) => void res.write(chunk));
|
|
115
139
|
res.end();
|
|
116
140
|
},
|
|
@@ -165,11 +189,13 @@ export function blenderRoutesPlugin(services: ProjectServingServices): Plugin {
|
|
|
165
189
|
res.end(await blenderGlueText(status.dir!));
|
|
166
190
|
return;
|
|
167
191
|
}
|
|
168
|
-
const
|
|
169
|
-
if (!
|
|
192
|
+
const onDisk = await blenderWasmOnDisk(status.dir!, file);
|
|
193
|
+
if (!onDisk) {
|
|
170
194
|
json(res, { error: `${file} vanished from ${status.dir}.` }, 404);
|
|
171
195
|
return;
|
|
172
196
|
}
|
|
197
|
+
// The substrate skew's module is served as brotli too, made once (`brotliArtifact`).
|
|
198
|
+
const found = file === BLENDER_WALI_ARTIFACT ? await brotliArtifact(onDisk) : onDisk;
|
|
173
199
|
// A VALIDATOR, SO THE COMPILED ENGINE IS CACHED. Chrome keeps the machine code it
|
|
174
200
|
// compiled from an `instantiateStreaming` response only while that response sits in its
|
|
175
201
|
// HTTP cache, and a response with no validator is never stored (measured: every boot
|
|
@@ -54,10 +54,11 @@
|
|
|
54
54
|
*/
|
|
55
55
|
import { createHash } from 'node:crypto';
|
|
56
56
|
import { createReadStream, existsSync } from 'node:fs';
|
|
57
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
57
|
+
import { mkdir, readdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
58
|
+
import { tmpdir } from 'node:os';
|
|
58
59
|
import { dirname, isAbsolute, join, posix, relative } from 'node:path';
|
|
59
60
|
import { fileURLToPath } from 'node:url';
|
|
60
|
-
import { brotliDecompressSync } from 'node:zlib';
|
|
61
|
+
import { brotliCompress, brotliDecompressSync, constants as zlibConstants } from 'node:zlib';
|
|
61
62
|
|
|
62
63
|
export type BlenderSkew = 'emscripten' | 'wali';
|
|
63
64
|
|
|
@@ -101,7 +102,17 @@ export const BLENDER_WALI_RUNTIME = 'runtime';
|
|
|
101
102
|
* clean (16 under 4; 8 under 4 trapped at boot), because Blender starts threads
|
|
102
103
|
* of its own beyond the oneTBB workers `-t` buys.
|
|
103
104
|
*/
|
|
104
|
-
|
|
105
|
+
/*
|
|
106
|
+
* ONE, MEASURED 2026-09-27, as the standalone skew runs Blender. Through the
|
|
107
|
+
* editor's own worker, on the same work as the standalone skew: at `-t 4` the
|
|
108
|
+
* session held 870 MB after it against the standalone skew's 632, and a
|
|
109
|
+
* boolean of two 8k-polygon spheres took 2.2 s; at `-t 1`, 629 MB and 1.3 s
|
|
110
|
+
* (the standalone skew 3.7 s). On this runtime more `-t` has cost more than
|
|
111
|
+
* it returned, as the courtyard's 12 against 4 already showed above. The
|
|
112
|
+
* pool stays 16: its workers start on a thread's first use, and Blender's
|
|
113
|
+
* libraries start threads of their own.
|
|
114
|
+
*/
|
|
115
|
+
export const BLENDER_WALI_WORKERS = { pool: 16, blender: 1 } as const;
|
|
105
116
|
|
|
106
117
|
/** The substrate packages the WALI engine imports, by the URL segment each is
|
|
107
118
|
* served under and the specifier prefix rewritten to it. */
|
|
@@ -345,6 +356,8 @@ export function blenderWasmReadStream(found: BlenderWasmOnDisk): NodeJS.Readable
|
|
|
345
356
|
|
|
346
357
|
export interface RuntimeIndex {
|
|
347
358
|
files: { path: string; size: number; mode: number }[];
|
|
359
|
+
/** The newest file's modification time: with the count and the total size, the blob's validator (`runtimeTag`). */
|
|
360
|
+
newestMtimeMs?: number;
|
|
348
361
|
}
|
|
349
362
|
|
|
350
363
|
let cachedRuntimeIndex: { dir: string; index: RuntimeIndex } | null = null;
|
|
@@ -363,6 +376,7 @@ export async function runtimeIndex(dir: string): Promise<RuntimeIndex> {
|
|
|
363
376
|
if (cachedRuntimeIndex?.dir === dir) return cachedRuntimeIndex.index;
|
|
364
377
|
const root = join(dir, BLENDER_WALI_RUNTIME);
|
|
365
378
|
const files: RuntimeIndex['files'] = [];
|
|
379
|
+
let newest = 0;
|
|
366
380
|
const walk = async (where: string): Promise<void> => {
|
|
367
381
|
for (const entry of (await readdir(where, { withFileTypes: true })).sort((left, right) =>
|
|
368
382
|
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
|
|
@@ -379,15 +393,27 @@ export async function runtimeIndex(dir: string): Promise<RuntimeIndex> {
|
|
|
379
393
|
size: info.size,
|
|
380
394
|
mode: info.mode & 0o7777,
|
|
381
395
|
});
|
|
396
|
+
newest = Math.max(newest, info.mtimeMs);
|
|
382
397
|
}
|
|
383
398
|
};
|
|
384
399
|
await walk(root);
|
|
385
400
|
if (files.length === 0) throw new Error(`${root} is empty`);
|
|
386
|
-
const index = { files };
|
|
401
|
+
const index = { files, newestMtimeMs: newest };
|
|
387
402
|
cachedRuntimeIndex = { dir, index };
|
|
388
403
|
return index;
|
|
389
404
|
}
|
|
390
405
|
|
|
406
|
+
/**
|
|
407
|
+
* THE RUNTIME BLOB'S VALIDATOR, so a warm boot takes it from the HTTP cache.
|
|
408
|
+
* Without one it was read from 1,317 files and sent again on every boot: the
|
|
409
|
+
* standalone skew's `.data` payload is one cached file, and this was most of
|
|
410
|
+
* what the substrate skew's boot spent before Blender ran (2026-09-27).
|
|
411
|
+
*/
|
|
412
|
+
export function runtimeTag(index: RuntimeIndex): string {
|
|
413
|
+
const total = index.files.reduce((sum, file) => sum + file.size, 0);
|
|
414
|
+
return `"runtime-${index.files.length}-${total}-${Math.floor(index.newestMtimeMs ?? 0)}"`;
|
|
415
|
+
}
|
|
416
|
+
|
|
391
417
|
/** Stream the runtime blob: every indexed file's bytes, in index order. */
|
|
392
418
|
export async function writeRuntimeBlob(dir: string, write: (chunk: Buffer) => void): Promise<void> {
|
|
393
419
|
const root = join(dir, BLENDER_WALI_RUNTIME);
|
|
@@ -447,3 +473,52 @@ export async function substrateModule(
|
|
|
447
473
|
);
|
|
448
474
|
return { text };
|
|
449
475
|
}
|
|
476
|
+
|
|
477
|
+
const compressing = new Map<string, Promise<void>>();
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* A file as brotli, kept under the system's temporary directory by `key`
|
|
481
|
+
* once made: its path, or null while it is being made (the first caller
|
|
482
|
+
* starts it and is answered uncompressed). The substrate skew's artifacts are
|
|
483
|
+
* past what Chrome keeps in its HTTP cache uncompressed -- `blender.wasm`
|
|
484
|
+
* 93 MB, the runtime blob 66 -- so every boot fetched them again and compiled
|
|
485
|
+
* the module again; the standalone skew's go out as brotli (18 MB and 7.7 MB)
|
|
486
|
+
* and are kept (2026-09-27: brotli, 20 MB and 25 MB).
|
|
487
|
+
*/
|
|
488
|
+
async function brotliCopy(key: string, produce: () => Promise<Buffer>): Promise<string | null> {
|
|
489
|
+
const path = join(tmpdir(), 'volter-blender-brotli', `${key.replace(/[^\w.-]/g, '')}.br`);
|
|
490
|
+
try {
|
|
491
|
+
if ((await stat(path)).isFile()) return path;
|
|
492
|
+
} catch { /* not made yet */ }
|
|
493
|
+
if (!compressing.has(path)) {
|
|
494
|
+
const making = (async () => {
|
|
495
|
+
const source = await produce();
|
|
496
|
+
const compressed = await new Promise<Buffer>((resolve, reject) => brotliCompress(source, {
|
|
497
|
+
params: { [zlibConstants.BROTLI_PARAM_QUALITY]: 9, [zlibConstants.BROTLI_PARAM_SIZE_HINT]: source.byteLength },
|
|
498
|
+
}, (error, result) => error ? reject(error) : resolve(result)));
|
|
499
|
+
await mkdir(dirname(path), { recursive: true });
|
|
500
|
+
const part = `${path}.${process.pid}.part`;
|
|
501
|
+
await writeFile(part, compressed);
|
|
502
|
+
await rename(part, path);
|
|
503
|
+
})().catch(() => undefined).finally(() => compressing.delete(path));
|
|
504
|
+
compressing.set(path, making);
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** The runtime blob as brotli (`brotliCopy`), by its validator. */
|
|
510
|
+
export async function runtimeBlobBrotli(dir: string, index: RuntimeIndex): Promise<string | null> {
|
|
511
|
+
return brotliCopy(`runtime-${runtimeTag(index)}`, async () => {
|
|
512
|
+
const chunks: Buffer[] = [];
|
|
513
|
+
await writeRuntimeBlob(dir, (chunk) => void chunks.push(chunk));
|
|
514
|
+
return Buffer.concat(chunks);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** An uncompressed artifact as brotli (`brotliCopy`), by its size and time: the file itself when none is made yet. */
|
|
519
|
+
export async function brotliArtifact(found: BlenderWasmOnDisk): Promise<BlenderWasmOnDisk> {
|
|
520
|
+
if (found.encoding !== null) return found;
|
|
521
|
+
const info = await stat(found.path);
|
|
522
|
+
const path = await brotliCopy(`artifact-${info.size}-${Math.floor(info.mtimeMs)}`, () => readFile(found.path));
|
|
523
|
+
return path ? { path, size: (await stat(path)).size, encoding: 'br' } : found;
|
|
524
|
+
}
|