Skip to content

Conversation

Copy link

Copilot AI commented Oct 13, 2025

Overview

This PR adds ergonomic subscription lifecycle management by introducing optional AbortSignal support to the subscribe() method, while maintaining full backward compatibility with existing code.

Motivation

In Web Components and dynamic UI contexts, managing subscription cleanup is critical to prevent memory leaks. Currently, developers must manually store and call unsubscribe functions:

class MyElement extends HTMLElement {
  private unsubscribe?: () => void;

  connectedCallback() {
    this.unsubscribe = store.subscribe('path', callback);
  }

  disconnectedCallback() {
    this.unsubscribe?.();  // Easy to forget!
  }
}

Changes

1. Optional AbortSignal Parameter

The subscribe() method now accepts an optional third parameter for automatic cleanup:

subscribe(path: string, cb: Listener, options?: { signal?: AbortSignal }): () => void

Usage:

const controller = new AbortController();

store.subscribe('user.name', (value) => {
  console.log('Name changed:', value);
}, { signal: controller.signal });

// Later: automatically unsubscribe
controller.abort();

2. Implementation Details

  • Already-aborted signals: If the signal is already aborted when subscribing, the subscription is immediately cleaned up
  • Event listener cleanup: Uses { once: true } to prevent memory leaks
  • Cache invalidation: Preserves existing listenerCache invalidation behavior on all code paths
  • Backward compatible: Existing subscribe(path, cb) calls work unchanged

3. Documentation

Added comprehensive "🔄 Lifecycle and cleanup" section to README with:

  • Manual unsubscribe pattern with disconnectedCallback()
  • Modern AbortController pattern for ergonomic cleanup
  • Helper function example for integrating both approaches
  • Clear guidance for Web Component lifecycle management

Web Component Example

class MyElement extends HTMLElement {
  private controller = new AbortController();

  connectedCallback() {
    const store = Proxable.create<{ count: number }>();
    
    // Multiple subscriptions with one controller
    store.subscribe('count', this.onCount, { signal: this.controller.signal });
    store.subscribe('status', this.onStatus, { signal: this.controller.signal });
  }

  disconnectedCallback() {
    // Clean up all subscriptions at once
    this.controller.abort();
  }
}

Testing

Added 4 comprehensive test cases covering:

  • ✅ Automatic unsubscribe when signal is aborted
  • ✅ No subscription activation if signal is pre-aborted
  • ✅ Manual off() works alongside AbortSignal
  • ✅ Backward compatibility without options parameter

All tests pass (14 total: 5 original + 4 new + 1 backward compat).

Breaking Changes

None. This is a fully backward-compatible addition.

Benefits

  • Ergonomic: One controller.abort() cleans up multiple subscriptions
  • Safe: Prevents common memory leaks in component lifecycles
  • Modern: Aligns with Web platform standards (fetch, addEventListener, etc.)
  • Optional: Existing code continues to work without any changes
  • Flexible: Both manual and signal-based patterns can coexist
Original prompt

Implement optional AbortSignal support in subscribe() and document lifecycle cleanup for Web Components.

Scope

  1. Code change: Add an optional { signal?: AbortSignal } parameter to ProxableObject.subscribe and wire it so that when the signal is aborted, the subscription is automatically unsubscribed (i.e., call the existing off()). Keep backward compatibility: existing two-argument subscribe(path, cb) continues to work unchanged.

  2. Documentation: Add a new README section that explains subscription lifetime, shows how to unsubscribe in disconnectedCallback, and shows an ergonomic AbortController-based pattern. Make clear that both approaches can coexist and are recommended for Web Components.

Acceptance criteria

  • Type definitions updated so subscribe(path, cb, options?: { signal?: AbortSignal }) is reflected in the public API (ProxableObject interface) without breaking existing code.
  • Implementation wires the AbortSignal to off(), handling the already-aborted case immediately.
  • listenerCache invalidation remains as before on subscribe and unsubscribe.
  • README includes a “Lifecycle and cleanup” section with:
    • Guidance: Subscriptions live until you unsubscribe; tie them to Web Component lifecycle.
    • Example with manual off() in disconnectedCallback.
    • Example using AbortController/AbortSignal; if the repo version doesn't yet formally expose { signal }, also include a tiny helper that wires off() to signal.
  • Build/tests still pass.

Implementation notes

  • File: src/index.ts

    • Update ProxableObject.subscribe signature to include options?: { signal?: AbortSignal }.
    • In create(), when defining root.subscribe, accept the optional options param. After calling index.add(path, cb) to get off(), if options?.signal is present:
      • If signal.aborted: call off() immediately.
      • Else: addEventListener('abort', off, { once: true }). Consider capturing a stable reference; since we return () => { off(); invalidate cache }, relying on off is fine.
    • Preserve existing listenerCache invalidation after subscribe call and inside returned unsubscribe closure.
  • File: README.md

    • Add a new section “Lifecycle and cleanup” after API Reference or before Benchmarks.
    • Provide two code samples: manual off() pattern, AbortSignal pattern, and a small helper for older usage.

Do not modify other APIs or behavior.

This pull request was created as a result of the following prompt from Copilot chat.

Implement optional AbortSignal support in subscribe() and document lifecycle cleanup for Web Components.

Scope

  1. Code change: Add an optional { signal?: AbortSignal } parameter to ProxableObject.subscribe and wire it so that when the signal is aborted, the subscription is automatically unsubscribed (i.e., call the existing off()). Keep backward compatibility: existing two-argument subscribe(path, cb) continues to work unchanged.

  2. Documentation: Add a new README section that explains subscription lifetime, shows how to unsubscribe in disconnectedCallback, and shows an ergonomic AbortController-based pattern. Make clear that both approaches can coexist and are recommended for Web Components.

Acceptance criteria

  • Type definitions updated so subscribe(path, cb, options?: { signal?: AbortSignal }) is reflected in the public API (ProxableObject interface) without breaking existing code.
  • Implementation wires the AbortSignal to off(), handling the already-aborted case immediately.
  • listenerCache invalidation remains as before on subscribe and unsubscribe.
  • README includes a “Lifecycle and cleanup” section with:
    • Guidance: Subscriptions live until you unsubscribe; tie them to Web Component lifecycle.
    • Example with manual off() in disconnectedCallback.
    • Example using AbortController/AbortSignal; if the repo version doesn't yet formally expose { signal }, also include a tiny helper that wires off() to signal.
  • Build/tests still pass.

Implementation notes

  • File: src/index.ts

    • Update ProxableObject.subscribe signature to include options?: { signal?: AbortSignal }.
    • In create(), when defining root.subscribe, accept the optional options param. After calling index.add(path, cb) to get off(), if options?.signal is present:
      • If signal.aborted: call off() immediately.
      • Else: addEventListener('abort', off, { once: true }). Consider capturing a stable reference; since we return () => { off(); invalidate cache }, relying on off is fine.
    • Preserve existing listenerCache invalidation after subscribe call and inside returned unsubscribe closure.
  • File: README.md

    • Add a new section “Lifecycle and cleanup” after API Reference or before Benchmarks.
    • Provide two code samples: manual off() pattern, AbortSignal pattern, and a small helper for older usage.

Do not modify other APIs or behavior.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits October 13, 2025 20:48
Co-authored-by: ReinsBrain <1740167+ReinsBrain@users.noreply.github.com>
Co-authored-by: ReinsBrain <1740167+ReinsBrain@users.noreply.github.com>
Copilot AI changed the title [WIP] Add optional AbortSignal support to subscribe method Add optional AbortSignal support to subscribe() and document lifecycle cleanup for Web Components Oct 13, 2025
Copilot AI requested a review from ReinsBrain October 13, 2025 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants