TriPSs / nestjs-query

Easy CRUD for GraphQL.
https://tripss.github.io/nestjs-query/
MIT License
161 stars 43 forks source link

Subscription clean up #302

Open coupster74 opened 1 month ago

coupster74 commented 1 month ago

more of a question...

in the implementation of subscriptions, is there a mechanism for pruning old subscriptions? While front ends SHOULD unsubscribe, often they do not.

Here is some rough code from chatGPT, but short of reimplementing subscriptions, I don't quite see how to integrate into the nestjs-query framework.

import { Resolver, Subscription } from '@nestjs/graphql';
import { PubSub } from 'graphql-subscriptions';
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

const pubSub = new PubSub();

@Injectable()
@Resolver()
export class SubscriptionsResolver implements OnModuleInit, OnModuleDestroy {
  private activeSubscriptions = new Map<string, { lastActive: Date }>();
  private readonly CLEANUP_INTERVAL = 60000; // 1 minute
  private readonly STALE_TIMEOUT = 300000; // 5 minutes
  private cleanupIntervalId: NodeJS.Timeout;

  onModuleInit() {
    // Start the periodic cleanup task
    this.cleanupIntervalId = setInterval(() => this.cleanUpStaleSubscriptions(), this.CLEANUP_INTERVAL);
  }

  onModuleDestroy() {
    // Clear the cleanup task when the module is destroyed
    clearInterval(this.cleanupIntervalId);
  }

  @Subscription(() => String, {
    resolve: (payload) => payload.message,
    filter: (payload, variables, context) => {
      // Optionally, apply filtering logic based on the payload or context
      return true;
    },
  })
  subscribeToMessage() {
    const subscriptionId = this.generateSubscriptionId(); // Define how you want to generate subscription IDs
    this.trackSubscription(subscriptionId);
    return pubSub.asyncIterator('message');
  }

  // Track subscriptions with timestamps
  private trackSubscription(subscriptionId: string) {
    const now = new Date();
    this.activeSubscriptions.set(subscriptionId, { lastActive: now });
    console.log(`Subscription ${subscriptionId} added`);
  }

  // Mark a subscription as active when an event is received
  private updateSubscriptionActivity(subscriptionId: string) {
    const now = new Date();
    if (this.activeSubscriptions.has(subscriptionId)) {
      this.activeSubscriptions.get(subscriptionId).lastActive = now;
      console.log(`Subscription ${subscriptionId} updated`);
    }
  }

  // Clean up stale subscriptions that haven't been active for a while
  private cleanUpStaleSubscriptions() {
    const now = new Date();
    this.activeSubscriptions.forEach((value, subscriptionId) => {
      if (now.getTime() - value.lastActive.getTime() > this.STALE_TIMEOUT) {
        // Unsubscribe from stale subscription
        this.activeSubscriptions.delete(subscriptionId);
        console.log(`Subscription ${subscriptionId} removed due to inactivity`);
      }
    });
  }

  // Simulate message publishing for testing
  publishMessage() {
    pubSub.publish('message', { message: 'Hello World' });
  }

  // Dummy function for generating subscription IDs
  private generateSubscriptionId(): string {
    return Math.random().toString(36).substring(2, 15);
  }
}

The periodic task is set up using setInterval in the onModuleInit method to run every 1 minute (CLEANUP_INTERVAL). The cleanUpStaleSubscriptions() method removes any subscriptions that have been inactive for more than the timeout (STALE_TIMEOUT).

This code provides a basic implementation of tracking subscriptions and cleaning up stale ones. You can adjust the timeout intervals or expand this logic to handle more advanced cases, like managing user-specific subscriptions or more complex payloads.

so.. is something implemented that does this already in the framework, and if not, is there an elegant approach you could recommend? Or should this become a feature request?

TriPSs commented 1 month ago

I do not use (or know) how the subscriptions part of this lib works as I do not use it.

However, in my own API's we do have subscriptions (Nestjs + Mercurius) and as far as I know even though the frontend does not explicitly unsubscribe it knows when a subscription disconnects and then cleans it up, even if this is not the case in our API at-least subscriptions expire after around 30 minutes and then need to reconnect, so worst case scenario it has an old subscription for max 30 minutes before it's removed.