As I studied an open-source codebase with intent to contribute, I used ChatGPT as a note-taking tool. This is my side of one of our conversations.


calling useDevice() eturns the state stored in this component. It seems overkill when you can simply query navigator.userAgent as needed. Device can’t change during a session, so I don’t see how there could be a case where the context changes and subscribed components re-render in response. Is the redux setup mostly meant to achieve a consistent global state layer?

import { createAction, createReducer } from '@reduxjs/toolkit'; 
import React, { PropsWithChildren, useReducer } from 'react';
import { useEffectOnce } from '../../hooks/useEffectOnce';
import { getDevice } from './getDevice';
import { Device } from './types';

const DEVICE_ACTIONS = {
setDevice: createAction<{ device: Device }>('setDevice'),
};

const INITIAL_USER_AGENT = navigator.userAgent || '';

const getInitialState = (): Device => getDevice(INITIAL_USER_AGENT);

const deviceReducer = createReducer(getInitialState(), (builder) => {
builder.addCase(DEVICE_ACTIONS.setDevice, (state, action) => {
    return action.payload.device;
});
});

export const DeviceCtx = React.createContext<Device>(getInitialState());

export const DeviceProvider: React.FC<PropsWithChildren<{}>> = (props) => {
const [state, dispatch] = useReducer(deviceReducer, getInitialState());

useEffectOnce(() => {
    const device = getDevice(navigator.userAgent || '');
    dispatch(DEVICE_ACTIONS.setDevice({ device }));
});

return <DeviceCtx.Provider value={state}>{props.children}</DeviceCtx.Provider>;
};

ViewportCtx’s Redux setup is a bit more understandable, since screen dimensions are obviously more likely to change throughout a session. Just to test understanding though: contexts are typically exposed as state and controllers that update that state. The AuthCtx’s controllers are returned along with the state by useAuth(), and operate via network calls – hands-on, user-triggered. However, ViewportCtx’s controllers are exposed as event listeners…. that are mounted to the subscribed component – hands-off.

import { createAction, createReducer } from '@reduxjs/toolkit';
import React, { PropsWithChildren, useEffect, useReducer } from 'react';
import { useMedia } from '../../hooks/useMedia';
import { getViewportState } from './getViewportState';
import { Breakpoint, ViewportState } from './types';

const VIEWPORT_ACTIONS = {
setViewportState: createAction<{ state: ViewportState }>('setBreakpoint'),
updateDimensions: createAction<{ innerHeight: number; innerWidth: number }>('updateDimensions'),
};

const BREAKPOINT_QUERIES = [
'(max-width: 575px)',
'(max-width: 767px)',
'(max-width: 991px)',
'(max-width: 1199px)',
'(max-width: 1599px)',
];

const BREAKPOINTS: Breakpoint[] = ['xs', 'sm', 'md', 'lg', 'xl'];

const getInitialState = (): ViewportState => getViewportState('xs');

const viewportReducer = createReducer(getInitialState(), (builder) => {
builder.addCase(VIEWPORT_ACTIONS.setViewportState, (state, action) => {
    return action.payload.state;
});
builder.addCase(VIEWPORT_ACTIONS.updateDimensions, (state, action) => {
    state.innerHeight = action.payload.innerHeight;
    state.innerWidth = action.payload.innerWidth;
});
});

export const ViewportCtx = React.createContext<ViewportState>(getInitialState());

export const ViewportProvider: React.FC<PropsWithChildren<{}>> = (props) => {
const [state, dispatch] = useReducer(viewportReducer, getInitialState());

const breakpoint = useMedia(BREAKPOINT_QUERIES, BREAKPOINTS, 'xxl');

useEffect(() => {
    dispatch(VIEWPORT_ACTIONS.setViewportState({ state: getViewportState(breakpoint) }));
}, [breakpoint]);

useEffect(() => {
    const onResize = () => {
    dispatch(VIEWPORT_ACTIONS.updateDimensions({ innerHeight: window.innerHeight, innerWidth: window.innerWidth }));
    };
    window.addEventListener('resize', onResize);
    window.addEventListener('orientationchange', onResize);
    return () => {
    window.removeEventListener('resize', onResize);
    window.addEventListener('orientationchange', onResize);
    };
}, []);

return <ViewportCtx.Provider value={state}>{props.children}</ViewportCtx.Provider>;
};

  • Why is database migration + seed performed on container restart? Not following the “migrate” docker-compose.yml service or how it’d apply here, unless it’s a formality to migrate upon db initialization for some sort of “clean start”? Or is this a way of providing dummy data to the application?
  • Why is secrets.env demanded by the readme and included in the environment if it’s not used? I’ve confirmed that only env/dev.env variables are used when starting the database in dev; those secrets presumably are required when connecting to AWS services in production, and there’s an entire /aws folder dedicated to orchestrating that prod infrastructure, but it’s never touched in dev or fakeprod, unless I’m missing something.
  • What’s the difference between a dispatcher and a worker? I’ve traced three “jobs” in the codebase: sendMail, pulseCheck, and AssociateVideoUrl, which all occur every 60 seconds, and they’re visible in the terminal while the docker network runs.

readme.md:

### Commands

codebase commands are run using the ./bin/cb command.

To view all the commands, run:


./bin/cb


Before running the api for the first time, you will need to generate a secrets file (not tracked by .git):


./bin/cb gensecrets


The file it generates will have fake credentials, but this should be OK as long as you don't interact with dev AWS resources (such as uploading a file to AWS).

To run the api, start Docker engine and run:


./bin/cb dev

backend package.json:

"migrate": "yarn mikro-orm migration:up",
    "seed": "yarn ts-node src/db/mikro-orm/seeds/seed.ts", 

docker-compose.dev.yml:

# A container is an OS-level virtualization, i.e., a box of dependencies meant to run an app, concealed from the rest of the OS. Many containers can be run on one OS instance (in Docker's case, a Linux VM). This prevents issues arising from outdated dependencies, version conflicts, dev/prod env inconsistencies, etc.

# A Docker Compose network is a group of Docker containers that can communicate with each other through Docker's internal DNS. For example, "web:3000" is the "web" service's port 3000, which the "api" service can see (for CORS whitelist, etc.), but not the local browser.

# "web" is the frontend's CRA dev server, which runs on localhost:3000 by default. But here, we use Nginx to route requests to port 80 (HTTP default), allowing us to see the frontend on localhost.

services:

# ===========================================
# build container from Dockerfile image (frontend)
# ===========================================

web:
    container_name: web
    image: codebase:latest
    volumes:
    - ./web:/app/web # node_modules troubleshooting requires full frontend dir bind mount

    # - ./web/src:/app/web/src
    # - ./web/public:/app/web/public
    environment:
    - CHOKIDAR_USEPOLLING=true
    - CHOKIDAR_INTERVAL=500  # 500ms poll interval instead of 100ms
    command: ['yarn', 'dev:web']

# ===========================================
# build containers from Dockerfile image (same backend, different service)
# ===========================================

api:
    container_name: api
    image: codebase:latest
    env_file:
    - ./env/dev.env
    - ./env/secrets.env
    volumes:
    - ./api/src:/app/api/src
    links:
    - db
    - redis
    ports:
    - 3000:3000
    command: ['yarn', 'dev:api']

migrate:
    container_name: migrate
    image: codebase:latest
    env_file:
    - ./env/dev.env
    - ./env/secrets.env
    volumes:
    - ./api/src:/app/api/src
    links:
    - db
    command: 'bash -c "yarn migrate && yarn seed"'

dispatch:
    container_name: dispatcher
    image: codebase:latest
    env_file:
    - ./env/dev.env
    - ./env/secrets.env
    environment:
    PORT: 3002
    volumes:
    - ./api/src:/app/api/src
    links:
    - db
    - redis
    ports:
    - 3002:3002
    command: ['yarn', 'dev:dispatcher']

worker:
    container_name: worker
    image: codebase:latest
    env_file:
    - ./env/dev.env
    - ./env/secrets.env
    environment:
    PORT: 3001
    volumes:
    - ./api/src:/app/api/src
    links:
    - db
    - redis
    ports:
    - 3001:3001
    command: ['yarn', 'dev:worker']

# ===========================================
# build containers from Docker Hub
# ===========================================

db:
    container_name: db
    image: postgres:11
    restart: always
    environment:
    POSTGRES_DB: codebase
    POSTGRES_USER: codebase
    POSTGRES_PASSWORD: codebase
    ports:
    - 5432:5432

redis:
    container_name: redis
    image: redis:5.0

# nginx as reverse proxy only
nginx:
    container_name: nginx
    image: nginx:1.21
    volumes:
    - ./nginx/nginx.dev.conf:/etc/nginx/nginx.conf
    links:
    - api
    - web
    ports:
    - 80:80

seed.ts seems to confirm that dummy data is provided (i missed this!). so it connects to the database from a separate server to the main API and populates it with ephemeral(?) data when the compose network starts:

import { last, random, sample, shuffle, times } from 'lodash';
import { isMikroORMDb } from '../..';
import { Notation, Tag, User, UserRole } from '../../../domain';
import { InternalError } from '../../../errors';
import { container } from '../../../inversify.config';
import { TYPES } from '../../../inversify.constants';
import { Logger, rand } from '../../../util';
import { Db } from '../../types';
import { NotationEntity, TagEntity, UserEntity } from '../entities';
import {
ADJECTIVES,
ARTIST_NAMES,
ENCRYPTED_PASSWORD,
MUSIC_XML_URLS,
NOTATION_THUMBNAIL_URLS,
NOUNS,
USERNAMES,
USER_AVATAR_URLS,
VIDEO_URL,
} from './constants';

(async () => {
const db = container.get<Db>(TYPES.Db);
const logger = container.get<Logger>(TYPES.Logger);

if (!isMikroORMDb(db)) {
    throw new InternalError('must use MikroORM db, check inversify.config.ts');
}
logger.info('connecting to the database...');
await db.init();
logger.info('connection succeeded');
logger.info('seeding database...');

const buildUser = (props: Partial<User> = {}) => {
    const user = new UserEntity(
    {
        ...rand.notation({
        id: undefined,
        username: ${sample(USERNAMES)}_${random(0, 99999)},
        email: ${rand.str(5)}@${rand.str(5)}.com,
        encryptedPassword: ENCRYPTED_PASSWORD,
        avatarUrl: sample(USER_AVATAR_URLS),
        ...props,
        }),
    },
    { em: db.em }
    );
    db.em.persist(user);
    return user;
};
const buildNotation = (props: Partial<Notation> = {}) => {
    const notation = new NotationEntity(
    rand.notation({
        id: undefined,
        cursor: 0,
        durationMs: 34933,
        songName: ${sample(ADJECTIVES)} ${sample(NOUNS)},
        artistName: sample(ARTIST_NAMES),
        transcriberId: undefined,
        thumbnailUrl: sample(NOTATION_THUMBNAIL_URLS),
        musicXmlUrl: sample(MUSIC_XML_URLS),
        videoUrl: VIDEO_URL,
        private: false,
        ...props,
    }),
    { em: db.em }
    );
    db.em.persist(notation);
    return notation;
};
const buildTag = (props: Partial<Tag> = {}) => {
    const tag = new TagEntity(rand.tag({ id: undefined, ...props }), { em: db.em });
    db.em.persist(tag);
    return tag;
};

// create users
const jared = buildUser({ username: 'jaredplaysguitar', email: 'jared@codebase.com', role: UserRole.ADMIN });
const admin = buildUser({ username: 'admin', email: 'admin@codebase.com', role: UserRole.ADMIN });
const teacher = buildUser({ username: 'teacher', email: 'teacher@codebase.com', role: UserRole.TEACHER });
const student = buildUser({ username: 'student', email: 'student@codebase.com', role: UserRole.STUDENT });
const students = times(50, () => buildUser({ role: UserRole.STUDENT }));
const teachers = [jared, admin, teacher];

// create notations
const notations = times(100, () => {
    const notation = buildNotation();
    const transcriber = sample(teachers)!;
    notation.transcriber.set(transcriber);
    return notation;
});

// create notations named after the music xml file that backs them
notations.push(
    ...MUSIC_XML_URLS.map((musicXmlUrl) => {
    const filename = last(musicXmlUrl.split('/'));
    const notation = buildNotation({ musicXmlUrl, songName: filename, deadTimeMs: -67 });
    const transcriber = sample(teachers)!;
    notation.transcriber.set(transcriber);
    return notation;
    })
);

// create tags
const tags = [
    buildTag({ name: 'acoustic' }),
    buildTag({ name: 'alternative' }),
    buildTag({ name: 'electric' }),
    buildTag({ name: 'jazz' }),
    buildTag({ name: 'neosoul' }),
    buildTag({ name: 'prog' }),
];

// create notation tags
for (const notation of notations) {
    const numTags = random(1, 3);
    const shuffledTags = shuffle(tags);
    const selectedTags = shuffledTags.slice(0, numTags);
    for (const selectedTag of selectedTags) {
    notation.tags.add(selectedTag);
    }
}

logger.info('persisting seeded entities...');
try {
    await db.em.flush();
    logger.info('seed succeeded');
} catch (e) {
    logger.error('seed failed');
    console.error(e);
} finally {
    logger.info('closing connection...');
    await db.closeConnection();
    logger.info('connection closed');
}
})();

Question: what did the readme mean by “dev AWS resources” if the dev environment doesn’t use AWS? And I assume the API endpoints that involve AWS in any way simply aren’t used in dev


  1. it seems that inversify infers from NODE_ENV whether to run DevApiServer.ts or ApiServer.ts via entrypoints/api.ts. In dev, middleware is absent (no withCors(), withSession(), etc.). Does this imply that dev does not use the network, and user data is limited to the database seed? Without an auth layer, how would something like login work?

  2. If Redis is used to persist both job and session data, and the Compose network has only one Redis container, how are its responsibilities split?

  3. Re: AWS services being exposed in gulpfile.ts and /aws, but not part of the straightahead dev / fakeprod flow – does this imply a middle-ground (staging?) environment not discussed in the readme, and the “using AWS resources” contingency mentioned a case of an external developer like me overstepping their role? Can you help pinpoint which gulpfile.ts commands involving aws are used in staging or prod, outside of the docker flow? I assume “install” is used in prod, but “deploy” and “cdkdeploy” seem like equal suspects.

  4. the WorkerServer (run by both dispatcher and worker services) has only a /health endpoint, meaning the core dispatcher/worker logic is happening elsewhere? Both dispatcher.ts and worker.ts entrypoints launch that WorkerServer, and only diverge in that they run startDispatching() and startWorking() on each job’s “job” property, respectively. Help me piece together how this cooperates with the main API server.

(1) inversify.config.ts:

if (config.NODE_ENV === 'development') {
container.bind<GraphqlServer>(TYPES.ApiServer).to(DevApiServer);
} else {
container.bind<GraphqlServer>(TYPES.ApiServer).to(ApiServer);
}
container.bind<JobServer>(TYPES.WorkerServer).to(WorkerServer);

(2) gulpfile.ts:

exports['dev'] = series(buildapp, dev);
exports['fakeprod'] = series(buildnginx, buildapp, fakeprod);

exports['down'] = down;
exports['typegen'] = typegen;
exports['gensecrets'] = gensecrets;
exports['logs'] = logs;
exports['deploy'] = deploy;
exports['rollback'] = rollback;
exports['db'] = db;
exports['redis'] = redis;
exports['migrator'] = series(buildapp, migrator);

exports['tscapi'] = tscapi;
exports['tscweb'] = tscweb;

exports['install'] = parallel(installapi, installweb, installaws);
exports['installapi'] = installapi;
exports['installweb'] = installweb;
exports['installaws'] = installaws;

exports['buildapp'] = buildapp;
exports['buildnginx'] = series(installweb, buildnginx);

exports['testall'] = series(gensecrets, buildapp, testapi, testweb);
exports['testapi'] = series(gensecrets, buildapp, testapi);
exports['testweb'] = series(gensecrets, buildapp, testweb);
exports['extractreports'] = extractReports;

exports['cdkdeploy'] = cdkdeploy;

exports['admin'] = admin;
exports['admindb'] = admindb;
exports['adminmigrate'] = adminmigrate;

(4) A job looks like this:

export class AssociateVideoUrl {
readonly job: Job<AssociateVideoUrlPayload>;
private readonly config: Config;

constructor(@inject(TYPES.Config) config: Config) {
    this.config = config;
    this.job = new BullMqJob('ASSOCIATE_VIDEO_URL', associateVideoUrl, this.config, { intervalMs: 60000 });
}
}

with these functions on its “job” property:

async startWorking(): Promise<void> {
    const queue = this.ensureQueue();
    await queue.waitUntilReady();

    const worker = this.ensureWorker();
    await worker.waitUntilReady();

    const scheduler = this.ensureScheduler();
    await scheduler.waitUntilReady();
}

async startDispatching(): Promise<void> {
    const queue = this.ensureQueue();
    await queue.waitUntilReady();

    const scheduler = this.ensureScheduler();
    await scheduler.waitUntilReady();

    if (isNumber(this.opts.intervalMs)) {
    const name = this.getRepeatTaskName();
    await queue.add(name, {} as P, { repeat: { every: this.opts.intervalMs } });
    }
}

I’m remembering that the dev nginx.conf forces HTTP 1.1. Could this explain the absence of middleware? Maybe since it’s a controlled environment, security is assumed? Or is it ultimately just an auth-less system.


Is it looking like I’m in too deep not to pursue a SWE career?


How generalizable is this knowledge? I’ve approached learning this codebase pretty obsessively for the past week and part of me hopes the patterns and architecture aren’t too bespoke to be useful. At least I’ve developed my systems thinking and tolerance for cognitive load.


in the HOC, is the fallback (spinner) component displayed only until the appropriate redirect (per switch block) is performed?


Are Number values -1, 0, and 1 treated as booleans in some JS contexts? The switch table in this HOC that tests auth status returns values like “isLoggedIn && [function* that returns -1, 0, or 1 by enumerating user role based on its index in its native array]”


Right, so we have the switch table return [boolean] && [boolean], since gtEq(userRole, minimumRole) is itself a test that returns a boolean.


Is the hierarchical order of custom context providers in this component tree intentional, or would the route components gain access to each state slice regardless of the order?


so, given the order here is intentional:

  • route state depends on auth state
  • auth state depends on device state
  • device state depends on viewport state?

i can’t imagine why operations requiring auth state would depend on device information.


no matches for “device” in AuthCtx.jsx (-returning module), so it’s probably incidental. device/auth context tree segment is intentional, all else above is arbitary.


Earlier I said “In dev, middleware is absent (no withCors(), withSession(), etc.). Does this imply that dev does not use the network, and user data is limited to the database seed?”

Upon closer inspection, I’ve realized that the DevApiServer DOES have middleware and thus functioning auth. The class extends ApiServer and runs all of the same app.use() calls – only difference is that it additionally mounts an Altar playground to /altair. So while the app has only seed data at the outset, it’s presumably possible to login and signup.