Skip to content

Version 2 RFC #125

@DaddyWarbucks

Description

@DaddyWarbucks

I have whipped up a new version on this package to add some features and solve some shortcomings of the current version. One of my main motivations in doing so is how well this package can work with the new version of feathers-dataloader. There is now good reason to use this package on the server because it allows sharing a dataloader across all batched services.

Problem

Server

  • The batch service only accepts ['create', 'users', ...] arguments in the calls property. This means that to use it serverside, the developer has has to manually convert their calls to this syntax.
  • The batch service converts all errors to JSON. When using this serverside, it should use real/original errors.

Client

  • Lack of control over who/what/when services are batched.
  • Batches cannot be deduped

Solution

Server

1 - The batch service now accepts an array of promises and/or JSON config. This makes it much easier for the developer to use on the server.

// server.js

// Returns Promise.allSettled() like results with JSON errors. But now it takes service promises too.
const results = await app.service('batch').create({
  calls: [
    app.service('users').get(1),
    app.service('posts').find(),
    // Still supports JSON args
    ['get', 'users', ...]
  ]
});

2- Two convenience methods have been added that re-parse errors and shorten/familiarize the syntax. service.all() and service.allSettled()

// server.js

// Returns Promise.all() like results, but throws with a real error
const results = await app.service('batch').all([
  app.service('users').get(1),
  app.service('posts').find(),
   // Still supports JSON args
   ['get', 'users', ...]
]);

// Returns Promise.allSettled() like results with real errors
const results = await app.service('batch').allSettled([
  app.service('users').get(1),
  app.service('posts').find(),
   // Still supports JSON args
   ['get', 'users', ...]
]);

Client

There are 3 new solutions here to offer the developer more control over the batching process. 2 of them use a BatchManager. The third does not use the BatchManger and is a very explicit way to create batches easily.

1 - Manually create batches. This batchMethods plugin does not attempt to capture batches in a BatchManger, instead it simply adds two methods to the client side batch service to make it easier to use. This is super low commitment from the developer and they don't have to worry about excluding services, skipping batch calls, timeouts, etc. Just a convenient way to make batches. But I don't love this...the whole service callback thing is weird.

// client.js
const { batchMethods } = require('feathers-batch/client')

app.configure(batchMethods({ batchService: 'batch' }));

// `service` is a class that DUCKS like a regular service, but actually returns JSON args
// Returns Promise.all() like results, but throws with a real error
app.service('batch').all((service) => {
  return [
   service('users').get(1),
   service('posts').find(),
   // Still supports JSON args
   ['get', 'users', ...]
  ]
});

// `service` is a class that DUCKS like a regular service, but actually returns JSON args
// Returns Promise.allSettled() like results with real errors
app.service('batch').allSettled((service) => {
  return [
   service('users').get(1),
   service('posts').find(),
   // Still supports JSON args
   ['get', 'users', ...]
  ]
});

2 - Rather than a plugin, the developer can use the batchHook directly. This allows them to use different BatchManager with different timeouts/configs across multiple services or even multiple methods. This is the most powerful solution, but also the most tedious to setup. Especially because many developers may not "set up" all of their remote services. This does not automatically add batchMethods, you would still need to use that plugin if you want those methods. This can be paired with batchClient because the hooks will override the batchClient's config, so it's nice to use both.

const { batchHook } = require('feathers-batch/client')

const batchHook1 = batchHook({ batchService: 'batch', timeout: 25 });
const batchHook1 = batchHook({ batchService: 'batch', timeout: 100 });

app.service('users').hooks({
   before: {
     get: [batchHook1],
     create: [batchHook2]
   }
});

3 - This is a rewrite of the batchClient to solve its main issue. Which was that it used an app.before.all hook. Instead the plugin now uses app.mixins to modify each service's methods to use the BatchManager. This automatically places the batching mechanism in the right place so that all clientside hooks work as expected. This does not automatically add batchMethods, you would still need to use that plugin if you want those methods. This can be paired with batchHook. The hooks will override this manager.

// client.js
const { batchClient } = require('feathers-batch/client')

app.configure(batchClient({ batchService: 'batch' }));

// Automatically batched, same as the old version
app.service('users').get(1);
app.service('posts').find();

// You can also manually skip batches now
app.service('users').get(1, { batch: false });
app.service('posts').find({ batch: false });
  • Deduping - The client now dedupes requests to the server.
// client.js

// Automatically deduped
app.service('users').get(1);
app.service('users').get(1);
  • This new client rewrite also opens the door for socket users to gain value from it as well. Previously this was not advantageous for socket users because sockets are already fast and this could actually causes a slowdown as you wait for the timeout. But, now you can mix and match config to make this more advantageous by sharing loaders on the server. This is particularly helpful in hooks and resolvers as well.
// Disable batching unless a batchManager is explicitly passed.
const disableBatch =  (ctx) => {
  if (!ctx.params.batchManager) {
    ctx.params.batch = false;
  }
  return context;
}

app.hooks({
  before: {
    all: [disableBatch]
  }
});

// Now I can use custom one off managers

const properties = {
  user: (result, context) => {
      const { batchManger } = context.params;
      return context.app.get(result.userId, { batchManger  });
  },
  comments: (result, context) => {
      const { batchManger } = context.params;
      return context.app.get(result.commentIds, { batchManger  });
  }
}

const myResolveHook = context => {
  const batchManager = new BatchManager({ batchService: 'batch', timeout: 0.01 });
  const ctx = {
    ...context,
    params: {
       ...context.params,
       batchManager
    }
  }
  return resolve(properties, ctx);
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions