@volter/blender-engine 0.1.137 → 0.1.138

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.
@@ -10,8 +10,23 @@ export class PullJob<T> {
10
10
  private expected: string | null = null;
11
11
  private resume: (() => void) | null = null;
12
12
  private waiter: { resolve: (value: PullResult<T>) => void; reject: (error: unknown) => void } | null = null;
13
+ /**
14
+ * How the work ended, when it ended while nobody was waiting on a step. Work normally ends only
15
+ * while a step waits, but work that races something against a checkpoint (an engine compiling
16
+ * beside an import) can fail in the gap after a `continue` was handed out. The outcome is kept
17
+ * for the step that comes next, so the caller is told what actually happened. Before this, the
18
+ * failure was thrown at nobody and the next step answered "Invalid or replayed load
19
+ * continuation", which named the protocol instead of the fault (measured 2026-10-08: a Blender
20
+ * start refused by a content security policy showed only that message, on every retry).
21
+ */
22
+ private ended: { readonly value: T } | { readonly error: unknown } | null = null;
13
23
  constructor(private readonly work: (checkpoint: (phase: string) => Promise<void>) => Promise<T>) {}
14
24
  step(token?: string): Promise<PullResult<T>> {
25
+ if (this.ended && !this.waiter && token === this.expected) {
26
+ const ended = this.ended;
27
+ this.ended = null; this.expected = null; this.resume = null;
28
+ return 'error' in ended ? Promise.reject(ended.error) : Promise.resolve({ load: 'done', value: ended.value });
29
+ }
15
30
  if (this.finished || this.waiter || (this.started ? token !== this.expected || !this.resume : token !== undefined))
16
31
  return Promise.reject(new Error('Invalid or replayed load continuation'));
17
32
  const answer = new Promise<PullResult<T>>((resolve, reject) => { this.waiter = { resolve, reject }; });
@@ -24,12 +39,18 @@ export class PullJob<T> {
24
39
  waiter.resolve({ load: 'continue', token: this.expected, phase });
25
40
  }))).then(value => {
26
41
  this.finished = true;
27
- const waiter = this.waiter!; this.waiter = null;
28
- waiter.resolve({ load: 'done', value });
42
+ const waiter = this.waiter; this.waiter = null;
43
+ if (waiter) waiter.resolve({ load: 'done', value });
44
+ else this.ended = { value };
29
45
  }, error => {
30
46
  this.finished = true;
31
- const waiter = this.waiter!; this.waiter = null;
32
- waiter.reject(error);
47
+ const waiter = this.waiter; this.waiter = null;
48
+ if (waiter) waiter.reject(error);
49
+ else {
50
+ this.ended = { error };
51
+ // Said now as well: if the owner never asks for the next step, nobody would collect it.
52
+ console.warn('A Blender load failed between steps; the next step reports it:', error);
53
+ }
33
54
  });
34
55
  } else {
35
56
  const resume = this.resume!; this.resume = null; this.expected = null; resume();
@@ -273,14 +273,26 @@ export class BlenderRuntime {
273
273
  );
274
274
  this.#project = project;
275
275
  this.#document ??= document ?? 'models/model.blend';
276
- this.#started ??= this.#request({
276
+ this.#started ??= (this.#request({
277
277
  op: 'start',
278
278
  project,
279
279
  ...(document === undefined ? {} : { document }),
280
- }) as Promise<RuntimeStart>;
280
+ }) as Promise<RuntimeStart>).catch((error: unknown) => {
281
+ this.#startFailed = true;
282
+ throw error;
283
+ });
281
284
  return this.#started;
282
285
  }
283
286
 
287
+ /**
288
+ * True once THIS session's own start has failed (not a refusal to start, such as a file another
289
+ * document holds). A start is asked for once and its outcome kept, and the worker that failed may
290
+ * hold a half-started engine, so such a session never starts: its owner discards it and makes a
291
+ * new one, with a new worker, for the next attempt.
292
+ */
293
+ get startFailed(): boolean { return this.#startFailed; }
294
+ #startFailed = false;
295
+
284
296
  #ready(): Promise<RuntimeStart> {
285
297
  if (this.#project === null)
286
298
  return Promise.reject(
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.137",
5
+ "version": "0.1.138",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },