@volter/blender-engine 0.1.137 → 0.1.139
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/browser/pull-job.mts +25 -4
- package/browser/runtime.ts +14 -2
- package/package.json +1 -1
package/browser/pull-job.mts
CHANGED
|
@@ -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
|
|
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
|
|
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();
|
package/browser/runtime.ts
CHANGED
|
@@ -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(
|