Server Extensibility

Our plans for this were to add priorities to packages. There is a small description on the AMP Implementation Proposal: AMP package - #10 by luisherranz

What I am wondering lately is at which level we should add priorities:

  • At a package level.
  • At a function level.

For some of the things, like Html2React processors and Source handlers, we have defined them at a function level. It’s more complex, much also more versatile.

WordPress also has them at a function level for actions (add_action) and filters (add_filter).

We should start a conversation about this :slightly_smiling_face:

The code you mention will work because app is exposed in ctx.

However, I think our recommendation should be to use a normal Frontity middleware, as described in this IP, and await for next() when you need to run something after the SSR.

You are right. Most of the time, when the rendering should be skipped, it would be enough to not call next().

I wonder if there will be sometimes when skipping the React SSR AND keep executing the rest of the middleware (from other packages) will be necessary.

I guess it’s better to wait until we see more real usage of this :slightly_smiling_face:

By the way, the API you describe is not required for this. "Not calling next()" is possible with the normal Frontity middleware functions described in this IP.

Awesome, thanks!

Yes, we will have to change that when we work in this implementation.

If you read my IP draft, that was one of the options explained there (section C). It included the pros and cons of not only that approach but also the alternatives: Server Extensibility - #13 by luisherranz.

It’s not too late to change some parts of the IP, so if you have a strong opinion about that specific part or any other, or you want to point out additional pros/cons of each approach that were not reflected in that draft, please do so :slightly_smiling_face:

2 Likes

Awesome IP @luisherranz :clap:

I’m sure there’s a reason that I missed here, but why do we want to support both signatures?

By the way, good points @cristianbote I also read the Final Implementation first and didn’t realize that priorities were discussed elsewhere and exposing the app was in the draft IP :blush:

I have just realized there is another important reason to use namespaces that I forgot to add to the Proposal Implementation Draft (section B1): The DevTools.

If we use namespaces, we will have much better information for the server DevTools in the future, because after we run each middleware we can report it to the DevTools, the same way we will do with actions:

  • Run server.somePackage.some
    • It mutated state.xxx.counter: 1 → 2
    • It run actions.zzz.toggle(true).
      • It mutated state.zzz.other: false → true
  • Run server.otherPackage.some

Because one is “the Frontity way” and the other is needed to reuse Koa middleware. It is explained in the draft, section D3.

import ratelimit from "koa-ratelimit";

export default {
  // ...
  server: {
    myPackage: {
      myMiddleware: ({ ctx, state }) => {
        // Do stuff with ctx, state and so on...
      },
      rateLimiter: ratelimit({
        // Add koa packages directly.
      }),
    },
  },
};

Ok, took some more time to read through the IP and I am confident that I don’t see my proposal :sweat_smile:. Maybe I didn’t lay it properly. Let me reiterate over it again, in a more structured way.

D4 Scoped app handler

In a middleware based, server framework, the order of the middleware matters. @luisherranz has talked about it more in this Onboarding Session Video. So, in order to let the server be extensible by using a package, we can define a scoped app handler per package:

export default {
  // ...rest of the configuration
  server: {
    myPackage: ({ ctx, app }) => {
      // Configure the app, middleware and such.
      app.silent = true;
      app.on("error", () => {
        // Do something...
      });

      // Package specific middleware
      app.use(/* middleware fn foo */);

      // Package specific middleware
      app.use(get('/api/cart', /* ... */));
    },
  },
};

For a safer multi package execution, each scoped server handler could be called not with the main app instance but with a new one, that would be mounted. A sub-app per se.

Object.values(store.server).forEach(handler => {
  const app = new Koa();
  handler({ ctx, app});

  // Mount the app.
  rootApp.mount(app);
});

Pros:

  • Stays true to the Koa pattern and paradigm. It is after all direct access to app instance.
  • Direct access to an app instance could lead to more experimentations(React Server Components with their custom api, static serve of resources, Etag with a in-memory cache layer, etc).
  • The order of the middleware is defined and owned by the package.
  • Easier mounting of existing node Koa server. They could use their existing server with the predefined middleware, by mounting their previous one:
import previousServer from './legacy/server';

export default {
  // ...rest of the configuration
  server: {
    myPackage: ({ ctx, app }) => {
      // Mount an existing server
      app.use(mount(previousServer));
    },
  },
};

Cons:

  • I believe this is the first deviation of how frontity has previously defined the scoped settings for a given namespace, so needs to be handled and explained in a such manner.

At the end of the day, the most important aspect is to let the server be extendable. I do not hold any variant close to heart. I see them as different ways of achieving the extensibility we desire.

I do lean a bit more on the above D4 as I can see more usages out of it.