'use strict' import { WebContents } from 'electron' const v8Util = process.electronBinding('v8_util') const getOwnerKey = (webContents: WebContents, contextId: string) => { return `${webContents.id}-${contextId}` } class ObjectsRegistry { private nextId: number = 0 // Stores all objects by ref-counting. // (id) => {object, count} private storage: Record = {} // Stores the IDs + refCounts of objects referenced by WebContents. // (ownerKey) => { id: refCount } private owners: Record> = {} // Register a new object and return its assigned ID. If the object is already // registered then the already assigned ID would be returned. add (webContents: WebContents, contextId: string, obj: any) { // Get or assign an ID to the object. const id = this.saveToStorage(obj) // Add object to the set of referenced objects. const ownerKey = getOwnerKey(webContents, contextId) let owner = this.owners[ownerKey] if (!owner) { owner = this.owners[ownerKey] = new Map() this.registerDeleteListener(webContents, contextId) } if (!owner.has(id)) { owner.set(id, 0) // Increase reference count if not referenced before. this.storage[id].count++ } owner.set(id, owner.get(id)! + 1) return id } // Get an object according to its ID. get (id: number) { const pointer = this.storage[id] if (pointer != null) return pointer.object } // Dereference an object according to its ID. // Note that an object may be double-freed (cleared when page is reloaded, and // then garbage collected in old page). // rendererSideRefCount is the ref count that the renderer process reported // at time of GC if this is different to the number of references we sent to // the given owner then a GC occurred between a ref being sent and the value // being pulled out of the weak map. // In this case we decrement out ref count and do not delete the stored // object // For more details on why we do renderer side ref counting see // https://github.com/electron/electron/pull/17464 remove (webContents: WebContents, contextId: string, id: number, rendererSideRefCount: number) { const ownerKey = getOwnerKey(webContents, contextId) const owner = this.owners[ownerKey] if (owner && owner.has(id)) { const newRefCount = owner.get(id)! - rendererSideRefCount // Only completely remove if the number of references GCed in the // renderer is the same as the number of references we sent them if (newRefCount <= 0) { // Remove the reference in owner. owner.delete(id) // Dereference from the storage. this.dereference(id) } else { owner.set(id, newRefCount) } } } // Clear all references to objects refrenced by the WebContents. clear (webContents: WebContents, contextId: string) { const ownerKey = getOwnerKey(webContents, contextId) const owner = this.owners[ownerKey] if (!owner) return for (const id of owner.keys()) this.dereference(id) delete this.owners[ownerKey] } // Private: Saves the object into storage and assigns an ID for it. saveToStorage (object: any) { let id: number = v8Util.getHiddenValue(object, 'atomId') if (!id) { id = ++this.nextId this.storage[id] = { count: 0, object: object } v8Util.setHiddenValue(object, 'atomId', id) } return id } // Private: Dereference the object from store. dereference (id: number) { const pointer = this.storage[id] if (pointer == null) { return } pointer.count -= 1 if (pointer.count === 0) { v8Util.deleteHiddenValue(pointer.object, 'atomId') delete this.storage[id] } } // Private: Clear the storage when renderer process is destroyed. registerDeleteListener (webContents: WebContents, contextId: string) { // contextId => ${processHostId}-${contextCount} const processHostId = contextId.split('-')[0] const listener = (_: any, deletedProcessHostId: string) => { if (deletedProcessHostId && deletedProcessHostId.toString() === processHostId) { webContents.removeListener('render-view-deleted' as any, listener) this.clear(webContents, contextId) } } // Note that the "render-view-deleted" event may not be emitted on time when // the renderer process get destroyed because of navigation, we rely on the // renderer process to send "ELECTRON_BROWSER_CONTEXT_RELEASE" message to // guard this situation. webContents.on('render-view-deleted' as any, listener) } } export default new ObjectsRegistry()