| author | title |
|---|---|
Mudassar |
An Overview of Labs project |
In this markdown, you will learn about the technologies and architecture of some of Labs projects. You can find all the projects at CERP GitHub organization.
We're building system using latest technologies, here's the list but we're not limited to them:
-
Frontend
- Javascript
- Typescript
- ReactJS + Redux
- MaterialUI, Tailwindcss
-
Backend
- Elixir, NodeJS
- Postgres
-
Platform
- Google Cloud Platform
- Docker
- Kubernetes
Here are some tools that we build which are currently used in most of our projects
-
Syncr
Make
asynchronousrequests to a server over WebSocket/Http connection -
Former
Handle
HTMLFormEventsand mutate the state. Currently supportclassbased components -
Dynamic
Provide useful tooling to manipulate the complex
Objects(JS) andMaps(Elixir)
At Labs we built MISchool and IlmxExchange as our starter products. Below are some stuff which we follow and reuse for every project that we already built and will building in future.
To make change to redux store or to send payload to server, we divide actions into two category
These actions can be futher categorized into Core and Simple (based on core actions) actions. Let's see how core actions works
For now we have createMerges() and createDeletes() actions which help to manipulate to complex deep object in state
-
createMerges()createMerges() takes single argument of type
Merge[]and returnvoid. Here we're only looking at the prototype and usage of createMerges().API
type Merge = { path: Array<string> value: any } createMerges(merges: Merge[]): void
What does merge mean? Basically merge is an object which contains path and value. "Path" is composed of complex deep object properties and "value" is any thing that we want to to store or update against the path.Usage
Let's see createMerges in actions. Consider we have state like below
State A"db": { "students": { [1]: { id: 1 Name: "ABC", Gender: "M", "attendance": {} } } }
1st Merge: Let's add a single student entry to state, so we create a merge like
// merge to create or update a student const student = { id: 2 Name: "XYZ", Gender: "X", payments: {} } const merges = [ { path: ["db", "students", student.id], value: student } ] dispatch(createMerges(merges))
After the merge action, state changes from
A=>Bwith a new student ofid:2State B"db": { "students": { [1]: { id: 1 Name: "ABC", Gender: "M", "attendance": {} }, [2]: { id: 2 Name: "XYZ", Gender: "M", "attendance": {} } } }
2nd Merge: Let's add an
attendanceentry against a student (deep object), so we create a merge like// merge to create or update a student const student_id = 2 const attendance = { date: "04-07-2020", status: "PRESENT", time: 1593806556000 } const merges = [ { path: ["db", "students", student_id, "attendance", attendance.date], value: attendance } ] dispatch(createMerges(merges))
After the 2nd merge action, state changes from
B=>Cwith an attendance entry of studentid:2State C"db": { "students": { [1]: { id: 1 Name: "ABC", Gender: "M", "attendance": {} }, [2]: { id: 2 Name: "XYZ", Gender: "M", "attendance": { ["04-07-2020"]: { date: "04-07-2020", status: "PRESENT", time: 1593806556000 } } } } }
-
createDeletes()createDeletes() takes single argument of type
Delete[]and returnvoid. Here we're only looking at the prototype and usage of createDeletes().API
type Delete = { path: Array<string> } createDeletes(deletes: Delete[]): void
Usage
1st Delete: Let's say we want to delete a student of
id: 1, so we create a delete likeconst student_id = 1 const deletes = [ { path: ["db", "students", student_id] } ] dispatch(createDeletes(deletes))
After a
createDeletes()action, state changes fromC=>D, deleted student withid:1State D"db": { "students": { [2]: { id: 2 Name: "XYZ", Gender: "M", "attendance": { ["04-07-2020"]: { date: "04-07-2020", status: "PRESENT", time: 1593806556000 } } } } }
async actions help to send data to server using Syncr.
How a state manipulated by createMerges() or createDeletes encoded as an action and send to server. Let's see with createDeletes() in below steps. Assuming that we're working on MISchool
-
Step 1: Dispatch createDeletes()const student_id = 1 const delete = [ { path: ["db", "students", student_id] } ] dispatch(createDeletes(delete))
-
Step-2: Prepare merges as an action for server (increateDeletes())const new_deletes = deletes.reduce((agg, curr) => ({ ...agg, [curr.path.join(',')]: { action: { type: "DELETE", path: curr.path.map(p => p === undefined ? "" : p), value: 1 // what's the reason behind this? }, date: new Date().getTime() } }), {}) const state = getState() const rationalized_deletes = getRationalizedQueuePayload(new_deletes, "mutations", state)
-
Step-3: Rationalizestate.queueditems with new delete mutations (ingetRationalizedQueuePayload())In this step, we're making sure, if there's anything else in queue(not send to server before), merge them together and send to server if
state.connected === true// we're mainly dealing with three types of payload, deletes, merges fall in "mutations" type QueuedType = "mutations" | "images" | "analytics" type Queued = { mutataions: {}; analytics: {}, images:{}} getRationalizedQueuePayload = (payload: any, key: QueuedType, state: Queued) => { return { ...state.queued, images: {}, [key]: { ...state.queued[key], ...payload } } }
Here's what
RootReducerStatelooks like -
Step-4: Send payload to server usingSyncrconst state = getState() syncr.send({ type: SYNC, school_id: state.auth.school_id, client_type: client_type, lastSnapshot: state.lastSnapshot, payload })
To achieve real-time functionality in MISchool and IlmExchange, syncing played an import role. In the following diagram, it's an abstract view of how does syncing happen among the connected clients.
In progress
Let's say we want to reset fees for student in MISchool, following diagram shows what happen behind the scene when we press reset fee button.
Service worker allows you to support offline experiences, giving engineers and developers end-to-end control over the user’s interactions with the app. A service worker enables you to run Javascript before a page even exists, makes your site faster, and allows you to display content even if there is no internet connection. A few properties of service worker are:
- Runs in its own
globalscript context - Is not directly tied to any particular page
- Cannot access the DOM
- Is event-driven (it’s terminated when it’s not in use and run again when needed)
- Is
HTTPSonly
For MISchool, we're using WorkBox, a high-level set of libraries. It provides a solid foundation for any service worker's caching, routing, and response generation logic. Currently we're intercepting follow URLs with image caching (using CacheFirst) to support MISchool offline-first user experience.
workbox.routing.registerRoute(
({ url, event }) => {
const match = url.host === "storage.googleapis.com" ||
url.host === "fonts.googleapis.com" ||
url.host === "googleapis.com" ||
url.host === "www.googleapis.com"
return match
},
// fetch from cache, but also fetch from network and update cache
new workbox.strategies.CacheFirst({
cacheName: 'images',
plugins: [
new workbox.expiration.Plugin({
maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days,
purgeOnQuotaError: true
})
]
})
)For MISchool we're adopting CacheFirst (fetch from cache, but also fetch from network and update cache) strategy. We can hook into onupdatefound function on the registered Service Worker. Even though we can cache tons of files, the Service Worker only checks the hash of registered service-worker.js. If that file has only 1 little change in it, it will be treated as a new version.
Here's simple demonstration of Cache First strategy:
The cache access failed and the Service Worker uses the network as a fallback.
For code formatting and liting we use ESLint for Typescript for frontends




