partforge 0.130.0 → 0.130.1
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/docs/KERNEL-CONTRACT.md
CHANGED
|
@@ -857,6 +857,16 @@ Manifold backend, thrown from `imports.js`'s registration policy, not `errors.js
|
|
|
857
857
|
`KernelCapabilityError`/`NEEDS_OCCT`) — is the same message shape and the same
|
|
858
858
|
non-epoch-gated risk as `needs-occt`; a host handling one should handle both the same way.
|
|
859
859
|
|
|
860
|
+
**Export replies cross a rebind by design.** Exports run to completion, so a host that
|
|
861
|
+
keeps one worker across mounts, and routes that worker's messages to whichever mount is
|
|
862
|
+
listening now, hands the next mount every reply to the previous mount's exports.
|
|
863
|
+
`mount()` handles this itself. Its export jobIds (`export-<c>-<n>`, and `warm-<c>-<n>`
|
|
864
|
+
for `warmExportKernel`) are unique to each mount, not numbered from 1 per mount. So a
|
|
865
|
+
previous mount's reply can never settle the new mount's export with the old geometry.
|
|
866
|
+
The new mount claims that reply and drops it, rather than letting its own handler read
|
|
867
|
+
the reply's `error` as a failed build or its `download` as a viewbar save. The previous
|
|
868
|
+
mount already rejected that export when it was disposed.
|
|
869
|
+
|
|
860
870
|
**Cancellation granularity is the sub-part.** The guard is checked only between
|
|
861
871
|
sub-parts (one macrotask yield each), so a single long WASM op — a big boolean, an OCCT
|
|
862
872
|
fillet — runs to completion no matter how stale it is. That is by design: WASM kernel
|
package/package.json
CHANGED
|
@@ -12,10 +12,11 @@ export function createCaptureBuild({ send }) {
|
|
|
12
12
|
// worker (a silent no-op) and its promise would hang forever. Resolve null instead
|
|
13
13
|
// — captureView's documented "disposed runtime resolves null" contract.
|
|
14
14
|
if (disposed) return Promise.resolve(null);
|
|
15
|
-
// String-namespaced ("cap-N") so a capture jobId
|
|
16
|
-
// export-controller's
|
|
17
|
-
// space, and exportCtl.handleMessage
|
|
18
|
-
// checking type, so a colliding id could otherwise settle
|
|
15
|
+
// String-namespaced ("cap-N") so a capture jobId stays outside
|
|
16
|
+
// export-controller's "export-"/"warm-" ids — both share the same worker
|
|
17
|
+
// message space, and exportCtl.handleMessage claims every id in its own
|
|
18
|
+
// namespace before checking type, so a colliding id could otherwise settle
|
|
19
|
+
// the wrong promise.
|
|
19
20
|
const jobId = `cap-${nextId++}`;
|
|
20
21
|
return new Promise((resolve) => {
|
|
21
22
|
pending.set(jobId, resolve);
|
|
@@ -15,17 +15,32 @@ export function backendForFormat(format, defaultBackend) {
|
|
|
15
15
|
return format === "step" ? "occt" : defaultBackend();
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Job ids are unique per controller, not just per mount: `export-<c>-<n>` and
|
|
19
|
+
// `warm-<c>-<n>`, where <c> numbers the controllers this module has made. Each
|
|
20
|
+
// mount makes one controller, but a host may keep a worker across mounts
|
|
21
|
+
// (partforge-cloud's pool rebinds it with setPart), and that worker's replies
|
|
22
|
+
// then reach whichever mount is listening now. Exports are never epoch-guarded
|
|
23
|
+
// (KERNEL-CONTRACT), so a disposed mount's slow export still answers after the
|
|
24
|
+
// remount. When every mount numbered its jobs from 1, that late reply settled
|
|
25
|
+
// the next mount's same-numbered export with pre-edit geometry. A worker can
|
|
26
|
+
// only be shared within one realm, and this counter covers the whole realm.
|
|
27
|
+
let controllers = 0;
|
|
28
|
+
// Every id any controller mints, so a reply to another controller's job is
|
|
29
|
+
// recognised even though this controller never sent it.
|
|
30
|
+
const CONTROLLER_JOB_ID = /^(?:export|warm)-\d+-\d+$/;
|
|
31
|
+
|
|
18
32
|
export function createExportController({ send, currentView, title, defaultBackend = () => "manifold", currentParams = () => ({}) }) {
|
|
19
33
|
const pending = new Map(); // jobId -> { resolve, reject, onProgress }
|
|
34
|
+
const controller = ++controllers;
|
|
20
35
|
let nextId = 1;
|
|
21
|
-
// Warm jobs
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
36
|
+
// Warm jobs get their own prefix for the reason mount.js spells out for
|
|
37
|
+
// tessellate-imports: this map is keyed by jobId alone and read before any
|
|
38
|
+
// type check, so a warm and an export sharing an id would settle each
|
|
39
|
+
// other's Promise.
|
|
25
40
|
let nextWarmId = 1;
|
|
26
41
|
|
|
27
42
|
function exportParts({ parts, format, quality = "print", onProgress } = {}) {
|
|
28
|
-
const jobId = nextId
|
|
43
|
+
const jobId = `export-${controller}-${nextId++}`;
|
|
29
44
|
const type = `export-${format}`;
|
|
30
45
|
const backend = backendForFormat(format, defaultBackend);
|
|
31
46
|
return new Promise((resolve, reject) => {
|
|
@@ -45,18 +60,25 @@ export function createExportController({ send, currentView, title, defaultBacken
|
|
|
45
60
|
// failure or teardown, and NEVER rejects — a speculative warm must not become
|
|
46
61
|
// an unhandled rejection in a host that fired it and moved on.
|
|
47
62
|
function warmKernel() {
|
|
48
|
-
const jobId = `warm-${nextWarmId++}`;
|
|
63
|
+
const jobId = `warm-${controller}-${nextWarmId++}`;
|
|
49
64
|
return new Promise((resolve) => {
|
|
50
65
|
pending.set(jobId, { resolve: () => resolve(true), reject: () => resolve(false) });
|
|
51
66
|
send({ type: "warm-kernel", jobId }, backendForFormat("step", defaultBackend));
|
|
52
67
|
});
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
// Returns true iff this message
|
|
56
|
-
// can skip legacy handling)
|
|
70
|
+
// Returns true iff this message is a reply to an export-controller job (so
|
|
71
|
+
// the caller can skip legacy handling): a pending job is settled, any other
|
|
72
|
+
// is dropped. `sink` is partforge's onDownload.
|
|
57
73
|
function handleMessage(m, sink) {
|
|
58
|
-
const
|
|
59
|
-
|
|
74
|
+
const jobId = m?.jobId;
|
|
75
|
+
const entry = jobId != null ? pending.get(jobId) : undefined;
|
|
76
|
+
// A controller's id with nothing pending here: a job this controller
|
|
77
|
+
// already settled, or one a disposed controller sent to a worker the host
|
|
78
|
+
// kept. Either way its Promise is already settled, so claim the reply and
|
|
79
|
+
// drop it. Passing it on would let mount's switch read an "error" as a
|
|
80
|
+
// failed build and a "download" as a viewbar save.
|
|
81
|
+
if (!entry) return typeof jobId === "string" && CONTROLLER_JOB_ID.test(jobId);
|
|
60
82
|
if (m.type === "progress") { entry.onProgress?.(m.phase); return true; }
|
|
61
83
|
if (m.type === "kernel-warm") { pending.delete(m.jobId); entry.resolve(); return true; }
|
|
62
84
|
if (m.type === "download") {
|
package/src/framework/mount.js
CHANGED
|
@@ -528,14 +528,14 @@ export function mount(part, { createWorker, elements = {}, onBuild, onPick, pick
|
|
|
528
528
|
// reset the latch instead of stranding it at "requested" forever.
|
|
529
529
|
// String-namespaced ("tess-N"), mirroring capture-build.js's "cap-N" —
|
|
530
530
|
// this request shares the OCCT worker's message space with
|
|
531
|
-
// export-controller's
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
531
|
+
// export-controller's "export-"/"warm-" jobIds, and exportCtl.handleMessage
|
|
532
|
+
// (called before mount's own switch, below) claims every id in its own
|
|
533
|
+
// namespace before checking type. An id inside that namespace would let the
|
|
534
|
+
// export controller wrongly claim this reply — rejecting an unrelated
|
|
535
|
+
// export with the tessellation failure text, or dropping the reply, AND
|
|
536
|
+
// leaving importMeshState stranded at "requested" (mount's own switch,
|
|
537
|
+
// which would have reset it, never runs). The "tess-" prefix makes that
|
|
538
|
+
// collision impossible.
|
|
539
539
|
let importTessellateJobId = null;
|
|
540
540
|
let importTessellateJobSeq = 0;
|
|
541
541
|
|