@volter/editor-blender 0.1.16 → 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.
@@ -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: 4 };
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(tmpdir(), "volter-blender-transfer");
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 (_req, res) => {
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 found = await blenderWasmOnDisk(status.dir, file);
380
- if (!found) {
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 mkdir(dirname2(path), { recursive: true });
479
+ await mkdir2(dirname2(path), { recursive: true });
419
480
  const body = await readBody(req);
420
- await writeFile(path, new Uint8Array(body));
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.16",
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.14",
73
- "@volter/editor-threejs": "0.5.76",
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.76",
78
- "@volter/editor-live": "0.5.76",
79
- "@volter/editor-project": "0.5.76",
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 (_req, res) => {
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 found = await blenderWasmOnDisk(status.dir!, file);
169
- if (!found) {
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
- export const BLENDER_WALI_WORKERS = { pool: 16, blender: 4 } as const;
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
+ }