asteroid/docs/PARENSCRIPT-EXPERIMENT.org

7.0 KiB

ParenScript Conversion Experiment

Overview

This branch experiments with converting all JavaScript files to ParenScript, allowing us to write client-side code in Common Lisp that compiles to JavaScript.

Goals

  • Replace all .js files with ParenScript equivalents
  • Maintain same functionality
  • Improve code maintainability by using one language (Lisp) for both frontend and backend
  • Take advantage of Lisp macros for client-side code generation

Current JavaScript Files

  • static/js/admin.js - Admin dashboard functionality
  • static/js/auth-ui.js - Authentication UI
  • static/js/front-page.js - Front page interactions
  • static/js/player.js - Audio player controls
  • static/js/profile.js - User profile page
  • static/js/users.js - User management

Implementation Plan

Phase 1: Setup [DONE]

  • Add ParenScript dependency to asteroid.asd
  • Create parenscript-utils.lisp with helper functions
  • Create experimental branch

Phase 2: Convert Simple Files First

  • Convert auth-ui.js (smallest, simplest) - COMPLETE
  • Convert front-page.js (stream quality, now playing, pop-out, frameset) - COMPLETE
  • Convert profile.js (user profile, stats, history) - COMPLETE
  • Convert users.js (user management, admin) - COMPLETE

Phase 3: Convert Complex Files

  • Convert player.js (audio player logic)
  • Convert admin.js (queue management, track controls)

Phase 4: Testing & Refinement

  • Test all functionality
  • Optimize generated JavaScript
  • Document ParenScript patterns used

Benefits

Code Reuse

  • Share utility functions between frontend and backend
  • Use same data structures and validation logic

Macros

  • Create domain-specific macros for common UI patterns
  • Generate repetitive JavaScript code programmatically

Type Safety

  • Catch more errors at compile time
  • Better IDE support with Lisp tooling

Maintainability

  • Single language for entire stack
  • Easier refactoring across frontend/backend boundary

Lessons Learned

auth-ui.js Conversion (2025-11-06)

Challenge 1: Route Precedence

Problem: Radiance routes are matched in load order, not definition order. The general static file route (/static/(.*)) was intercepting our specific ParenScript route.

Solution: Intercept the static file route and check if path is js/auth-ui.js. If yes, serve ParenScript; otherwise serve regular file.

Challenge 2: Async/Await Syntax

Problem: ParenScript doesn't support async/await syntax. Using (async lambda ...) generated invalid JavaScript.

Solution: Use promise chains with .then() instead of async/await.

Challenge 3: Compile Time vs Runtime

Problem: ParenScript compiler (ps:ps*) isn't available in saved binary at runtime.

Solution: Compile JavaScript at load time and store in a parameter. The function just returns the pre-compiled string.

Success Metrics

  • JavaScript compiles correctly (1386 characters)
  • No browser console errors
  • Auth UI works perfectly (show/hide elements based on login status)
  • Generated code is readable and maintainable

Key Patterns

Compile at load time:

(defparameter *my-js*
  (ps:ps* '(progn ...)))

(defun generate-my-js ()
  *my-js*)

Promise chains instead of async/await:

(ps:chain (fetch url)
          (then (lambda (response) (ps:chain response (json))))
          (then (lambda (data) (process data)))
          (catch (lambda (error) (handle error))))

Intercept static route:

(define-page static #@"/static/(.*)" (:uri-groups (path))
  (if (string= path "js/my-file.js")
      (serve-parenscript)
      (serve-static-file)))

Notes

This is an EXPERIMENTAL branch. The goal is to evaluate ParenScript for this project, not to immediately replace all JavaScript.

If successful, we can merge incrementally, one file at a time.

Conversion Progress

  • auth-ui.js (2025-11-06): Successfully converted. 1386 chars. All functionality working.
  • front-page.js (2025-11-06): Successfully converted. 6900 chars. Stream quality, now playing, pop-out player, frameset mode all working.
  • profile.js (2025-11-06): Successfully converted. Profile data, listening stats, recent tracks, top artists, password change all working.
  • users.js (2025-11-06): Successfully converted. User stats, user list, role changes, activate/deactivate, create user all working.

front-page.js Conversion Notes

This was a more complex file with multiple features. Key learnings:

Global Variables

ParenScript uses (defvar *variable-name* value) for global variables:

(defvar *popout-window* nil)

String Concatenation

Use + operator for string concatenation:

(+ "width=" width ",height=" height)

Conditional Logic

Use cond for multiple conditions in route interception:

(cond
  ((string= path "js/auth-ui.js") ...)
  ((string= path "js/front-page.js") ...)
  (t ...))

Object Property Access

Use ps:getprop for dynamic property access:

(ps:getprop config encoding)  ; config[encoding]

All features tested and working:

  • Stream quality selector changes stream correctly
  • Now playing updates every 10 seconds
  • Pop-out player functionality works
  • Frameset mode toggle works
  • Auto-reconnect on stream errors works

profile.js and users.js Conversion Notes

Modulo Operator

ParenScript doesn't support % for modulo. Use rem (remainder) instead:

;; WRONG:
(% seconds 3600)

;; CORRECT:
(rem seconds 3600)

Property Access with Hyphens

For properties with hyphens (like "last-login"), use ps:getprop:

(ps:getprop user "last-login")
;; Instead of (ps:@ user last-login)

Template Literals in HTML Generation

Build HTML strings with + concatenation:

(+ "<td>" (ps:@ user username) "</td>"
   "<td>" (ps:@ user email) "</td>")

Conditional Attributes

Use if expressions inline for conditional HTML attributes:

(+ "<option value=\"listener\" "
   (if (= (ps:@ user role) "listener") "selected" "")
   ">Listener</option>")

Summary of Key ParenScript Patterns

  1. Async/Await: Use promise chains with .then() instead
  2. Modulo: Use rem instead of %
  3. Global Variables: Use defvar with asterisks: *variable-name*
  4. String Concatenation: Use + operator
  5. Property Access: Use ps:getprop for dynamic/hyphenated properties
  6. Object Creation: Use ps:create with keyword arguments
  7. Array Methods: Use ps:chain for method chaining
  8. Route Interception: Use cond in static route handler
  9. Compile at Load Time: Store compiled JS in defparameter
  10. Return Pre-compiled String: Function just returns the parameter value