-
Notifications
You must be signed in to change notification settings - Fork 8
Description
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 thecalls
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);
}