chore: cleanup app.js #1050

Closed
opened 2026-03-28 04:31:11 +00:00 by mfreeman451 · 0 comments
Owner

Imported from GitHub.

Original GitHub issue: #2891
Original author: @mfreeman451
Original URL: https://github.com/carverauto/serviceradar/issues/2891
Original created: 2026-02-22T18:54:36Z


app.js is huge and needs to get cleaned up:

Phase 1: The Target Folder Structure

We are going to break your assets/js folder into logical domains. Here is what your directory structure should look like when we are done:

assets/js/
├── app.js                     # Only setup and initialization
├── hooks/
│   ├── index.js               # Gathers and exports all hooks
│   ├── JdmEditorHook.js
│   ├── MapboxFlowMap.js
│   ├── SRQLTimeCookie.js
│   ├── charts/                # Group your D3/SVG charts
│   │   ├── TimeseriesChart.js
│   │   ├── NetflowSankeyChart.js
│   │   └── ...
│   └── god_view/              # Group your massive GodView tools
│       ├── GodViewBinaryStream.js
│       └── GodViewControlsState.js
├── lib/                       # Standalone classes / WebGL layers
│   └── deckgl/
│       ├── PacketFlowLayer.js
│       └── shaders.js
└── utils/                     # Pure helper functions
    ├── formatters.js
    └── theme.js

Phase 2: Extracting the Code (Step-by-Step)

1. Extract Utilities (assets/js/utils/formatters.js)

Move all those detached helper functions at the top of your file into their own module.

// assets/js/utils/formatters.js
export function nfFormatBytes(n) { ... }
export function nfFormatBits(n) { ... }
export function nfFormatCountPerSec(n) { ... }
export function nfFormatRateValue(units, n) { ... }

2. Extract Shaders and Custom Classes (assets/js/lib/deckgl/PacketFlowLayer.js)

Move the packetFlowVS, packetFlowFS, and the PacketFlowLayer class into their own file.

// assets/js/lib/deckgl/PacketFlowLayer.js
import { Layer, project32, picking } from "@deck.gl/core"
import { Geometry, Model } from "@luma.gl/engine"

const packetFlowVS = `...`
const packetFlowFS = `...`

export default class PacketFlowLayer extends Layer {
  // ... your existing class code
}

PacketFlowLayer.layerName = 'PacketFlowLayer';
PacketFlowLayer.defaultProps = { ... };

3. Extract Individual Hooks (assets/js/hooks/MapboxFlowMap.js)

Take each key inside your Hooks object and make it the default export of its own file.

// assets/js/hooks/MapboxFlowMap.js
import mapboxgl from "mapbox-gl"

export default {
  mounted() {
    this._initOrUpdate()
    // ... rest of your mounted logic
  },
  updated() {
    this._initOrUpdate()
  },
  destroyed() {
    // ...
  },
  _initOrUpdate() { ... },
  // ... other methods
}

4. The hooks/index.js Aggregator

Instead of manually defining the Hooks object in app.js, create an index.js file inside your hooks directory that imports them all and exports a single object.

// assets/js/hooks/index.js
import JdmEditorHook from "./JdmEditorHook"
import SRQLTimeCookie from "./SRQLTimeCookie"
import MapboxFlowMap from "./MapboxFlowMap"

// Charts
import TimeseriesChart from "./charts/TimeseriesChart"
import NetflowSankeyChart from "./charts/NetflowSankeyChart"
// ... import the other 6 charts

// God View
import GodViewBinaryStream from "./god_view/GodViewBinaryStream"
import GodViewControlsState from "./god_view/GodViewControlsState"

export default {
  JdmEditorHook,
  SRQLTimeCookie,
  MapboxFlowMap,
  TimeseriesChart,
  NetflowSankeyChart,
  GodViewBinaryStream,
  GodViewControlsState,
  // ... etc
}

5. Your New, Beautiful app.js

Now, your app.js goes from 4,300 lines down to about 40 lines. It strictly handles bootstrapping Phoenix.

// assets/js/app.js
import "phoenix_html"
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import topbar from "../vendor/topbar"

// Import your aggregated hooks!
import Hooks from "./hooks"

const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: { _csrf_token: csrfToken },
  hooks: Hooks // Pass them in here
})

// Progress bar setup
topbar.config({ barColors: { 0: "#50fa7b" }, shadowColor: "rgba(0, 0, 0, .3)" })
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// Global Window Events
window.addEventListener("phx:clipboard", async (event) => { /* ... */ })
window.addEventListener("phx:download_yaml", (event) => { /* ... */ })

liveSocket.connect()
window.liveSocket = liveSocket

if (process.env.NODE_ENV === "development") {
  window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => {
    reloader.enableServerLogs()
    // ... etc
  })
}

Phase 3: Best Practice for Massive Hooks (The "Class Wrapper" Pattern)

Look at your GodViewBinaryStream hook. It is nearly 1,500 lines long. While putting it in its own file solves the file size problem, putting 1,500 lines of complex WebGL, WebAssembly, and WebSocket logic inside a basic LiveView Hook object is an anti-pattern.

Best Practice: The Hook object should only be "glue" between LiveView and the DOM. The actual logic should live in an ES6 Class.

Here is how you refactor giant hooks:

Create a class GodViewRenderer (assets/js/lib/GodViewRenderer.js):

export default class GodViewRenderer {
  constructor(el, pushEvent) {
    this.el = el;
    this.pushEvent = pushEvent; // Pass LiveView's pushEvent so the class can talk to Elixir
    this.canvas = null;
    this.deck = null;
    // ... all your initialization logic
  }

  mount() {
    this.ensureDOM();
    this.startAnimationLoop();
    // ... setup sockets, wasm, etc.
  }

  update() {
    this.resizeCanvas();
  }

  destroy() {
    this.stopAnimationLoop();
    if (this.deck) this.deck.finalize();
    // ... cleanup
  }
  
  // ... the other 50 methods from your current hook
}

Then, your actual LiveView Hook (assets/js/hooks/god_view/GodViewBinaryStream.js) becomes incredibly clean:

import GodViewRenderer from "../../lib/GodViewRenderer";

export default {
  mounted() {
    // Instantiate your logic class, passing the DOM element and the pushEvent method
    this.renderer = new GodViewRenderer(this.el, this.pushEvent.bind(this));
    
    // Pass event handlers from the server down to the class
    this.handleEvent("god_view:set_filters", payload => this.renderer.setFilters(payload));
    this.handleEvent("god_view:set_zoom_mode", payload => this.renderer.setZoomMode(payload));

    this.renderer.mount();
  },
  
  updated() {
    this.renderer.update();
  },
  
  destroyed() {
    this.renderer.destroy();
  }
}

Summary of Benefits

  1. No merge conflicts: If one developer is working on D3 Charts and another is working on GodView DeckGL features, they are touching completely different files.
  2. Lazy Loading: With this setup, if you want to use dynamic imports later (e.g., await import('./GodViewRenderer') so you don't load 5MB of Deck.gl/Arrow code for users who never visit the map page), it's drastically easier.
  3. Readability: You don't have to scroll past 4,000 lines to find how liveSocket is connected.
Imported from GitHub. Original GitHub issue: #2891 Original author: @mfreeman451 Original URL: https://github.com/carverauto/serviceradar/issues/2891 Original created: 2026-02-22T18:54:36Z --- app.js is huge and needs to get cleaned up: ### Phase 1: The Target Folder Structure We are going to break your `assets/js` folder into logical domains. Here is what your directory structure should look like when we are done: ```text assets/js/ ├── app.js # Only setup and initialization ├── hooks/ │ ├── index.js # Gathers and exports all hooks │ ├── JdmEditorHook.js │ ├── MapboxFlowMap.js │ ├── SRQLTimeCookie.js │ ├── charts/ # Group your D3/SVG charts │ │ ├── TimeseriesChart.js │ │ ├── NetflowSankeyChart.js │ │ └── ... │ └── god_view/ # Group your massive GodView tools │ ├── GodViewBinaryStream.js │ └── GodViewControlsState.js ├── lib/ # Standalone classes / WebGL layers │ └── deckgl/ │ ├── PacketFlowLayer.js │ └── shaders.js └── utils/ # Pure helper functions ├── formatters.js └── theme.js ``` --- ### Phase 2: Extracting the Code (Step-by-Step) #### 1. Extract Utilities (`assets/js/utils/formatters.js`) Move all those detached helper functions at the top of your file into their own module. ```javascript // assets/js/utils/formatters.js export function nfFormatBytes(n) { ... } export function nfFormatBits(n) { ... } export function nfFormatCountPerSec(n) { ... } export function nfFormatRateValue(units, n) { ... } ``` #### 2. Extract Shaders and Custom Classes (`assets/js/lib/deckgl/PacketFlowLayer.js`) Move the `packetFlowVS`, `packetFlowFS`, and the `PacketFlowLayer` class into their own file. ```javascript // assets/js/lib/deckgl/PacketFlowLayer.js import { Layer, project32, picking } from "@deck.gl/core" import { Geometry, Model } from "@luma.gl/engine" const packetFlowVS = `...` const packetFlowFS = `...` export default class PacketFlowLayer extends Layer { // ... your existing class code } PacketFlowLayer.layerName = 'PacketFlowLayer'; PacketFlowLayer.defaultProps = { ... }; ``` #### 3. Extract Individual Hooks (`assets/js/hooks/MapboxFlowMap.js`) Take each key inside your `Hooks` object and make it the default export of its own file. ```javascript // assets/js/hooks/MapboxFlowMap.js import mapboxgl from "mapbox-gl" export default { mounted() { this._initOrUpdate() // ... rest of your mounted logic }, updated() { this._initOrUpdate() }, destroyed() { // ... }, _initOrUpdate() { ... }, // ... other methods } ``` #### 4. The `hooks/index.js` Aggregator Instead of manually defining the `Hooks` object in `app.js`, create an `index.js` file inside your `hooks` directory that imports them all and exports a single object. ```javascript // assets/js/hooks/index.js import JdmEditorHook from "./JdmEditorHook" import SRQLTimeCookie from "./SRQLTimeCookie" import MapboxFlowMap from "./MapboxFlowMap" // Charts import TimeseriesChart from "./charts/TimeseriesChart" import NetflowSankeyChart from "./charts/NetflowSankeyChart" // ... import the other 6 charts // God View import GodViewBinaryStream from "./god_view/GodViewBinaryStream" import GodViewControlsState from "./god_view/GodViewControlsState" export default { JdmEditorHook, SRQLTimeCookie, MapboxFlowMap, TimeseriesChart, NetflowSankeyChart, GodViewBinaryStream, GodViewControlsState, // ... etc } ``` #### 5. Your New, Beautiful `app.js` Now, your `app.js` goes from 4,300 lines down to about **40 lines**. It strictly handles bootstrapping Phoenix. ```javascript // assets/js/app.js import "phoenix_html" import { Socket } from "phoenix" import { LiveSocket } from "phoenix_live_view" import topbar from "../vendor/topbar" // Import your aggregated hooks! import Hooks from "./hooks" const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") const liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: { _csrf_token: csrfToken }, hooks: Hooks // Pass them in here }) // Progress bar setup topbar.config({ barColors: { 0: "#50fa7b" }, shadowColor: "rgba(0, 0, 0, .3)" }) window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) // Global Window Events window.addEventListener("phx:clipboard", async (event) => { /* ... */ }) window.addEventListener("phx:download_yaml", (event) => { /* ... */ }) liveSocket.connect() window.liveSocket = liveSocket if (process.env.NODE_ENV === "development") { window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => { reloader.enableServerLogs() // ... etc }) } ``` --- ### Phase 3: Best Practice for Massive Hooks (The "Class Wrapper" Pattern) Look at your `GodViewBinaryStream` hook. It is nearly 1,500 lines long. While putting it in its own file solves the *file size* problem, putting 1,500 lines of complex WebGL, WebAssembly, and WebSocket logic inside a basic LiveView Hook object is an anti-pattern. **Best Practice:** The Hook object should only be "glue" between LiveView and the DOM. The actual logic should live in an ES6 Class. Here is how you refactor giant hooks: Create a class `GodViewRenderer` (`assets/js/lib/GodViewRenderer.js`): ```javascript export default class GodViewRenderer { constructor(el, pushEvent) { this.el = el; this.pushEvent = pushEvent; // Pass LiveView's pushEvent so the class can talk to Elixir this.canvas = null; this.deck = null; // ... all your initialization logic } mount() { this.ensureDOM(); this.startAnimationLoop(); // ... setup sockets, wasm, etc. } update() { this.resizeCanvas(); } destroy() { this.stopAnimationLoop(); if (this.deck) this.deck.finalize(); // ... cleanup } // ... the other 50 methods from your current hook } ``` Then, your actual LiveView Hook (`assets/js/hooks/god_view/GodViewBinaryStream.js`) becomes incredibly clean: ```javascript import GodViewRenderer from "../../lib/GodViewRenderer"; export default { mounted() { // Instantiate your logic class, passing the DOM element and the pushEvent method this.renderer = new GodViewRenderer(this.el, this.pushEvent.bind(this)); // Pass event handlers from the server down to the class this.handleEvent("god_view:set_filters", payload => this.renderer.setFilters(payload)); this.handleEvent("god_view:set_zoom_mode", payload => this.renderer.setZoomMode(payload)); this.renderer.mount(); }, updated() { this.renderer.update(); }, destroyed() { this.renderer.destroy(); } } ``` ### Summary of Benefits 1. **No merge conflicts:** If one developer is working on D3 Charts and another is working on GodView DeckGL features, they are touching completely different files. 2. **Lazy Loading:** With this setup, if you want to use dynamic imports later (e.g., `await import('./GodViewRenderer')` so you don't load 5MB of Deck.gl/Arrow code for users who never visit the map page), it's drastically easier. 3. **Readability:** You don't have to scroll past 4,000 lines to find how `liveSocket` is connected.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
carverauto/serviceradar#1050
No description provided.