refactor: mixin

This commit is contained in:
cha0s 2024-01-05 15:52:29 -06:00
parent 37d9a22479
commit c8a6f68628
28 changed files with 164 additions and 91 deletions

View File

@ -29,7 +29,6 @@
- [ ] redux store provider fails if not in request
- [ ] add building to publish process ...
- [ ] @babel/register@7.18.x has a bug
- [ ] `$flecks/db/sequelize` should be `$flecks/db.sequelize`
- [x] `url()` in styles breaks HMR
- [x] 2 underscores for `FLECKS_ENV` variables
- [x] flecks add

View File

@ -128,8 +128,8 @@ export default class ServerFlecks extends Flecks {
const entries = Object.keys(resolver).map((path) => [path, R(this.resolve(resolver, path))]);
// Flecks mixins.
const mixins = entries.map(([, M]) => M.hooks?.['@flecks/core.mixin']).filter((e) => e);
const Class = compose(...mixins)(ServerFlecks);
return new Class({
const MixedServerFlecks = compose(...mixins)(ServerFlecks);
return new MixedServerFlecks({
config,
flecks: Object.fromEntries(entries),
platforms,

View File

@ -50,7 +50,7 @@ export async function createDatabaseConnection(flecks) {
});
}
}
const Models = flecks.get('$flecks/db.models')[ByType];
const Models = flecks.db.Models[ByType];
await register(Models, sequelize);
debug('synchronizing...');
await sequelize.sync();

View File

@ -44,21 +44,32 @@ export const hooks = {
}),
'@flecks/core.hmr.gathered': (gathered, hook, flecks) => {
if ('@flecks/db/server.models' === hook) {
register(gathered, flecks.get('$flecks/db/sequelize'));
register(gathered, flecks.db.sequelize);
}
},
'@flecks/core.mixin': (Flecks) => (
class FlecksWithDb extends Flecks {
db = {
Models: {},
$$sequelize: undefined,
get sequelize() {
return this.$$sequelize;
},
set sequelize(sequelize) {
this.$$sequelize = sequelize;
this.transaction = sequelize.transaction.bind(sequelize);
},
transaction: () => {},
}
}
),
'@flecks/core.starting': (flecks) => {
flecks.set('$flecks/db.models', flecks.gather(
'@flecks/db/server.models',
{typeProperty: 'name'},
));
flecks.db.Models = flecks.gather('@flecks/db/server.models', {typeProperty: 'name'});
},
'@flecks/docker.containers': containers,
'@flecks/server.up': async (flecks) => {
flecks.set('$flecks/db/sequelize', await createDatabaseConnection(flecks));
flecks.db.sequelize = await createDatabaseConnection(flecks);
},
'@flecks/repl.context': (flecks) => ({
Models: flecks.get('$flecks/db.models'),
sequelize: flecks.get('$flecks/db/sequelize'),
}),
};

View File

@ -2,6 +2,8 @@ import {join} from 'path';
import {banner} from '@flecks/core/server';
const electron = __non_webpack_require__('electron');
const {
NODE_ENV,
} = process.env;
@ -9,7 +11,7 @@ const {
let win;
async function createWindow(flecks) {
const {BrowserWindow} = flecks.get('$flecks/electron');
const {BrowserWindow} = flecks.electron;
const {browserWindowOptions} = flecks.get('@flecks/electron/server');
win = new BrowserWindow(browserWindowOptions);
await flecks.invokeSequentialAsync('@flecks/electron/server.window', win);
@ -69,6 +71,13 @@ export const hooks = {
}
}
},
'@flecks/core.mixin': (Flecks) => (
class FlecksWithElectron extends Flecks {
electron = electron.app ? electron : undefined;
}
),
'@flecks/electron/server.initialize': async (electron, flecks) => {
electron.app.on('window-all-closed', () => {
const {quitOnClosed} = flecks.get('@flecks/electron/server');
@ -121,14 +130,6 @@ export const hooks = {
},
}),
'@flecks/server.up': async (flecks) => {
// Local require because electron is kinda skittish.
// eslint-disable-next-line global-require
const electron = require('electron');
// `electron.app` will be undefined if we aren't running in an electron environment. Just bail.
if (!electron.app) {
return;
}
flecks.set('$flecks/electron', electron);
await flecks.invokeSequentialAsync('@flecks/electron/server.initialize', electron);
await flecks.invokeSequentialAsync('@flecks/electron/server.initialize', flecks.electron);
},
};

View File

@ -4,8 +4,7 @@ export default (flecks, [name, Packet]) => {
constructor(...args) {
super(...args);
const {[name]: limiter} = flecks.get('$flecks/governor.packet.limiters');
this.limit = limiter;
this.limit = flecks.governor.packet[name];
}
static async validate(packet, socket) {

View File

@ -24,12 +24,18 @@ export const hooks = {
ttl: 30,
},
}),
'@flecks/core.mixin': (Flecks) => (
class FlecksWithGovernor extends Flecks {
governor = {}
}
),
'@flecks/db/server.models': Flecks.provide(require.context('./models', false, /\.js$/)),
'@flecks/web/server.request.route': (flecks) => {
const {web} = flecks.get('@flecks/governor/server');
const limiter = flecks.get('$flecks/governor.web.limiter');
return async (req, res, next) => {
const {Ban} = flecks.get('$flecks/db.models');
const {Ban} = flecks.db.Models;
try {
await Ban.check(req);
}
@ -43,7 +49,7 @@ export const hooks = {
res.status(403).send(`<pre>${Ban.format([ban])}</pre>`);
};
try {
await limiter.consume(req.ip);
await flecks.governor.web.consume(req.ip);
next();
}
catch (error) {
@ -57,20 +63,18 @@ export const hooks = {
'@flecks/server.up': async (flecks) => {
if (flecks.fleck('@flecks/web/server')) {
const {web} = flecks.get('@flecks/governor/server');
const limiter = await createLimiter(
flecks.governor.web = await createLimiter(
flecks,
{
keyPrefix: '@flecks/governor.web.request.route',
...web,
},
);
flecks.set('$flecks/governor.web.limiter', limiter);
}
if (flecks.fleck('@flecks/socket/server')) {
const {[ByType]: Packets} = flecks.get('$flecks/socket.packets');
const limiters = Object.fromEntries(
flecks.governor.packet = Object.fromEntries(
await Promise.all(
Object.entries(Packets)
Object.entries(flecks.socket.Packets[ByType])
.filter(([, Packet]) => Packet.limit)
.map(async ([name, Packet]) => (
[
@ -83,23 +87,20 @@ export const hooks = {
)),
),
);
flecks.set('$flecks/governor.packet.limiters', limiters);
const {socket} = flecks.get('@flecks/governor/server');
const limiter = await createLimiter(
flecks.governor.socket = await createLimiter(
flecks,
{
keyPrefix: '@flecks/governor.socket.request.socket',
...socket,
},
);
flecks.set('$flecks/governor.socket.limiter', limiter);
}
},
'@flecks/socket/server.request.socket': (flecks) => {
const limiter = flecks.get('$flecks/governor.socket.limiter');
return async (socket, next) => {
'@flecks/socket/server.request.socket': (flecks) => (
async (socket, next) => {
const {handshake: req} = socket;
const {Ban} = flecks.get('$flecks/db.models');
const {Ban} = flecks.db.Models;
try {
await Ban.check(req);
}
@ -112,7 +113,7 @@ export const hooks = {
socket.disconnect();
};
try {
await limiter.consume(req.ip);
await flecks.governor.socket.consume(req.ip);
next();
}
catch (error) {
@ -120,8 +121,8 @@ export const hooks = {
await Ban.create(Ban.fromRequest(req, keys, ttl));
next(error);
}
};
},
}
),
'@flecks/socket.packets.decorate': (Packets, flecks) => (
Object.fromEntries(
Object.entries(Packets).map(([keyPrefix, Packet]) => [

View File

@ -6,7 +6,7 @@ import {HistoryRouter as ReduxHistoryRouter} from 'redux-first-history/rr6';
export const hooks = {
'@flecks/react.providers': (req, flecks) => (
flecks.fleck('@flecks/redux')
? [ReduxHistoryRouter, {history: createReduxHistory(flecks.get('$flecks/redux.store'))}]
? [ReduxHistoryRouter, {history: createReduxHistory(flecks.redux)}]
: [HistoryRouter, {history}]
),
};

View File

@ -5,13 +5,20 @@ import configureStore, {createReducer} from '../store';
import localStorageEnhancer from './local-storage';
export const hooks = {
'@flecks/core.mixin': (Flecks) => (
class FlecksWithRedux extends Flecks {
redux;
}
),
'@flecks/react.providers': async (req, flecks) => {
const slices = await flecks.invokeMergeUnique('@flecks/redux.slices');
const reducer = createReducer(flecks, slices);
// Hydrate from server.
const {preloadedState} = flecks.get('@flecks/redux/client');
const store = await configureStore(flecks, reducer, {preloadedState});
flecks.set('$flecks/redux.store', store);
flecks.redux = store;
return [Provider, {store}];
},
'@flecks/redux.store': ({enhancers}) => {

View File

@ -1,7 +1,7 @@
export default (Action, flecks) => class ActionClient extends Action {
static async respond(packet) {
flecks.get('$flecks/redux.store').dispatch(packet.data);
flecks.redux.dispatch(packet.data);
}
};

View File

@ -61,7 +61,7 @@ export async function createReplServer(flecks) {
}
}
const socket = join(tmpdir(), 'flecks', id, 'repl', `${id}-${Date.now()}.sock`);
flecks.set('$flecks/repl.socket', socket);
flecks.repl = socket;
await new Promise((resolve) => {
netServer.listen(socket, resolve);
});

View File

@ -3,5 +3,12 @@ import {createReplServer} from './repl';
export const hooks = {
'@flecks/core.commands': commands,
'@flecks/core.mixin': (Flecks) => (
class FlecksWithRepl extends Flecks {
repl;
}
),
'@flecks/server.up': (flecks) => createReplServer(flecks),
};

View File

@ -2,8 +2,8 @@ import {mkdir} from 'fs/promises';
import {tmpdir} from 'os';
import {join} from 'path';
import {D} from '@flecks/core';
import {Flecks} from '@flecks/core/server';
import {compose, D} from '@flecks/core';
import {Flecks as BaseFlecks} from '@flecks/core/server';
const {version} = require('../package.json');
@ -22,12 +22,18 @@ const {version} = require('../package.json');
const debug = D('@flecks/server/entry');
debug('starting server...');
// Make resolver.
// Flecks mixins.
const flecks = await loadFlecks();
const mixins = Object.entries(flecks)
.map(([, M]) => M.hooks?.['@flecks/core.mixin'])
.filter((e) => e);
const Flecks = compose(...mixins)(BaseFlecks);
const resolver = Flecks.makeResolver(config);
const rcs = Flecks.loadRcs(resolver);
Flecks.installCompilers(rcs, resolver);
global.flecks = new Flecks({
config,
flecks: await loadFlecks(),
flecks,
platforms,
resolver,
rcs,

View File

@ -1,9 +1,22 @@
import SocketClient from './socket';
export const hooks = {
'@flecks/core.mixin': (Flecks) => (
class FlecksWithSocket extends Flecks {
constructor(...args) {
super(...args);
if (!this.socket) {
this.socket = {};
}
this.socket.client = undefined;
}
}
),
'@flecks/web/client.up': (flecks) => {
const socket = new SocketClient(flecks);
flecks.set('$flecks/socket.socket', socket);
flecks.socket.client = socket;
socket.connect();
socket.listen();
},

View File

@ -2,6 +2,5 @@ import {useFlecks} from '@flecks/react';
export default function useSocket() {
const flecks = useFlecks();
const sock = flecks.get('$flecks/socket.socket');
return sock;
return flecks.socket.client;
}

View File

@ -8,11 +8,21 @@ export * from './hooks';
export {Packet, Packer, ValidationError} from './packet';
export const hooks = {
'@flecks/core.mixin': (Flecks) => (
class FlecksWithSocket extends Flecks {
constructor(...args) {
super(...args);
if (!this.socket) {
this.socket = {};
}
this.socket.Packets = {};
}
}
),
'@flecks/core.starting': (flecks) => {
flecks.set('$flecks/socket.packets', flecks.gather(
'@flecks/socket.packets',
{check: badPacketsCheck},
));
flecks.socket.Packets = flecks.gather('@flecks/socket.packets', {check: badPacketsCheck});
},
'@flecks/web.config': async (
req,

View File

@ -1,5 +1,5 @@
const hydrate = (flecks, [type, data]) => {
const {[type]: Packet} = flecks.get('$flecks/socket.packets');
const {[type]: Packet} = flecks.socket.Packets;
if (!Packet) {
throw new TypeError(`No packet of type '${type}'`);
}

View File

@ -68,7 +68,7 @@ export default (flecks) => class BundlePacket extends PacketClass {
packedPacket.writeUInt8(buffer.readUInt8(caret++), i++);
}
// Lookup packet.
const {[packetId]: Packet} = flecks.get('$flecks/socket.packets');
const {[packetId]: Packet} = flecks.socket.Packets;
res.push(new Packet(Packet.decode(packedPacket)));
}
return res;

View File

@ -1,25 +1,30 @@
import createIntercom from './create-intercom';
import SocketServer from './server';
const flecksServers = new WeakMap();
export const server = (flecks) => flecksServers.get(flecks);
export const hooks = {
'@flecks/web/server.request.socket': (flecks) => (req, res, next) => {
req.intercom = createIntercom(server(flecks), 'web');
req.intercom = createIntercom(flecks.socket.server, 'web');
next();
},
'@flecks/web/server.up': async (httpServer, flecks) => {
const server = new SocketServer(httpServer, flecks);
flecksServers.set(flecks, server);
flecks.socket.server = server;
await server.connect();
},
'@flecks/repl.context': (flecks) => ({
Packets: flecks.get('$flecks/socket.packets'),
socketServer: server(flecks),
}),
'@flecks/socket.server': ({config: {'@flecks/core': {id}}}) => ({
path: `/${id}/socket.io`,
}),
'@flecks/core.mixin': (Flecks) => (
class FlecksWithSocketServer extends Flecks {
constructor(...args) {
super(...args);
if (!this.socket) {
this.socket = {};
}
this.socket.server = undefined;
}
}
),
};

View File

@ -35,7 +35,6 @@ export default class SocketServer {
...await this.flecks.invokeMergeAsync('@flecks/socket.server'),
serveClient: false,
});
this.flecks.set('$flecks/socket.io', this.io);
this.io.use(this.makeSocketMiddleware());
this.io.on('@flecks/socket.intercom', this.localIntercom);
this.flecks.invoke('@flecks/socket.server.io', this);

View File

@ -12,7 +12,7 @@ export default class Socket {
}
listen() {
const {[ByType]: PacketsByType} = this.flecks.get('$flecks/socket.packets');
const {[ByType]: PacketsByType} = this.flecks.socket.Packets;
const Packets = Object.entries(PacketsByType);
for (let i = 0; i < Packets.length; i++) {
const [type, Packet] = Packets[i];

View File

@ -29,7 +29,7 @@ export const hooks = {
];
},
'@flecks/repl.commands': (flecks) => {
const {User} = flecks.get('$flecks/db.models');
const {User} = flecks.db.Models;
return {
createUser: async (spec) => {
const [email, maybePassword] = spec.split(' ', 2);
@ -54,7 +54,7 @@ export const hooks = {
passport.use(new LocalStrategy(
{usernameField: 'email'},
async (email, password, fn) => {
const {User} = flecks.get('$flecks/db.models');
const {User} = flecks.db.Models;
try {
const user = await User.findOne({where: {email}});
fn(undefined, user && await user.validatePassword(password) && user);

View File

@ -13,7 +13,7 @@ export const hooks = {
debugSilly('@flecks/web/server.request.route: passport.session()');
passport.session()(req, res, () => {
if (!req.user) {
const {User} = flecks.get('$flecks/db.models');
const {User} = flecks.db.Models;
req.user = new User();
req.user.id = 0;
}
@ -34,7 +34,7 @@ export const hooks = {
'@flecks/server.up': (flecks) => {
passport.serializeUser((user, fn) => fn(null, user.id));
passport.deserializeUser(async (id, fn) => {
const {User} = flecks.get('$flecks/db.models');
const {User} = flecks.db.Models;
try {
fn(undefined, await User.findByPk(id));
}
@ -63,7 +63,7 @@ export const hooks = {
debugSilly('@flecks/socket/server.request.socket: passport.session()');
passport.session()(socket.handshake, undefined, async () => {
if (!socket.handshake.user) {
const {User} = flecks.get('$flecks/db.models');
const {User} = flecks.db.Models;
socket.handshake.user = new User();
socket.handshake.user.id = 0;
}

View File

@ -1,6 +1,6 @@
import {D} from '@flecks/core';
import express from 'express';
import expressSession from 'express-session';
import user.session from 'express-session';
const debug = D('@flecks/user/session');
const debugSilly = debug.extend('silly');
@ -16,6 +16,15 @@ export const hooks = {
'Set the FLECKS_ENV__flecks_user_session_server__cookieSecret environment variable!'
),
}),
'@flecks/core.mixin': (Flecks) => (
class FlecksWithUser extends Flecks {
user = {
session: undefined,
}
}
),
'@flecks/web/server.request.route': (flecks) => {
const urle = express.urlencoded({extended: true});
return (req, res, next) => {
@ -26,7 +35,7 @@ export const hooks = {
return;
}
debugSilly('@flecks/web/server.request.route: session()');
flecks.get('$flecks/user.session')(req, res, (error) => {
flecks.user.session(req, res, (error) => {
if (error) {
next(error);
return;
@ -38,17 +47,17 @@ export const hooks = {
};
},
'@flecks/server.up': async (flecks) => {
flecks.set('$flecks/user.session', expressSession({
flecks.user.session = user.session({
resave: false,
sameSite: true,
saveUninitialized: false,
secret: flecks.get('@flecks/user/session/server.cookieSecret'),
...await flecks.invokeMergeAsync('@flecks/user.session'),
}));
});
},
'@flecks/socket/server.request.socket': (flecks) => (socket, next) => {
debugSilly('@flecks/socket/server.request.socket: session()');
flecks.get('$flecks/user.session')(socket.handshake, {}, () => {
flecks.user.session(socket.handshake, {}, () => {
const id = socket.handshake.session?.id;
socket.join(id);
next();

View File

@ -1,4 +1,4 @@
import {compose, D, Flecks} from '@flecks/core';
import {compose, D, Flecks as BaseFlecks} from '@flecks/core';
// eslint-disable-next-line import/no-extraneous-dependencies, import/no-unresolved
const {version} = require('@flecks/web/package.json');
@ -56,8 +56,8 @@ const {version} = require('@flecks/web/package.json');
const mixins = Object.entries(runtime.flecks)
.map(([, M]) => M.hooks?.['@flecks/core.mixin'])
.filter((e) => e);
const Class = compose(...mixins)(Flecks);
const flecks = new Class(runtime);
const Flecks = compose(...mixins)(BaseFlecks);
const flecks = new Flecks(runtime);
window.flecks = flecks;
try {
await Promise.all(flecks.invokeFlat('@flecks/core.starting'));

View File

@ -2,7 +2,7 @@ import {Transform} from 'stream';
const config = async (flecks, req) => {
const httpConfig = await flecks.invokeMergeAsync('@flecks/web.config', req);
const {config} = flecks.get('$flecks/web.flecks');
const {config} = flecks.web.flecks;
const reducedConfig = Object.keys(config)
.filter((path) => !path.startsWith('$'))
.filter((path) => !path.endsWith('/server'))

View File

@ -32,7 +32,7 @@ export const createHttpServer = async (flecks) => {
app.set('trust proxy', trust);
const httpServer = createServer(app);
httpServer.app = app;
flecks.set('$flecks/web/server.instance', httpServer);
flecks.web.server = httpServer;
// Compression. heheh
app.use(compression({level: 'production' === NODE_ENV ? 6 : 9}));
// Socket connection.

View File

@ -183,6 +183,16 @@ export const hooks = {
*/
trust: false,
}),
'@flecks/core.mixin': (Flecks) => (
class FlecksWithWeb extends Flecks {
web = {
flecks: undefined,
server: undefined,
}
}
),
'@flecks/core.starting': (flecks) => {
debug('bootstrapping flecks...');
const webFlecks = Flecks.bootstrap({
@ -190,7 +200,7 @@ export const hooks = {
platforms: ['client', '!server'],
});
debug('bootstrapped');
flecks.set('$flecks/web.flecks', webFlecks);
flecks.web.flecks = webFlecks;
},
'@flecks/core.targets': (flecks) => [
'web',
@ -208,7 +218,4 @@ export const hooks = {
],
'@flecks/web/server.stream.html': inlineConfig,
'@flecks/server.up': (flecks) => createHttpServer(flecks),
'@flecks/repl.context': (flecks) => ({
httpServer: flecks.get('$flecks/web/server.instance'),
}),
};