npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

dcrf-client

v1.1.0

Published

Websocket client for Django Channels v2 APIs powered by hishnash/djangochannelsrestframework

Downloads

109

Readme

dcrf-client

npm version

This package aims to provide a simple, reliable, and generic interface to consume Django Channels REST Framework powered WebSocket APIs.

NOTE: This library is a TypeScript port of channels-api-client to support Django Channels v2, and @hishnash's port of linuxlewis's channels-api: djangochannelsrestframework and channelsmultiplexer.

Features

  • Promises encapsulating the request/response cycle
  • Subscribe to updates with a callback
  • Automatically reconnect when connection is broken (with backoff — thanks to reconnecting-websocket)
  • Automatically restart subscriptions on reconnection
  • Requests are queued until a connection is made (no need to wait for connection before sending requests)

Install

npm install dcrf-client

Usage

const dcrf = require('dcrf-client');
const client = dcrf.connect('wss://example.com');

client.create('people', {name: 'Alex'}).then(person => {
  console.info('Created:', person);
});

client.retrieve('people', 4).then(person => {
  console.info('Retrieved person 4:', person);
});

client.update('people', 4, {name: 'Johannes', address: '123 Easy St'}).then(person => {
  console.info('Overwrote person 4. Properties after change:', person);
});

client.patch('people', 4, {name: 'Jefe'}).then(person => {
  console.info('Changed name of person 4. Properties after change:', person);
});

client.delete('people', 4).then(() => {
  console.info('Deleted person 4. No one liked them, anyway :)');
});


// Subscribe to updates to person 1
const personalSubscription = client.subscribe('people', 1, (person, action) => {
  if (action === 'update') {
    console.info('Person 1 was updated:', person);
  }
  else if (action === 'delete') {
    console.info('Person 1 was deleted!');
  }
});

// Stop listening for updates
personalSubscription.cancel();


// Make a generic request to a multiplexer stream
client.request('mystream', {key: 'value'}).then(response => {
  console.info('Got mystream response, yo:', response);
});

// Subscribe using a custom action
const customSubscription = client.subscribe(
  'people',
  {},  // Additional arguments may be passed to action
  (person, action) => {
    if (action === 'create') {
      console.info(`Person ${person.pk} was created:`, person);
    }
    else if (action === 'update') {
      console.info(`Person ${person.pk} was updated:`, person);
    }
    else if (action === 'delete') {
      console.info(`Person ${person.pk} was deleted!`);
    }
  },
  {
    includeCreateEvents: true,
    subscribeAction: 'subscribe_all',
    unsubscribeAction: 'unsubscribe_all',
  },
);

Configuration

The client can be customized by passing an object as the second argument to connect() or createClient(). The available options are described below.

const dcrf = require('dcrf-client');

const client = dcrf.connect('wss://example.com', {
  /**
   * Options to pass along to ReconnectingWebsocket
   *
   * See https://github.com/pladaria/reconnecting-websocket#available-options for more info
   */
  websocket: {
    WebSocket?: any; // WebSocket constructor, if none provided, defaults to global WebSocket
    maxReconnectionDelay?: number; // max delay in ms between reconnections
    minReconnectionDelay?: number; // min delay in ms between reconnections
    reconnectionDelayGrowFactor?: number; // how fast the reconnection delay grows
    minUptime?: number; // min time in ms to consider connection as stable
    connectionTimeout?: number; // retry connect if not connected after this time, in ms
    maxRetries?: number; // maximum number of retries
    maxEnqueuedMessages?: number; // maximum number of messages to buffer until reconnection
    startClosed?: boolean; // start websocket in CLOSED state, call `.reconnect()` to connect
    debug?: boolean; // enables debug output
  },

  /**
   * Name of serializer field is used to identify objects in subscription event payloads.
   *
   * Default: 'pk'
   */
  pkField: 'id',

  /**
   * Whether to ensure subscription delete event payloads store the primary key of the object
   * in the configured `pkField`, instead of the default 'pk'.
   *
   * Because subscription delete payloads aren't run through the configured serializer (as the
   * objects do not exist), the DCRF backend must pick a field to store the primary key of the
   * object in the payload. DCRF chooses 'pk' for this field. If `pkField` is *not* 'pk' (and is
   * instead, say, 'id'), then subscription update payloads will return `{id: 123}`, but delete
   * payloads will return `{pk: 123}`.
   *
   * To address the potential inconsistencies between subscription update and delete payloads,
   * setting this option to true (default) will cause dcrf-client to replace the 'pk' field with
   * the configured `pkField` setting.
   *
   * Default: true
   */
  ensurePkFieldInDeleteEvents: true,

  /**
   * Customizes the format of a multiplexed message to be sent to the server.
   *
   * In almost all circumstances, the default behaviour is usually desired.
   *
   * The default behaviour is reproduced here.
   */
  buildMultiplexedMessage(stream: string, payload: object): object {
    return {stream, payload};
  },

  /**
   * Customizes the selector (a pattern matching an object) for the response to an API request
   *
   * In almost all circumstances, the default behaviour is usually desired.
   *
   * The default behaviour is reproduced here.
   */
  buildRequestResponseSelector(stream: string, requestId: string): object {
    return {
      stream,
      payload: {request_id: requestId},
    };
  },

  /**
   * Customizes the selector (a pattern matching an object) matching a subscription update event for
   * an object.
   *
   * In almost all circumstances, the default behaviour is usually desired.
   *
   * The default behaviour is reproduced here.
   */
  buildSubscribeUpdateSelector(stream: string, pk: number, requestId: string): object {
    return {
      stream,
      payload: {
        action: 'update',
        data: {[this.pkField]: pk},
        request_id: requestId,
      },
    };
  },

  /**
   * Customizes the selector (a pattern matching an object) matching a subscription delete event for
   * an object.
   *
   * In almost all circumstances, the default behaviour is usually desired.
   *
   * The default behaviour is reproduced here.
   */
  buildSubscribeDeleteSelector(stream: string, pk: number, requestId: string): object {
    return {
      stream,
      payload: {
        action: 'delete',
        data: {pk},
        request_id: requestId,
      },
    };
  },

  /**
   * Customizes the payload sent to begin subscriptions
   *
   * In almost all circumstances, the default behaviour is usually desired.
   *
   * The default behaviour is reproduced here.
   */
  buildSubscribePayload(pk: number, requestId: string): object {
    return {
      action: 'subscribe_instance',
      request_id: requestId,
      pk,  // NOTE: the subscribe_instance action REQUIRES the literal argument `pk`.
           //       this argument is NOT the same as the ID field of the model.
    };
  },

  preprocessPayload: (stream, payload, requestId) => {
    // Modify payload any way you see fit, before it's sent over the wire
    // For instance, add a custom authentication token:
    payload.token = '123';
    // Be sure not to return anything if you modify payload

    // Or, you can overwrite the payload by returning a new object:
    return {'this': 'is my new payload'};
  },

  preprocessMessage: (message) => {
    // The "message" is the final value which will be serialized and sent over the wire.
    // It includes the stream and the payload.

    // Modify the message any way you see fit, before its sent over the wire.
    message.token = 'abc';
    // Don't return anything if you modify message

    // Or, you can overwrite the the message by returning a new object:
    return {stream: 'creek', payload: 'craycrayload'};
  },
});

Development

There are two main test suites: unit tests (in test/test.ts) to verify intended behaviour of the client, and integration tests (in test/integration/tests/test.ts) to verify the client interacts with the server properly.

Both suites utilize Mocha as the test runner, though the integration tests are executed through py.test, to provide a live server to make requests against.

The integration tests require separate dependencies. To install them, first install pipenv, then run pipenv install --dev.

To run both test suites: npm run test

To run unit tests: npm run test:unit or mocha

To run integration tests: npm run test:integration or pipenv run py.test

How do the integration tests work?

pytest provides a number of hooks to modify how tests are collected, executed, and reported. These are utilized to discover tests from a Mocha suite, and execute them on pytest's command.

Our pytest-mocha plugin first spawns a subprocess to a custom Mocha runner, which collects its own TypeScript-based tests and emits that test info in JSON format to stdout. pytest-mocha reads this info and reports it to pytest, allowing pytest to print out the true names from the Mocha suite. Using deasync, the Mocha process waits for pytest-mocha to send an acknowledgment (a newline) to stdin before continuing.

pytest-mocha then spins up a live Daphne server for the tests to utilize. Before each test, the Mocha suite emits another JSON message informing pytest-mocha which test is about to run. pytest-mocha replies with the connection info in JSON format to the Mocha runner's stdin. The Mocha suite uses this to initialize a DCRFClient for each test.

At the end of each test, our custom Mocha runner emits a "test ended" message. pytest-mocha then wipes the database (with the help of pytest-django) for the next test run.

(Note that technically, Mocha's "test end" event is somewhat misleading, and isn't used directly to denote test end. Mocha's "test end" demarcates when the test method has completed, but not any afterEach hooks. Since we use an afterEach hook to unsubscribe all subscriptions from the DCRFClient, care must be taken to ensure the DB remains unwiped and test server remains up until the afterEach hook has culminated. To this end, we actually emit our "test ended" message right before the next test starts, or the suite ends. See mochajs/mocha#1860. The logic is inspired by the workaround used in JetBrains's mocha-intellij)

NOTE: this is sorta complicated and brittle. it would be nice to refactor this into something more robust. at least for now it provides some assurance the client interacts with the server properly, and also sorta serves as an example for properly setting up a Django Channels REST Framework project.