logux/docs

Concept/recipe for bigger data

hcomnetworkers opened this issue · 15 comments

Is it possible to load multiple objects at once? With bigger data and lazy-loading, subscribing to each individual object via an object/:id-like channel would cause too many db-requests - unless we'd debounce it server-side, which would cause additional lag.

Imagine I had to load the first 50 rows of a large table. This might be doable with one channel rows/ids to load all ids, and then another channel like { channel: 'rows/multi', ids: [1, 2, ..., 50] } for the first rows. But then what if the user scrolls down a bit and I need the next 20 rows? React would re-subscribe me with { channel: 'rows/multi', ids: [1, 2, ..., 70] }. This however seems very expensive - the first 50 rows would be loaded again.

Or do you see another way to not load things again upon a re-subscription of a bulk-load-action? The guide talks about the action.since.time, but the server is probably unaware of what data is already loaded.


Another issue is permissions on large data. As long as all users may see all the actions on a channel, things are fine. But what if a channel has to filter the result based on the user's permissions. How can I prevent the resend-actions from only reaching users allowed to see them?

For example, be there a channel openTerminations and user A gets [1, 2, 4], and user B gets [2, 3, 5] with sensitive data inside. User B edits termination 5 via an action and the server is configured to resend to channel openTerminations. How do I prevent user A from getting this action with possible sensitive data inside?


Our current project uses redux, downloads objects only once as long as they don't change and invalidates them via websocket-events. This worked great for us, but it has no offline-capability. That's why we are looking into logux, which seems very promising on this end!!

ai commented

Good question. I like to think of how Logux architecture can fit different use cases. Feel free to ask more of these questions.

We definitely need a good article about it. Short answer:

This however seems very expensive - the first 50 rows would be loaded again.

You can check current client subscription on the server and avoid loading extra rows:

let rows = {}

server.channel('rows', {
  async init (ctx, action) {
    let prevIds = rows[ctx.clientId]
    if (!prevIds) prevIds = rows[ctx.clientId] = []
    let newIds = action.ids.filter(i => !prevIds.includes(i))
    await Promise.all(newIds.map(async id => {
      ctx.sendBack(await loadRow(id))
    }))
    prevIds.push(...newIds)
  }
})

server.on('disconnected', client => {
  if (client.clientId) {
    delete rows[client.clientId]
  }
})

Another issue is permissions on large data.

filter callback in server.channel can help you here:

server.channel('rows', {,
  async filter (ctx, action) {
    let hasAccess = await loadUserRows(ctx.userId)
    return (action, meta) => {
      return hasAccess.includes(action.id)
    }
  }
})

If we have an action with both sensitive and nonsensitive data, the solution is more tricky. You will need to create a new action with nonsensitive data only in server.on('add'.

server.on('add', (action, meta) => {
  if (action.type === 'changePassword') { // this action contains password and can’t be re-send
    server.log.add({ type: 'userChangedPassword', userId: action.userId }) // this action doesn’t have password
  }
})

Do my answers fit your use case?

Thank you, that already helps a lot!

Your guide mentions it tracks subscriptions - is it aware of additional data in the action, like the ids-array in the example here? So one component can load a different set of ids than another?

This hook automatically tracks all subscriptions and doesn’t subscribe to channel if another component already subscribed to the same channel.

ai commented

Your guide mentions it tracks subscriptions - is it aware of additional data in the action, like the ids-array in the example here? So one component can load a different set of ids than another?

Sorry, right now it tracks subscription by JSON.stringify(action). If one component relies to { type: 'logux/subscribe', channel: 'rows', ids: [1, 2] } and other component relies on the same ids: [1, 2], useSubscription will call subscription only once. But if other relies on ids: [1, 2, 3], useSubscription will send { type: 'logux/subscribe', channel: 'rows', ids: [1, 2, 3] } to thre server again.

However, useSubscription is not so compilcated. You can write smarter useRows, which can generate logux/subscribe actions.

Okay, that's fine. Thanks so far!

ai commented

I will keep it open until I will write good guides for this questions

server.channel('rows', {,
  async filter (ctx, action) {
    let hasAccess = await loadUserRows(ctx.userId)
    return (action, meta) => {
      return hasAccess.includes(action.id)
    }
  }
})

Hey, when is this outer filter executed? Every time an action is resent? Or just once upon subscribing? In the latter case: How can this hasAccess be invalidated if the permissions changed?

Furthermore: I think either your JSDoc or your guide is wrong. The JSDoc mentions that the returned filter-function also has a ctx as first argument, but it is missing in the guide and in this quote here.

ai commented

when is this outer filter executed?

On every new action with this channel

How can this hasAccess be invalidated if the permissions changed?

Good question. You can load fresh data from database on every call.

Ok, I don't know I understand the filter.
The filter is supposed to filter to which clients an action is resend, is it not? I tried with a false response, but I still get resends on all clients:

server.channel<GroupsLoad, GroupsAll>('groups/load', {
  async filter(ctx, action) {
    return () => false;
  },
});

Good question. You can load fresh data from database on every call.

And no, the inner filter-function is not async, so I cannot reload from db on every call. But I suppose I can keep an updated synchronous cache outside the scope.

ai commented

filter callback must be sync https://github.com/logux/server/blob/master/base-server.js#L917

We can add an async version too if you know the good use case for it.

ai commented

I am talking about filter creator. I think this could fix filtering:

  server.channel<GroupsLoad, GroupsAll>('groups/load', {
-   async filter(ctx, action) {
+   filter(ctx, action) {
      return () => false;
    },
  });

Ah, yes, and my Promise is thruthy, which is why the resends go through.

But why can't you change line 917:
let filter = i.filter && i.filter(ctx, action, meta)
to
let filter = i.filter && await i.filter(ctx, action, meta)

It's already an async function.

ai commented

Of course, send PR to v0.6

Sorry, I am a noob with forking and pull requests.
It ended up in logux/server#68, too.

ai commented

I am closing it right now. Task about the built-in solution is moved to logux/logux#11