From 603c221365c28c25996315dab1f442460bf11607 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Fri, 17 Jan 2020 18:23:32 +0100 Subject: [PATCH] Improved DisposablePromise disposal process. --- src/core/DisposablePromise.ts | 275 ++++++++++++++---- src/quest_editor/loading/EntityAssetLoader.ts | 71 ++--- test/src/core/FileSystemHttpClient.ts | 4 +- 3 files changed, 261 insertions(+), 89 deletions(-) diff --git a/src/core/DisposablePromise.ts b/src/core/DisposablePromise.ts index 4e9196b3..a67b32ac 100644 --- a/src/core/DisposablePromise.ts +++ b/src/core/DisposablePromise.ts @@ -1,79 +1,214 @@ import { Disposable } from "./observable/Disposable"; -export class DisposablePromise extends Promise implements Disposable { - static resolve(value?: T | PromiseLike): DisposablePromise { - return new DisposablePromise((resolve, reject) => { - if (value === undefined) { - new DisposablePromise(() => undefined); - } else if ("then" in value) { - value.then(resolve, reject); - } else { - resolve(value); - } - }); - } +enum State { + Pending, + Fulfilled, + Rejected, + Disposed, +} - static wrap(promise: Promise, dispose?: () => void): DisposablePromise { - if (promise instanceof DisposablePromise) { - return promise; - } else { - return new DisposablePromise((resolve, reject) => { - promise.then(resolve).catch(reject); - }, dispose); - } - } +export class DisposablePromise implements Promise, Disposable { + static all(values: Iterable>): DisposablePromise { + return new DisposablePromise( + (resolve, reject) => { + const results: T[] = []; + let len = 0; - private disposed: boolean; + function add_result(r: T): void { + results.push(r); - private readonly disposal_handler?: () => void; + if (results.length === len) { + resolve(results); + } + } - constructor( - executor: ( - resolve: (value?: T | PromiseLike) => void, - reject: (reason?: any) => void, - ) => void, - dispose?: () => void, - ) { - let resolve_fn: (value?: T | PromiseLike | undefined) => void; - let reject_fn: (value?: T | PromiseLike | undefined) => void; + for (const value of values) { + len++; - super((resolve, reject) => { - resolve_fn = resolve; - reject_fn = reject; - }); - - this.disposed = false; - this.disposal_handler = dispose; - - executor( - value => { - if (!this.disposed) { - resolve_fn(value); + if (is_promise_like(value)) { + value.then(add_result, reject); + } else { + add_result(value); + } } }, - reason => { - if (!this.disposed) { - reject_fn(reason); + () => { + for (const value of values) { + if (value instanceof DisposablePromise) { + value.dispose(); + } } }, ); } + static resolve(value: T | PromiseLike, dispose?: () => void): DisposablePromise { + if (is_promise_like(value)) { + return new DisposablePromise((resolve, reject) => { + value.then(resolve, reject); + }, dispose); + } else { + return new DisposablePromise(resolve => { + resolve(value); + }, dispose); + } + } + + private state: State = State.Pending; + private value?: T; + private reason?: any; + + private readonly fulfillment_listeners: ((value: T) => unknown)[] = []; + private readonly rejection_listeners: ((reason: any) => unknown)[] = []; + private readonly disposal_handler?: () => void; + + [Symbol.toStringTag] = "DisposablePromise"; + + constructor( + executor: ( + resolve: (value: T | PromiseLike) => void, + reject: (reason?: any) => void, + ) => void, + dispose?: () => void, + ) { + this.disposal_handler = dispose; + + executor(this.executor_resolve, this.executor_reject); + } + + private executor_resolve = (value: T | PromiseLike): void => { + if (is_promise_like(value)) { + if (this.state !== State.Pending) return; + + value.then( + p_value => { + this.fulfilled(p_value); + }, + p_reason => { + this.rejected(p_reason); + }, + ); + } else { + this.fulfilled(value); + } + }; + + private executor_reject = (reason?: any): void => { + this.rejected(reason); + }; + + private fulfilled(value: T): void { + if (this.state !== State.Pending) return; + + this.state = State.Fulfilled; + this.value = value; + + for (const listener of this.fulfillment_listeners) { + listener(value); + } + + this.fulfillment_listeners.splice(0); + this.rejection_listeners.splice(0); + } + + private rejected(reason?: any): void { + if (this.state !== State.Pending) return; + + this.state = State.Rejected; + this.reason = reason; + + for (const listener of this.rejection_listeners) { + listener(reason); + } + + this.fulfillment_listeners.splice(0); + this.rejection_listeners.splice(0); + } + then( - onfulfilled?: ((value: T) => PromiseLike | TResult1) | undefined | null, - onrejected?: ((reason: any) => PromiseLike | TResult2) | undefined | null, + onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, + onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null, ): DisposablePromise { - return DisposablePromise.wrap(super.then(onfulfilled, onrejected), () => this.dispose()); + return new DisposablePromise( + (resolve, reject) => { + if (onfulfilled == undefined) { + this.add_fulfillment_listener(resolve as any); + } else { + this.add_fulfillment_listener(value => { + try { + resolve(onfulfilled(value)); + } catch (e) { + reject(e); + } + }); + } + + if (onrejected == undefined) { + this.add_rejection_listener(reject); + } else { + this.add_rejection_listener(reason => { + try { + resolve(onrejected(reason)); + } catch (e) { + reject(e); + } + }); + } + }, + () => this.dispose(), + ); } catch( onrejected?: ((reason: any) => PromiseLike | TResult) | undefined | null, ): DisposablePromise { - return DisposablePromise.wrap(super.catch(onrejected), () => this.dispose()); + return new DisposablePromise( + (resolve, reject) => { + this.add_fulfillment_listener(resolve as any); + + if (onrejected == undefined) { + this.add_rejection_listener(reject); + } else { + this.add_rejection_listener(reason => { + try { + resolve(onrejected(reason)); + } catch (e) { + reject(e); + } + }); + } + }, + () => this.dispose(), + ); } finally(onfinally?: (() => void) | undefined | null): DisposablePromise { - return DisposablePromise.wrap(super.finally(onfinally), () => this.dispose()); + if (onfinally == undefined) { + return this; + } else { + return new DisposablePromise( + (resolve, reject) => { + this.add_fulfillment_listener(value => { + try { + onfinally(); + resolve(value); + } catch (e) { + reject(e); + } + }); + + this.add_rejection_listener(value => { + try { + onfinally(); + reject(value); + } catch (e) { + reject(e); + } + }); + }, + () => this.dispose(), + ); + } } /** @@ -81,9 +216,41 @@ export class DisposablePromise extends Promise implements Disposable { * be called. */ dispose(): void { - if (!this.disposed) { - this.disposed = true; + if (this.state !== State.Disposed) { + this.state = State.Disposed; this.disposal_handler?.(); } } + + private add_fulfillment_listener(listener: (value: T) => unknown): void { + switch (this.state) { + case State.Pending: + this.fulfillment_listeners.push(listener); + break; + case State.Fulfilled: + listener(this.value!); + break; + case State.Rejected: + case State.Disposed: + break; + } + } + + private add_rejection_listener(listener: (reason: any) => unknown): void { + switch (this.state) { + case State.Pending: + this.rejection_listeners.push(listener); + break; + case State.Rejected: + listener(this.reason); + break; + case State.Fulfilled: + case State.Disposed: + break; + } + } +} + +function is_promise_like(value?: T | PromiseLike): value is PromiseLike { + return value != undefined && typeof (value as any).then === "function"; } diff --git a/src/quest_editor/loading/EntityAssetLoader.ts b/src/quest_editor/loading/EntityAssetLoader.ts index ac99c43d..8868d1e0 100644 --- a/src/quest_editor/loading/EntityAssetLoader.ts +++ b/src/quest_editor/loading/EntityAssetLoader.ts @@ -26,15 +26,11 @@ DEFAULT_ENTITY.translate(0, 10, 0); DEFAULT_ENTITY.computeBoundingBox(); DEFAULT_ENTITY.computeBoundingSphere(); -const DEFAULT_ENTITY_PROMISE: DisposablePromise = DisposablePromise.resolve( - DEFAULT_ENTITY, -); +const DEFAULT_ENTITY_PROMISE = DisposablePromise.resolve(DEFAULT_ENTITY); const DEFAULT_ENTITY_TEX: Texture[] = []; -const DEFAULT_ENTITY_TEX_PROMISE: DisposablePromise = DisposablePromise.resolve( - DEFAULT_ENTITY_TEX, -); +const DEFAULT_ENTITY_TEX_PROMISE = DisposablePromise.resolve(DEFAULT_ENTITY_TEX); export class EntityAssetLoader implements Disposable { private readonly disposer = new Disposer(); @@ -91,7 +87,7 @@ export class EntityAssetLoader implements Disposable { ); } - load_data( + private load_data( type: EntityType, asset_type: AssetType, ): DisposablePromise<{ url: string; data: ArrayBuffer }> { @@ -191,7 +187,14 @@ enum AssetType { Texture, } -function entity_type_to_url(type: EntityType, asset_type: AssetType): string { +/** + * @param type + * @param asset_type + * @param no - Asset number. Some entities have multiple assets that need to be combined. + */ +function entity_type_to_url(type: EntityType, asset_type: AssetType, no?: number): string { + const no_str = no == undefined ? "" : `-${no}`; + if (is_npc_type(type)) { switch (type) { // The dubswitch model is in XJ format. @@ -201,53 +204,55 @@ function entity_type_to_url(type: EntityType, asset_type: AssetType): string { // Episode II VR Temple case NpcType.Hildebear2: - return entity_type_to_url(NpcType.Hildebear, asset_type); + return entity_type_to_url(NpcType.Hildebear, asset_type, no); case NpcType.Hildeblue2: - return entity_type_to_url(NpcType.Hildeblue, asset_type); + return entity_type_to_url(NpcType.Hildeblue, asset_type, no); case NpcType.RagRappy2: - return entity_type_to_url(NpcType.RagRappy, asset_type); + return entity_type_to_url(NpcType.RagRappy, asset_type, no); case NpcType.Monest2: - return entity_type_to_url(NpcType.Monest, asset_type); + return entity_type_to_url(NpcType.Monest, asset_type, no); case NpcType.Mothmant2: - return entity_type_to_url(NpcType.Mothmant, asset_type); + return entity_type_to_url(NpcType.Mothmant, asset_type, no); case NpcType.PoisonLily2: - return entity_type_to_url(NpcType.PoisonLily, asset_type); + return entity_type_to_url(NpcType.PoisonLily, asset_type, no); case NpcType.NarLily2: - return entity_type_to_url(NpcType.NarLily, asset_type); + return entity_type_to_url(NpcType.NarLily, asset_type, no); case NpcType.GrassAssassin2: - return entity_type_to_url(NpcType.GrassAssassin, asset_type); + return entity_type_to_url(NpcType.GrassAssassin, asset_type, no); case NpcType.Dimenian2: - return entity_type_to_url(NpcType.Dimenian, asset_type); + return entity_type_to_url(NpcType.Dimenian, asset_type, no); case NpcType.LaDimenian2: - return entity_type_to_url(NpcType.LaDimenian, asset_type); + return entity_type_to_url(NpcType.LaDimenian, asset_type, no); case NpcType.SoDimenian2: - return entity_type_to_url(NpcType.SoDimenian, asset_type); + return entity_type_to_url(NpcType.SoDimenian, asset_type, no); case NpcType.DarkBelra2: - return entity_type_to_url(NpcType.DarkBelra, asset_type); + return entity_type_to_url(NpcType.DarkBelra, asset_type, no); // Episode II VR Spaceship case NpcType.SavageWolf2: - return entity_type_to_url(NpcType.SavageWolf, asset_type); + return entity_type_to_url(NpcType.SavageWolf, asset_type, no); case NpcType.BarbarousWolf2: - return entity_type_to_url(NpcType.BarbarousWolf, asset_type); + return entity_type_to_url(NpcType.BarbarousWolf, asset_type, no); case NpcType.PanArms2: - return entity_type_to_url(NpcType.PanArms, asset_type); + return entity_type_to_url(NpcType.PanArms, asset_type, no); case NpcType.Dubchic2: - return entity_type_to_url(NpcType.Dubchic, asset_type); + return entity_type_to_url(NpcType.Dubchic, asset_type, no); case NpcType.Gilchic2: - return entity_type_to_url(NpcType.Gilchic, asset_type); + return entity_type_to_url(NpcType.Gilchic, asset_type, no); case NpcType.Garanz2: - return entity_type_to_url(NpcType.Garanz, asset_type); + return entity_type_to_url(NpcType.Garanz, asset_type, no); case NpcType.Dubswitch2: - return entity_type_to_url(NpcType.Dubswitch, asset_type); + return entity_type_to_url(NpcType.Dubswitch, asset_type, no); case NpcType.Delsaber2: - return entity_type_to_url(NpcType.Delsaber, asset_type); + return entity_type_to_url(NpcType.Delsaber, asset_type, no); case NpcType.ChaosSorcerer2: - return entity_type_to_url(NpcType.ChaosSorcerer, asset_type); + return entity_type_to_url(NpcType.ChaosSorcerer, asset_type, no); default: - return `/npcs/${NpcType[type]}.${asset_type === AssetType.Geometry ? "nj" : "xvm"}`; + return `/npcs/${NpcType[type]}${no_str}.${ + asset_type === AssetType.Geometry ? "nj" : "xvm" + }`; } } else { if (asset_type === AssetType.Geometry) { @@ -268,13 +273,13 @@ function entity_type_to_url(type: EntityType, asset_type: AssetType): string { case ObjectType.FallingRock: case ObjectType.DesertFixedTypeBoxBreakableCrystals: case ObjectType.BeeHive: - return `/objects/${object_data(type).pso_id}.nj`; + return `/objects/${object_data(type).pso_id}${no_str}.nj`; default: - return `/objects/${object_data(type).pso_id}.xj`; + return `/objects/${object_data(type).pso_id}${no_str}.xj`; } } else { - return `/objects/${object_data(type).pso_id}.xvm`; + return `/objects/${object_data(type).pso_id}${no_str}.xvm`; } } } diff --git a/test/src/core/FileSystemHttpClient.ts b/test/src/core/FileSystemHttpClient.ts index 5d0125b2..f2d806c9 100644 --- a/test/src/core/FileSystemHttpClient.ts +++ b/test/src/core/FileSystemHttpClient.ts @@ -6,13 +6,13 @@ export class FileSystemHttpClient implements HttpClient { get(url: string): HttpResponse { return { json(): DisposablePromise { - return DisposablePromise.wrap(fs.promises.readFile(`./assets${url}`)).then(buf => + return DisposablePromise.resolve(fs.promises.readFile(`./assets${url}`)).then(buf => JSON.parse(buf.toString()), ); }, array_buffer(): DisposablePromise { - return DisposablePromise.wrap(fs.promises.readFile(`./assets${url}`)).then(buf => + return DisposablePromise.resolve(fs.promises.readFile(`./assets${url}`)).then(buf => buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength), ); },