Improved DisposablePromise disposal process.

This commit is contained in:
Daan Vanden Bosch 2020-01-17 18:23:32 +01:00
parent 79a68a6b7b
commit 603c221365
3 changed files with 261 additions and 89 deletions

View File

@ -1,79 +1,214 @@
import { Disposable } from "./observable/Disposable";
export class DisposablePromise<T> extends Promise<T> implements Disposable {
static resolve<T>(value?: T | PromiseLike<T>): DisposablePromise<T> {
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<T>(promise: Promise<T>, dispose?: () => void): DisposablePromise<T> {
if (promise instanceof DisposablePromise) {
return promise;
} else {
return new DisposablePromise((resolve, reject) => {
promise.then(resolve).catch(reject);
}, dispose);
}
}
export class DisposablePromise<T> implements Promise<T>, Disposable {
static all<T>(values: Iterable<T | PromiseLike<T>>): DisposablePromise<T[]> {
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<T>) => void,
reject: (reason?: any) => void,
) => void,
dispose?: () => void,
) {
let resolve_fn: (value?: T | PromiseLike<T> | undefined) => void;
let reject_fn: (value?: T | PromiseLike<T> | 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<T>(value: T | PromiseLike<T>, dispose?: () => void): DisposablePromise<T> {
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<T>) => 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<T>): 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<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => PromiseLike<TResult1> | TResult1) | undefined | null,
onrejected?: ((reason: any) => PromiseLike<TResult2> | TResult2) | undefined | null,
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null,
): DisposablePromise<TResult1 | TResult2> {
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<TResult = never>(
onrejected?: ((reason: any) => PromiseLike<TResult> | TResult) | undefined | null,
): DisposablePromise<T | TResult> {
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<T> {
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<T> extends Promise<T> 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<T>(value?: T | PromiseLike<T>): value is PromiseLike<T> {
return value != undefined && typeof (value as any).then === "function";
}

View File

@ -26,15 +26,11 @@ DEFAULT_ENTITY.translate(0, 10, 0);
DEFAULT_ENTITY.computeBoundingBox();
DEFAULT_ENTITY.computeBoundingSphere();
const DEFAULT_ENTITY_PROMISE: DisposablePromise<BufferGeometry> = DisposablePromise.resolve(
DEFAULT_ENTITY,
);
const DEFAULT_ENTITY_PROMISE = DisposablePromise.resolve<BufferGeometry>(DEFAULT_ENTITY);
const DEFAULT_ENTITY_TEX: Texture[] = [];
const DEFAULT_ENTITY_TEX_PROMISE: DisposablePromise<Texture[]> = DisposablePromise.resolve(
DEFAULT_ENTITY_TEX,
);
const DEFAULT_ENTITY_TEX_PROMISE = DisposablePromise.resolve<Texture[]>(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`;
}
}
}

View File

@ -6,13 +6,13 @@ export class FileSystemHttpClient implements HttpClient {
get(url: string): HttpResponse {
return {
json<T>(): DisposablePromise<T> {
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<ArrayBuffer> {
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),
);
},