Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions api/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export const flockModels = {
}
};

// --- bail if there is no live scene (e.g. called during dispose) ---
if (!flock.scene || flock.scene.isDisposed) {
console.warn("createCharacter: no active scene");
return "error_no_scene";
}

// --- validate ---
if (!modelName || typeof modelName !== "string" || modelName.length > 100) {
console.warn("createCharacter: invalid modelName");
Expand Down Expand Up @@ -94,6 +100,11 @@ export const flockModels = {
resolveReady = res;
rejectReady = rej;
});
// Aborted loads reject this promise; attach a benign catch so the
// rejection never surfaces as an unhandled rejection when no
// whenModelReady consumer attached a handler. Real consumers still
// observe the rejection via their own .then/.catch registrations.
readyPromise.catch(() => {});
flock.modelReadyPromises.set(meshName, readyPromise);
// Also register the pre-sanitization name (e.g. "dimnnd monkey" → "dimnndmonkey").
// This lets event handlers declared before createCharacter (which still hold the
Expand Down Expand Up @@ -131,6 +142,33 @@ export const flockModels = {
{ signal: flock.abortController?.signal },
)
.then((container) => {
// The scene was disposed / the load was aborted while this
// container was still loading. Drop it without touching the
// (now null) scene. On the abort path onAbort has already
// rejected readyPromise and released the reserved name; on the
// disposed-without-abort path we must do that cleanup here so
// whenModelReady waiters settle and the name isn't leaked.
if (signal?.aborted || !flock.scene || flock.scene.isDisposed) {
try {
container?.dispose?.();
} catch (_) {
console.warn("Suppressed non-critical error:", _);
}
if (!signal?.aborted) {
rejectReady(new Error("scene disposed"));
flock.modelReadyPromises.delete(meshName);
if (
originalBase !== meshName &&
flock.modelReadyPromises.get(originalBase) === readyPromise
) {
flock.modelReadyPromises.delete(originalBase);
}
flock._releaseName?.(meshName);
}
cleanupAbort();
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

container.addAllToScene();

const mesh = container.meshes[0];
Expand Down Expand Up @@ -303,6 +341,10 @@ export const flockModels = {
};

try {
if (!flock.scene || flock.scene.isDisposed) {
console.warn("createObject: no active scene");
return "error_no_scene";
}
if (flock.maxMeshesReached()) return "error_" + flock.scene.getUniqueId();

let [desiredBase, bKey] = modelId.includes("__")
Expand Down Expand Up @@ -337,6 +379,7 @@ export const flockModels = {

if (flock.modelsBeingLoaded[modelName]) {
flock.modelsBeingLoaded[modelName].then(() => {
if (!flock.scene || flock.scene.isDisposed) return;
flock._recycleOldestByKey(modelName);
const mesh = flock.modelCache[modelName].clone(
flock.modelCache[modelName].name,
Expand All @@ -358,6 +401,17 @@ export const flockModels = {
// ... inside the loadPromise.then block ...

loadPromise.then((container) => {
// Scene disposed while the container was loading: drop it.
if (!flock.scene || flock.scene.isDisposed) {
try {
container?.dispose?.();
} catch (_) {
console.warn("Suppressed non-critical error:", _);
}
delete flock.modelsBeingLoaded[modelName];
return;
}

container.addAllToScene();
container.animationGroups.forEach((ag) => ag.stop());
container.meshes.forEach((m) => {
Expand Down
Loading