@volter/blender-engine 0.1.62 → 0.1.63

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.
@@ -27,6 +27,7 @@
27
27
  * `session.py` chmods its own root as its first act.
28
28
  */
29
29
 
30
+ import { checkpointStream } from './pull-job.mts';
30
31
  import { writeStreamedFile, type StreamFileSystem } from './stream-file.mts';
31
32
  import {
32
33
  artifactUrl,
@@ -149,14 +150,17 @@ function moduleFiles(module: BlenderModule): BlenderFiles {
149
150
  */
150
151
  const ARTIFACT_CACHE = 'volter-blender-artifacts';
151
152
 
152
- async function cachedArtifact(file: string, digest: string | undefined): Promise<Response> {
153
+ async function cachedArtifact(file: string, digest: string | undefined, checkpoint: () => Promise<void>): Promise<Response> {
154
+ const bounded = (response: Response): Response => response.ok && response.body
155
+ ? new Response(checkpointStream(response.body, checkpoint), { status: response.status, headers: response.headers })
156
+ : response;
153
157
  const url = artifactUrl(file);
154
- if (!digest || typeof caches === 'undefined') return fetch(url);
158
+ if (!digest || typeof caches === 'undefined') return bounded(await fetch(url));
155
159
  const key = `${url}?sha256=${digest}`;
156
160
  const cache = await caches.open(ARTIFACT_CACHE);
157
161
  const hit = await cache.match(key);
158
- if (hit) return hit;
159
- const response = await fetch(url);
162
+ if (hit) return bounded(hit);
163
+ const response = bounded(await fetch(url));
160
164
  if (!response.ok) return response;
161
165
  // One build's bytes per file: an older build's are dropped as this one lands.
162
166
  for (const old of await cache.keys()) if (old.url.startsWith(`${url}?sha256=`) && old.url !== key) await cache.delete(old);
@@ -177,15 +181,18 @@ export async function startEmscriptenBlenderEngine(
177
181
  );
178
182
  const glueUrl = artifactUrl('blender_browser.js');
179
183
  const digests = status.digests ?? {};
184
+ const checkpoint = async (phase: string): Promise<void> => { await options.ask({ checkpoint: phase }); };
185
+ const artifact = (file: string) => cachedArtifact(file, digests[file], () => checkpoint(`artifact/${file}`));
180
186
  // The `.data` package is handed to the glue whole (`getPreloadedPackage`),
181
187
  // so it is read before the module starts; the wasm streams in beside it.
182
188
  const [factory, preloaded] = await Promise.all([
183
189
  loadFactory(glueUrl),
184
- cachedArtifact('blender_browser.data', digests['blender_browser.data']).then(async (response) => {
190
+ artifact('blender_browser.data').then(async (response) => {
185
191
  if (!response.ok) throw new Error(`blender_browser.data: HTTP ${response.status}`);
186
192
  return response.arrayBuffer();
187
193
  }),
188
194
  ]);
195
+ await checkpoint('runtime-package-ready');
189
196
  let bootError: unknown = null;
190
197
  const started = performance.now();
191
198
  let readyLine: string | null = null;
@@ -221,9 +228,10 @@ export async function startEmscriptenBlenderEngine(
221
228
  getPreloadedPackage: () => preloaded,
222
229
  instantiateWasm: (imports: WebAssembly.Imports, receive: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void) => {
223
230
  void (async () => {
224
- const response = await cachedArtifact('blender_browser.wasm', digests['blender_browser.wasm']);
231
+ const response = await artifact('blender_browser.wasm');
225
232
  if (!response.ok) throw new Error(`blender_browser.wasm: HTTP ${response.status}`);
226
233
  const { instance, module: compiled } = await WebAssembly.instantiateStreaming(response, imports);
234
+ await checkpoint('wasm-instantiated');
227
235
  receive(instance, compiled);
228
236
  })().catch((error: unknown) => { bootError = error; });
229
237
  return {};
@@ -273,6 +281,7 @@ export async function startEmscriptenBlenderEngine(
273
281
  }).finally(() => {
274
282
  (WebAssembly as { Memory: unknown }).Memory = Memory;
275
283
  });
284
+ await checkpoint('runtime-initialized');
276
285
  // `main()` runs on a pthread (`-sPROXY_TO_PTHREAD`), so the factory resolves
277
286
  // long before the session exists. The ready line is what says it does.
278
287
  while (readyLine === null) {
@@ -337,7 +346,8 @@ export async function startEmscriptenBlenderEngine(
337
346
 
338
347
  const files = moduleFiles(module);
339
348
  FS.chmod('/bw/datafiles', 0o755);
340
- await mountEssentials(files, digests['essentials.bin']);
349
+ await checkpoint('python-ready');
350
+ await mountEssentials(files, digests['essentials.bin'], checkpoint);
341
351
  const { request } = openSessionChannel(files, options);
342
352
 
343
353
  return {
@@ -364,9 +374,9 @@ export async function startEmscriptenBlenderEngine(
364
374
  /** Assets are data, not a second engine. Ship them separately so a data update
365
375
  * does not relink the 86 MB Wasm binary. Both payload and per-file bounds are
366
376
  * checked before anything enters Blender's filesystem. */
367
- async function mountEssentials(files: BlenderFiles, digest: string | undefined): Promise<void> {
377
+ async function mountEssentials(files: BlenderFiles, digest: string | undefined, checkpoint: (phase: string) => Promise<void>): Promise<void> {
368
378
  const [indexResponse, payloadResponse] = await Promise.all([
369
- fetch(artifactUrl('essentials.json')), cachedArtifact('essentials.bin', digest),
379
+ fetch(artifactUrl('essentials.json')), cachedArtifact('essentials.bin', digest, () => checkpoint('artifact/essentials.bin')),
370
380
  ]);
371
381
  if (!indexResponse.ok || !payloadResponse.ok)
372
382
  throw new Error(`Blender Essentials assets are missing (${indexResponse.status}/${payloadResponse.status})`);
@@ -380,6 +390,7 @@ async function mountEssentials(files: BlenderFiles, digest: string | undefined):
380
390
  if (payload.byteLength !== index.bytes || hash !== index.sha256)
381
391
  throw new Error('Blender Essentials payload does not match its source manifest');
382
392
  for (const file of index.files) {
393
+ await checkpoint('essentials-file');
383
394
  if (!file.path || file.path.includes('\\') || file.path.split('/').some(p => !p || p === '.' || p === '..') ||
384
395
  !Number.isInteger(file.offset) || !Number.isInteger(file.bytes) ||
385
396
  file.offset < 0 || file.bytes < 0 || file.offset + file.bytes > payload.byteLength)
@@ -258,6 +258,7 @@ export async function artifactStatus(): Promise<BlenderArtifactStatus> {
258
258
  */
259
259
  export async function startBlenderEngine(options: BlenderEngineOptions): Promise<BlenderEngine> {
260
260
  const status = await artifactStatus();
261
+ await options.ask({ checkpoint: 'artifact-status' });
261
262
  if (!status.available)
262
263
  throw new Error(
263
264
  `The headless Blender WebAssembly build is not served by this editor: ${status.missing.join('; ')}`,
@@ -219,6 +219,8 @@ export class BlenderRuntime {
219
219
  for (const id of [...this.#pending.keys()]) this.#settled(id);
220
220
  for (const pending of this.#pending.values()) pending.reject(error);
221
221
  this.#pending.clear();
222
+ // Queued logical operations must reject too; this worker cannot answer them.
223
+ this.terminate();
222
224
  };
223
225
  }
224
226
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@volter/blender-engine",
3
3
  "author": "Volter AI, Inc.",
4
4
  "license": "GPL-3.0-or-later",
5
- "version": "0.1.62",
5
+ "version": "0.1.63",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },