UNPKG

3d-tiles-renderer

Version:

https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification

282 lines (177 loc) 5.06 kB
// Error thrown when a cache item's promise is rejected because the item was released // before its callback could run. class DataCacheItemRemovedError extends DOMException { constructor() { super( 'DataCache: Item removed', 'AbortError' ); } } function hash( ...args ) { return args.join( '_' ); } // class for retrieving and locking data being requested // "fetchItem" and "disposeItem" should be implemented export class DataCache { constructor() { this.cache = {}; this.count = 0; this.cachedBytes = 0; this.active = 0; } // overridable fetchItem( keys, signal ) {} // called with null if the fetch failed disposeItem( item, keys ) {} getMemoryUsage( item ) { return 0; } // sets the data in the cache explicitly without need to load setData( ...args ) { const { cache } = this; const data = args.pop(); const key = hash( ...args ); if ( key in cache ) { throw new Error( `DataCache: "${ key }" is already present.` ); } else { this.cache[ key ] = { abortController: new AbortController(), result: data, count: 1, bytes: this.getMemoryUsage( data ), }; this.count ++; this.cachedBytes += this.cache[ key ].bytes; } return data; } // fetches the associated data if it doesn't exist and increments the lock counter lock( ...args ) { const { cache } = this; const key = hash( ...args ); if ( key in cache ) { cache[ key ].count ++; } else { const abortController = new AbortController(); const info = { abortController, result: null, count: 1, bytes: 0, args, }; this.active ++; info.result = this.fetchItem( args, abortController.signal ); if ( info.result instanceof Promise ) { info.result = info.result.then( res => { abortController.signal.throwIfAborted(); info.result = res; info.bytes = this.getMemoryUsage( res ); this.cachedBytes += info.bytes; return res; } ).finally( () => { this.active --; } ); } else { this.active --; info.bytes = this.getMemoryUsage( info.result ); this.cachedBytes += info.bytes; } this.cache[ key ] = info; this.count ++; } return cache[ key ].result; } // decrements the lock counter for the item and deletes the item if it has reached zero release( ...args ) { const key = hash( ...args ); this.releaseViaFullKey( key ); } // get the loaded item get( ...args ) { const { cache } = this; const key = hash( ...args ); if ( key in cache && cache[ key ].count > 0 ) { return cache[ key ].result; } else { return null; } } has( ...args ) { const { cache } = this; const key = hash( ...args ); return key in cache; } forEachItem( cb ) { const { cache } = this; for ( const key in cache ) { const info = cache[ key ]; if ( ! ( info.result instanceof Promise ) ) { cb( info.result, info.args ); } } } // dispose all items dispose() { const { cache } = this; for ( const key in cache ) { const { abortController } = cache[ key ]; abortController.abort( new DataCacheItemRemovedError() ); this.releaseViaFullKey( key, true ); } this.cache = {}; } // releases an item with an optional force flag releaseViaFullKey( key, force = false ) { const { cache } = this; if ( key in cache && cache[ key ].count > 0 ) { // decrement the lock const info = cache[ key ]; info.count --; // if the item is no longer being used if ( info.count === 0 || force ) { const disposeCallback = () => { // if the object isn't in the cache anymore then exit early because it's been disposed elsewhere if ( cache[ key ] !== info ) { return; } // abort any loads const { result, abortController } = info; abortController.abort( new DataCacheItemRemovedError() ); // dispose of the object even if it still is in progress if ( result instanceof Promise ) { // "disposeItem" will throw potentially if fetch, etc are cancelled using the abort signal result .then( item => { this.disposeItem( item, info.args ); } ) .catch( () => { this.disposeItem( null, info.args ); } ) .finally( () => { this.count --; this.cachedBytes -= info.bytes; } ); } else { this.disposeItem( result, info.args ); this.count --; this.cachedBytes -= info.bytes; } delete cache[ key ]; }; if ( force ) { // if we're forcing disposal then dispose immediately disposeCallback(); } else { // queue for disposal in a frame here - we need to make sure we're not disposing of something twice // this can get called multiple times in a row to increment then decrement again. queueMicrotask( () => { if ( info.count === 0 ) { disposeCallback(); } } ); } } return true; } throw new Error( 'DataCache: Attempting to release key that does not exist' ); } }