or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

application-engine.mdcomponents.mdcontrollers.mddata-structures.mddebugging-development.mddestroyables-cleanup.mdindex.mdmodifiers.mdobject-model.mdreactivity-tracking.mdrouting.mdservices.mdtemplates-rendering.mdtesting.mdutilities.md
tile.json

destroyables-cleanup.mddocs/

Destroyables & Cleanup

Resource management system for automatic cleanup of objects and prevention of memory leaks.

Capabilities

Destruction Management

Core functions for managing object destruction and cleanup lifecycle.

/**
 * Destroy an object and run all registered destructors
 * @param destroyable - Object to destroy
 */
function destroy(destroyable: object): void;

/**
 * Check if an object is currently being destroyed
 * @param destroyable - Object to check
 * @returns Whether object is in destruction process
 */
function isDestroying(destroyable: object): boolean;

/**
 * Check if an object has been completely destroyed
 * @param destroyable - Object to check
 * @returns Whether object is destroyed
 */
function isDestroyed(destroyable: object): boolean;

Usage Examples:

import { destroy, isDestroying, isDestroyed } from "@ember/destroyable";
import Component from "@glimmer/component";

export default class WebSocketComponent extends Component {
  socket = null;
  
  constructor(owner, args) {
    super(owner, args);
    this.setupWebSocket();
  }
  
  setupWebSocket() {
    this.socket = new WebSocket('ws://localhost:8080');
    
    this.socket.onmessage = (event) => {
      // Only process messages if component not destroyed
      if (!isDestroyed(this)) {
        this.handleMessage(JSON.parse(event.data));
      }
    };
  }
  
  handleMessage(data) {
    // Check if destruction is in progress
    if (isDestroying(this)) {
      console.log('Component destroying, ignoring message');
      return;
    }
    
    // Process message
    this.args.onMessage?.(data);
  }
  
  willDestroy() {
    super.willDestroy();
    
    // Manual cleanup
    if (this.socket) {
      this.socket.close();
      this.socket = null;
    }
  }
}

// Programmatic destruction
const component = SomeComponent.create();
// ... use component
destroy(component); // Triggers cleanup
console.log(isDestroyed(component)); // true

Destructor Registration

System for registering cleanup functions that run when objects are destroyed.

/**
 * Register a destructor function to run when object is destroyed
 * @param destroyable - Object to register destructor for
 * @param destructor - Function to run on destruction
 * @param eager - Whether to run destructor eagerly (before other cleanup)
 */
function registerDestructor(destroyable: object, destructor: (destroyable: object) => void, eager?: boolean): void;

/**
 * Unregister a previously registered destructor
 * @param destroyable - Object the destructor was registered for
 * @param destructor - Destructor function to unregister
 */
function unregisterDestructor(destroyable: object, destructor: (destroyable: object) => void): void;

Usage Examples:

import { registerDestructor, unregisterDestructor } from "@ember/destroyable";
import Service from "@ember/service";

export default class DatabaseService extends Service {
  connections = new Map();
  
  init() {
    super.init();
    
    // Register cleanup for all connections
    registerDestructor(this, (service) => {
      console.log('Cleaning up database connections...');
      service.connections.forEach(connection => {
        connection.close();
      });
      service.connections.clear();
    });
  }
  
  createConnection(name, config) {
    const connection = new DatabaseConnection(config);
    this.connections.set(name, connection);
    
    // Register destructor for individual connection
    const connectionDestructor = () => {
      console.log(`Closing connection: ${name}`);
      connection.close();
      this.connections.delete(name);
    };
    
    registerDestructor(this, connectionDestructor);
    
    // Return function to manually close connection
    return () => {
      unregisterDestructor(this, connectionDestructor);
      connectionDestructor();
    };
  }
}

// Component with resource cleanup
export default class TimerComponent extends Component {
  timerId = null;
  
  constructor(owner, args) {
    super(owner, args);
    this.startTimer();
    
    // Register cleanup for timer
    registerDestructor(this, (component) => {
      if (component.timerId) {
        clearInterval(component.timerId);
        component.timerId = null;
      }
    });
  }
  
  startTimer() {
    this.timerId = setInterval(() => {
      if (!isDestroyed(this)) {
        this.args.onTick?.();
      }
    }, 1000);
  }
}

Hierarchical Cleanup

System for associating child objects with parents for automatic cleanup.

/**
 * Associate a child destroyable with a parent for automatic cleanup
 * When parent is destroyed, child will be destroyed too
 * @param parent - Parent object
 * @param child - Child object to associate
 */
function associateDestroyableChild(parent: object, child: object): void;

Usage Examples:

import { associateDestroyableChild } from "@ember/destroyable";
import EmberObject from "@ember/object";

export default class ParentService extends Service {
  children = new Set();
  
  createChild(config) {
    const child = ChildObject.create(config);
    this.children.add(child);
    
    // Associate child with parent for automatic cleanup
    associateDestroyableChild(this, child);
    
    return child;
  }
  
  createWorker(taskConfig) {
    const worker = new Worker('worker.js');
    const workerWrapper = EmberObject.create({
      worker,
      
      postMessage(data) {
        this.worker.postMessage(data);
      },
      
      terminate() {
        this.worker.terminate();
      }
    });
    
    // Auto-cleanup worker when service is destroyed
    associateDestroyableChild(this, workerWrapper);
    
    // Register destructor for worker cleanup
    registerDestructor(workerWrapper, (wrapper) => {
      wrapper.worker.terminate();
    });
    
    return workerWrapper;
  }
}

// Factory pattern with automatic cleanup
class ResourceFactory {
  static createResource(parent, type, config) {
    let resource;
    
    switch (type) {
      case 'database':
        resource = DatabaseResource.create(config);
        break;
      case 'cache':
        resource = CacheResource.create(config);
        break;
      case 'queue':
        resource = QueueResource.create(config);
        break;
      default:
        throw new Error(`Unknown resource type: ${type}`);
    }
    
    // Associate with parent for cleanup
    associateDestroyableChild(parent, resource);
    
    return resource;
  }
}

export default class ApplicationService extends Service {
  init() {
    super.init();
    
    // Create resources that will be auto-cleaned up
    this.database = ResourceFactory.createResource(this, 'database', {
      host: 'localhost',
      port: 5432
    });
    
    this.cache = ResourceFactory.createResource(this, 'cache', {
      maxSize: 1000
    });
    
    this.queue = ResourceFactory.createResource(this, 'queue', {
      maxJobs: 10
    });
  }
}

Advanced Cleanup Patterns

Advanced patterns for complex cleanup scenarios.

/**
 * Enable eager destruction (immediate cleanup) for an object
 * @param destroyable - Object to enable eager destruction for
 */
function enableDestroyableTracking(destroyable: object): void;

/**
 * Check if destruction tracking is enabled for an object
 * @param destroyable - Object to check
 * @returns Whether tracking is enabled
 */
function isDestroyableTrackingEnabled(destroyable: object): boolean;

Usage Examples:

import { 
  registerDestructor, 
  associateDestroyableChild,
  enableDestroyableTracking 
} from "@ember/destroyable";

// Resource pool with automatic cleanup
export default class ResourcePool extends Service {
  pool = [];
  activeResources = new Set();
  
  init() {
    super.init();
    
    // Enable tracking for immediate cleanup feedback
    enableDestroyableTracking(this);
    
    // Register pool cleanup
    registerDestructor(this, (pool) => {
      console.log(`Cleaning up resource pool with ${pool.activeResources.size} active resources`);
      
      // Force cleanup of all active resources
      pool.activeResources.forEach(resource => {
        resource.forceCleanup();
      });
      
      pool.pool.length = 0;
      pool.activeResources.clear();
    }, true); // Eager cleanup
  }
  
  acquireResource() {
    let resource = this.pool.pop();
    
    if (!resource) {
      resource = this.createResource();
    }
    
    this.activeResources.add(resource);
    
    // Auto-cleanup when parent is destroyed
    associateDestroyableChild(this, resource);
    
    return resource;
  }
  
  releaseResource(resource) {
    this.activeResources.delete(resource);
    
    if (resource.isReusable() && !isDestroyed(this)) {
      resource.reset();
      this.pool.push(resource);
    } else {
      destroy(resource);
    }
  }
  
  createResource() {
    const resource = ResourceObject.create({
      pool: this,
      
      forceCleanup() {
        this.cleanup();
        this.pool = null;
      }
    });
    
    // Register individual resource cleanup
    registerDestructor(resource, (res) => {
      if (res.pool) {
        res.pool.activeResources.delete(res);
      }
      res.cleanup();
    });
    
    return resource;
  }
}

Memory Leak Prevention

Utilities and patterns for preventing common memory leaks.

/**
 * Create a weak reference that doesn't prevent garbage collection
 * @param target - Object to create weak reference to
 * @returns Weak reference wrapper
 */
function createWeakRef(target: object): WeakRef<object>;

/**
 * Register callback to run when object is garbage collected
 * @param target - Object to watch
 * @param callback - Function to run on GC
 * @returns FinalizationRegistry entry
 */
function onGarbageCollect(target: object, callback: () => void): any;

Usage Examples:

// Preventing circular references with weak references
export default class EventBus extends Service {
  listeners = new Map();
  weakRefs = new Map();
  
  subscribe(target, eventName, callback) {
    // Create weak reference to prevent memory leaks
    const weakRef = new WeakRef(target);
    const listenerId = Math.random().toString(36);
    
    const listener = {
      id: listenerId,
      eventName,
      callback,
      target: weakRef
    };
    
    if (!this.listeners.has(eventName)) {
      this.listeners.set(eventName, new Set());
    }
    
    this.listeners.get(eventName).add(listener);
    this.weakRefs.set(listenerId, listener);
    
    // Cleanup when target is destroyed
    registerDestructor(target, () => {
      this.unsubscribe(listenerId);
    });
    
    return listenerId;
  }
  
  emit(eventName, data) {
    const listeners = this.listeners.get(eventName);
    if (!listeners) return;
    
    // Clean up dead weak references
    const deadListeners = [];
    
    listeners.forEach(listener => {
      const target = listener.target.deref();
      
      if (target && !isDestroyed(target)) {
        listener.callback.call(target, data);
      } else {
        deadListeners.push(listener);
      }
    });
    
    // Remove dead listeners
    deadListeners.forEach(listener => {
      listeners.delete(listener);
      this.weakRefs.delete(listener.id);
    });
  }
  
  unsubscribe(listenerId) {
    const listener = this.weakRefs.get(listenerId);
    if (listener) {
      const listeners = this.listeners.get(listener.eventName);
      if (listeners) {
        listeners.delete(listener);
      }
      this.weakRefs.delete(listenerId);
    }
  }
}

Types

interface Destroyable {
  /** Whether object is being destroyed */
  isDestroying?: boolean;
  
  /** Whether object is destroyed */
  isDestroyed?: boolean;
  
  /** Cleanup method (optional) */
  destroy?(): void;
  
  /** Pre-destruction hook (optional) */
  willDestroy?(): void;
}

interface DestructorFunction {
  /** Function called when object is destroyed */
  (destroyable: object): void;
}

interface DestroyableChild extends Destroyable {
  /** Parent destroyable object */
  parent?: Destroyable;
}

interface ResourceWrapper {
  /** Wrapped resource */
  resource: any;
  
  /** Cleanup function */
  cleanup(): void;
  
  /** Whether resource is reusable */
  isReusable(): boolean;
  
  /** Reset resource for reuse */
  reset(): void;
}