Skip to content

Instantly share code, notes, and snippets.

@zgover
Created August 20, 2022 02:59
Show Gist options
  • Save zgover/5be9c93488ad0175351f102dc6afebb6 to your computer and use it in GitHub Desktop.
Save zgover/5be9c93488ad0175351f102dc6afebb6 to your computer and use it in GitHub Desktop.
JavaScript Dependency Manager for Loading and Destroying Modules Based on it's Dependencies
export enum DependencyStatus {
WAITING = 'waiting',
LOADING = 'loading',
LOADED = 'loaded',
UNLOADING = 'unloading',
}
export type DependencyId = string
export type DependencyStatuses = Record<DependencyId, DependencyStatus>
export type DependenciesById = Record<DependencyId, Dependency>
export type DependencyDependencies = Record<DependencyId, true>
export type Dependents = Record<DependencyId, true>
export type DependencyDependents = Record<DependencyId, Dependents>
export interface Dependency {
id: DependencyId
dependencies?: DependencyDependencies
load(...args: any[]): void
destroy(...args: any[]): void
}
export default class DependencyManager {
readonly #statusesById: DependencyStatuses = {}
readonly #dependentsById: DependencyDependents = {}
readonly #dependenciesById: DependenciesById = {}
constructor()
constructor(dependencies: Array<Dependency>)
constructor(dependencies?: Array<Dependency>) {
this.#initialize(dependencies)
}
//region Self Lifecycle
#initialize(dependencies?: Array<Dependency>) {
const _dependencies = Array.isArray(dependencies) ? dependencies : []
for (const dependency of _dependencies) {
this.addDependency(dependency)
}
}
#destroy() {
const dependencyIds = Object.keys(this.#dependenciesById)
for (const id of dependencyIds) {
this.#removeDependency(id)
}
}
//endregion
//region Private Getters
#getDependency(dependencyId: DependencyId): Dependency | undefined {
return this.#dependenciesById[dependencyId]
}
#getDependencyDependents(dependencyId: DependencyId): Dependents | undefined {
return this.#dependentsById[dependencyId]
}
#getDependencyStatus(
dependencyId: DependencyId,
): DependencyStatus | undefined {
return this.#statusesById[dependencyId]
}
//endregion
//region Private Setters
#setDependencyProperties(
dependencyId: DependencyId,
dependency: Dependency,
): this {
this.#statusesById[dependencyId] = DependencyStatus.WAITING
this.#dependentsById[dependencyId] ||= {}
this.#dependenciesById[dependencyId] = dependency
return this
}
#setDependencyDependents(dependencyId: DependencyId): this {
for (const dependentId of Object.keys(
this.#getDependency(dependencyId)?.dependencies || {},
)) {
const dependents = (this.#dependentsById[dependentId] ||= {})
dependents[dependencyId] = true
}
return this
}
#removeDependencyDependents(dependencyId: DependencyId): this {
for (const dependentId of Object.keys(
this.#getDependency(dependencyId)?.dependencies || {},
)) {
delete this.#dependentsById[dependentId]?.[dependencyId]
}
return this
}
#removeDependencyProperties(dependencyId: DependencyId): this {
delete this.#dependentsById[dependencyId]
delete this.#statusesById[dependencyId]
delete this.#dependenciesById[dependencyId]
return this
}
//endregion
//region Private Dependency Guards
#hasDependency(dependencyId: DependencyId): boolean {
return Boolean(dependencyId && this.#getDependency(dependencyId))
}
#isDependencyWaiting(dependencyId: DependencyId): boolean {
return this.#getDependencyStatus(dependencyId) === DependencyStatus.WAITING
}
#isDependencyLoading(dependencyId: DependencyId): boolean {
return this.#getDependencyStatus(dependencyId) === DependencyStatus.LOADING
}
#isDependencyLoaded(dependencyId: DependencyId): boolean {
return this.#getDependencyStatus(dependencyId) === DependencyStatus.LOADED
}
#isDependencyUnloading(dependencyId: DependencyId): boolean {
return (
this.#getDependencyStatus(dependencyId) === DependencyStatus.UNLOADING
)
}
#areAllDependenciesLoaded(dependentId: DependencyId): boolean {
for (const dependencyId of Object.keys(
this.#getDependency(dependentId)?.dependencies || {},
)) {
if (!this.#isDependencyLoaded(dependencyId)) {
return false
}
}
return true
}
//endregion
//region Private Dependency Lifecycle
#loadDependencyDependents(dependencyId: DependencyId): this {
for (const dependentId of Object.keys(
this.#getDependencyDependents(dependencyId) || {},
)) {
if (!this.#isDependencyLoaded(dependentId)) {
this.#loadDependencyAndDependents(dependentId)
}
}
return this
}
#loadDependency(dependencyId: DependencyId): this {
if (this.#isDependencyWaiting(dependencyId)) {
this.#statusesById[dependencyId] = DependencyStatus.LOADING
this.#getDependency(dependencyId)?.load?.()
this.#statusesById[dependencyId] = DependencyStatus.LOADED
}
return this
}
#unloadDependencyDependents(dependencyId: DependencyId): this {
for (const dependentId of Object.keys(
this.#getDependencyDependents(dependencyId) || {},
)) {
this.#unloadDependencyAndDependents(dependentId)
}
return this
}
#unloadDependency(dependencyId: DependencyId): this {
if (this.#isDependencyLoaded(dependencyId)) {
this.#statusesById[dependencyId] = DependencyStatus.UNLOADING
this.#getDependency(dependencyId)?.destroy?.()
this.#statusesById[dependencyId] = DependencyStatus.WAITING
}
return this
}
//endregion
//region Private Dependency Handlers
#handleAddingDependencyAndDependents(dependency: Dependency): this {
const dependencyId: DependencyId = dependency.id
if (!dependency) throw new Error('Invalid dependency')
if (!dependencyId) throw new Error('Invalid dependencyId')
/**
* Set properties on local dependencies object
*/
this.#setDependencyProperties(dependencyId, dependency)
/**
* Set dependencies' dependent relationships
*/
this.#setDependencyDependents(dependencyId)
/**
* Load applicable dependencies
*/
this.#loadDependencyAndDependents(dependencyId)
return this
}
#loadDependencyAndDependents(dependencyId: DependencyId): this {
if (!this.#hasDependency(dependencyId)) return this
/**
* Verify all dependencies are loaded
*/
if (!this.#areAllDependenciesLoaded(dependencyId)) {
return this
}
/**
* Load the self dependency
*/
this.#loadDependency(dependencyId)
/**
* Load waiting dependents
*/
this.#loadDependencyDependents(dependencyId)
return this
}
#unloadDependencyAndDependents(dependencyId: DependencyId): this {
if (!this.#hasDependency(dependencyId)) return this
/**
* Unload all dependents of the dependency
*/
this.#unloadDependencyDependents(dependencyId)
/**
* Unload self dependency
*/
this.#unloadDependency(dependencyId)
return this
}
#removeDependencyAndDependents(dependencyId: DependencyId): this {
if (!this.#hasDependency(dependencyId)) return this
/**
* Unload dependency and its dependents
*/
this.#unloadDependencyAndDependents(dependencyId)
/**
* Remove dependencies dependent relationships
*/
this.#removeDependencyDependents(dependencyId)
/**
* Remove self from local dependencies property
*/
this.#removeDependencyProperties(dependencyId)
return this
}
//endregion
//region Private Add/Remove
#addDependency(dependency: Dependency): this {
return this.#handleAddingDependencyAndDependents(dependency)
}
#removeDependency(id: DependencyId): this {
return this.#removeDependencyAndDependents(id)
}
//endregion
/**
* Loads a dependency and its dependents. Do not call unless necessary.
* Process flow automatically handles loading/unloading dependency when
* all required dependencies have been loaded for itself. However, if
* called should be ok as it checks if dependencies are loaded first.
* Step 1: Verify all dependencies are loaded Step 2: Load the self
* dependency Step 3: Load waiting dependents
*/
public loadDependency(id: DependencyId): this {
return this.#loadDependencyAndDependents(id)
}
/**
* Unloads a dependency and its dependents. Do not call unless necessary.
* Process flow automatically handles loading/unloading dependency when
* all required dependencies have been unloaded for itself. However, if
* called should be ok as it checks if dependencies are unloaded first.
* Step 1: Unload all dependents of the dependency Step 2: Unload self
* dependency
*/
public unloadDependency(id: DependencyId): this {
return this.#unloadDependencyAndDependents(id)
}
/**
* Check if the dependency has 'waiting' status
*/
public isDependencyWaiting(dependencyId: DependencyId): boolean {
return this.#isDependencyWaiting(dependencyId)
}
/**
* Check if the dependency has 'loading' status
*/
public isDependencyLoading(dependencyId: DependencyId): boolean {
return this.#isDependencyLoading(dependencyId)
}
/**
* Check if the dependency has 'loaded' status
*/
public isDependencyLoaded(dependencyId: DependencyId): boolean {
return this.#isDependencyLoaded(dependencyId)
}
/**
* Check if the dependency has 'unloaded' status
*/
public isDependencyUnloading(dependencyId: DependencyId): boolean {
return this.#isDependencyUnloading(dependencyId)
}
/**
* Get a copy of the local dependency object
*/
public getDependency(
dependencyId: DependencyId,
): Readonly<Dependency> | undefined {
const dependency = this.#getDependency(dependencyId)
return dependency ? { ...dependency } : undefined
}
/**
* Adds a new dependency and if dependencies are loaded it loads, then
* loads checks all waiting dependencies if they can now load as well.
* Step 1: Set properties on local dependencies object Step 2: Set
* dependencies' dependent relationships Step 3: Load dependencies
* waiting with all requirements met
*/
public addDependency(dependency: Dependency): this {
return this.#addDependency(dependency)
}
/**
* Unloads all dependency dependents and then removes the dependency.
* Step 1: Unload dependency and its dependents
* Step 2: Remove dependencies dependent relationships
* Step 3: Remove self from local dependencies property
*/
public removeDependency(id: DependencyId): this {
return this.#removeDependency(id)
}
/**
* Breaks down and removes all dependencies
*/
public destroy() {
this.#destroy()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment