mirror of
https://github.com/Shu-Ying/noname-shuying.git
synced 2026-10-03 18:40:01 +08:00
107 lines
5.3 KiB
JavaScript
107 lines
5.3 KiB
JavaScript
// Candidate only. No engine globals, event prototype patches, or save writes.
|
|
const owners = new WeakMap();
|
|
|
|
export function createBattleSession(id) {
|
|
if (typeof id !== "string" || !id.trim()) throw new TypeError("Session ID required");
|
|
const token = Object.freeze({ id });
|
|
let state = "running", disposePromise = null;
|
|
const pending = new Set(), events = new Set(), resources = new Map(), errors = [];
|
|
const waiters = new Set();
|
|
const notify = () => { for (const wake of waiters) wake(); waiters.clear(); };
|
|
const ensureRunning = () => { if (state !== "running") throw new Error("Session is stopping"); };
|
|
const claim = object => {
|
|
if (!object || (typeof object !== "object" && typeof object !== "function")) throw new TypeError("Object required");
|
|
const owner = owners.get(object);
|
|
if (owner && owner !== token) throw new Error("Foreign session resource");
|
|
owners.set(object, token);
|
|
};
|
|
// Observe a native completion promise; never assimilate GameEvent.then(), which can start queues.
|
|
const observe = promise => {
|
|
if (!(promise instanceof Promise)) throw new TypeError("Native completion Promise required");
|
|
pending.add(promise);
|
|
promise.then(() => { pending.delete(promise); notify(); }, error => {
|
|
errors.push(error); pending.delete(promise); notify();
|
|
});
|
|
return promise;
|
|
};
|
|
const api = {
|
|
id,
|
|
get state() { return state; },
|
|
get active() { return state === "running"; },
|
|
get pendingCount() { return pending.size; },
|
|
guard(callback) {
|
|
if (typeof callback !== "function") throw new TypeError("Callback required");
|
|
return (...args) => state === "running" ? callback(...args) : undefined;
|
|
},
|
|
runTask(factory) {
|
|
ensureRunning();
|
|
if (typeof factory !== "function") throw new TypeError("Task factory required");
|
|
// Reserve before invoking user code so a stop request inside it cannot race drain().
|
|
return observe(Promise.resolve().then(() => state === "running" ? factory(api) : undefined));
|
|
},
|
|
ownEvent(event, completion) {
|
|
ensureRunning();
|
|
if (typeof event?.finish !== "function" || !(completion instanceof Promise)) throw new TypeError("Event and native completion required");
|
|
claim(event);
|
|
if (events.has(event)) throw new Error("Event already registered");
|
|
events.add(event); observe(completion);
|
|
return event;
|
|
},
|
|
ownResource(resource, release) {
|
|
ensureRunning();
|
|
if (typeof release !== "function") throw new TypeError("Release function required");
|
|
claim(resource);
|
|
if (resources.has(resource)) throw new Error("Resource already registered");
|
|
resources.set(resource, release);
|
|
return resource;
|
|
},
|
|
requestStop() {
|
|
if (state !== "running") return false;
|
|
state = "stopping";
|
|
// Only explicitly registered events. Do not erase global next/after or guess ownership.
|
|
for (const event of events) {
|
|
try { event.finish(); } catch (error) { errors.push(error); }
|
|
}
|
|
notify();
|
|
return true;
|
|
},
|
|
async drain() {
|
|
if (state === "running") throw new Error("requestStop required before drain");
|
|
// Must be awaited by an external coordinator, not from a task tracked by this session.
|
|
while (pending.size) await new Promise(resolve => waiters.add(resolve));
|
|
if (errors.length) throw new AggregateError(errors, "Battle tasks failed; cleanup blocked");
|
|
if (state === "stopping") state = "drained";
|
|
},
|
|
dispose() {
|
|
if (state === "disposed") return Promise.resolve();
|
|
if (disposePromise) return disposePromise;
|
|
if (state !== "drained") return Promise.reject(new Error("Drain must complete before disposal"));
|
|
state = "disposing";
|
|
disposePromise = (async () => {
|
|
// Reverse acquisition order; stop at first failure to preserve dependent resources.
|
|
for (const [resource, release] of [...resources].reverse()) {
|
|
await release(resource);
|
|
resources.delete(resource); owners.delete(resource);
|
|
}
|
|
for (const event of events) owners.delete(event);
|
|
events.clear(); state = "disposed";
|
|
})().catch(error => { state = "drained"; throw error; }).finally(() => { disposePromise = null; });
|
|
return disposePromise;
|
|
},
|
|
};
|
|
return Object.freeze(api);
|
|
}
|
|
|
|
// A reused ID must never cause old-session cleanup to delete the new player's entry.
|
|
export function ownPlayerIndex(session, player, playerMap) {
|
|
const id = player.playerid;
|
|
if (!id || playerMap[id] !== player) throw new Error("Player must be registered first");
|
|
return session.ownResource(player, () => { if (playerMap[id] === player) delete playerMap[id]; });
|
|
}
|
|
|
|
// Engine pause booleans are not ownership tokens. Accept a lease from a coordinator instead.
|
|
export function ownPauseLease(session, lease) {
|
|
if (typeof lease?.release !== "function") throw new TypeError("Pause lease required");
|
|
return session.ownResource(lease, () => lease.release());
|
|
}
|