11 KiB
Multi-Project Support Scoping Document
Executive Summary
This document scopes the feature request to enable OpenNoodl to have multiple projects open simultaneously. Two approaches are analyzed: multi-project within a single Electron app, and multiple Electron app instances.
Recommendation: Start with Option B (Multiple Electron Instances) as Phase 1 due to significantly lower complexity and risk. Consider Option A as a future enhancement if user demand warrants the investment.
Current Architecture Analysis
Key Findings
The codebase has several architectural patterns that make multi-project support challenging:
1. Singleton Pattern Throughout
// ProjectModel is a strict singleton
public static get instance() {
return ProjectModel._instance;
}
public static set instance(project: ProjectModel | undefined) {
// Only one project at a time...
}
This pattern is referenced extensively across the codebase:
ProjectModel.instance- Core project dataNodeLibrary.instance- Node definitions (registers/unregisters per project)CloudService.instance- Cloud backend per projectViewerConnection.instance- Single preview connectionSidebarModel.instance,UndoQueue.instance, etc.
2. Router Enforces Single Project
The router explicitly disposes the old project when switching:
route(args: AppRouteOptions) {
if (ProjectModel.instance && ProjectModel.instance !== args.project) {
ProjectModel.instance.dispose();
// ...
ProjectModel.instance = undefined;
}
}
3. IPC Assumes Single Project
Main process IPC events like project-opened and project-closed assume one active project:
ipcMain.on('project-opened', (e, newProjectName) => {
projectName = newProjectName; // Single name tracked
// ...
});
4. Viewer Window is Tightly Coupled
The viewer window is a child of the main window with direct IPC communication assuming a single project context.
Option A: Multi-Project Within Single Electron App
Description
Transform the architecture to support multiple projects open as tabs or panels within a single application window.
Required Changes
Phase A1: Core Architecture Refactoring
| Component | Current State | Required Change | Complexity |
|---|---|---|---|
ProjectModel |
Singleton | Registry with active project tracking | 🔴 High |
NodeLibrary |
Singleton with project registration | Per-project library instances | 🔴 High |
EventDispatcher |
Global events | Project-scoped events | 🟡 Medium |
UndoQueue |
Singleton | Per-project undo stacks | 🟡 Medium |
Router |
Single route | Multi-route or tab system | 🔴 High |
ViewerConnection |
Single connection | Connection pool by project | 🟡 Medium |
Phase A2: UI/UX Development
- Tab bar or project switcher component
- Visual indicators for active project
- Window management (detach projects to separate windows)
- Cross-project drag & drop considerations
Phase A3: Resource Management
- Memory management for multiple loaded projects
- Preview server port allocation per project
- Cloud service connection pooling
- File watcher consolidation
Effort Estimate
| Phase | Estimated Time | Risk Level |
|---|---|---|
| A1: Core Architecture | 8-12 weeks | 🔴 High |
| A2: UI/UX | 3-4 weeks | 🟡 Medium |
| A3: Resource Management | 2-3 weeks | 🟡 Medium |
| Testing & Stabilization | 3-4 weeks | 🔴 High |
| Total | 16-23 weeks | High |
Risks
- Regression Risk: Touching ProjectModel singleton affects nearly every feature
- Memory Pressure: Multiple full projects in RAM
- State Isolation: Ensuring complete isolation between projects
- Performance: Managing multiple preview servers
- Complexity Explosion: Every new feature must consider multi-project context
Benefits
- Single dock icon / application instance
- Potential for cross-project features (copy/paste between projects)
- Professional multi-document interface
- Shared resources (single node library load)
Option B: Multiple Electron App Instances
Description
Allow multiple independent Electron app instances, each with its own project. Minimal code changes required.
Required Changes
Phase B1: Enable Multi-Instance
// Current: Single instance lock (likely present)
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
}
// Change to: Allow multiple instances
// Simply remove or conditionally bypass the single-instance lock
Phase B2: Instance Isolation
| Component | Change Required | Complexity |
|---|---|---|
| Single-instance lock | Remove or make conditional | 🟢 Low |
| Preview server ports | Dynamic port allocation | 🟢 Low |
| UDP broadcast | Include instance ID | 🟢 Low |
| Window bounds storage | Per-project storage key | 🟢 Low |
| Design tool import server | Instance-aware | 🟡 Medium |
Phase B3: UX Polish (Optional)
- Menu item: "Open Project in New Window"
- Keyboard shortcut support
- Recent projects list per instance awareness
Implementation Details
Port Allocation:
// Instead of fixed port:
// const port = Config.PreviewServer.port;
// Use dynamic allocation:
const server = net.createServer();
server.listen(0); // OS assigns available port
const port = server.address().port;
Window Bounds:
// Key by project directory or ID
const boundsKey = `windowBounds_${projectId}`;
jsonstorage.get(boundsKey, (bounds) => { /* ... */ });
Effort Estimate
| Phase | Estimated Time | Risk Level |
|---|---|---|
| B1: Multi-Instance | 1-2 days | 🟢 Low |
| B2: Instance Isolation | 3-5 days | 🟢 Low |
| B3: UX Polish | 3-5 days | 🟢 Low |
| Testing | 2-3 days | 🟢 Low |
| Total | 2-3 weeks | Low |
Risks
- Multiple dock icons: May confuse some users
- Memory duplication: Each instance loads full editor
- No cross-project features: Can't drag nodes between projects
- OS Integration: May complicate app bundling/signing
Benefits
- Minimal code changes
- Complete isolation (no state bleed)
- Each project has dedicated resources
- Can close one project without affecting others
- Already supported pattern in many apps (VS Code, terminal apps)
Comparison Matrix
| Criteria | Option A (Single App) | Option B (Multi-Instance) |
|---|---|---|
| Development Time | 16-23 weeks | 2-3 weeks |
| Risk Level | 🔴 High | 🟢 Low |
| Code Changes | Extensive refactoring | Minimal, isolated changes |
| Memory Usage | Shared (more efficient) | Duplicated (less efficient) |
| UX Polish | Professional tabbed interface | Multiple windows/dock icons |
| Cross-Project Features | Possible | Not possible |
| Isolation | Requires careful engineering | Automatic |
| Maintenance Burden | Higher (ongoing complexity) | Lower |
Recommendation
Phase 1: Multiple Electron Instances (Option B)
Timeline: 2-3 weeks
Start here because:
- Low risk, high value
- Validates user need before major investment
- Can ship quickly and gather feedback
- Doesn't block future Option A work
Phase 2 (Future): Evaluate Single-App Approach
Timeline: After 6+ months of Phase 1 feedback
Consider Option A if:
- Users strongly request tabbed interface
- Cross-project features become a priority
- Memory usage becomes a significant concern
- User feedback indicates multiple windows is problematic
Implementation Plan for Option B
Week 1: Core Multi-Instance Support
Day 1-2: Single Instance Lock
- Locate and understand current single-instance handling
- Add configuration flag
allowMultipleInstances - Test launching multiple instances manually
Day 3-4: Port Allocation
- Modify preview server to use dynamic ports
- Update ViewerConnection to handle dynamic ports
- Test multiple instances with different projects
Day 5: Basic Testing
- Test simultaneous editing of different projects
- Verify no state leakage between instances
- Check cloud service isolation
Week 2: Polish & Edge Cases
Day 1-2: Storage Isolation
- Key window bounds by project ID/path
- Handle recent projects list updates
- UDP broadcast instance differentiation
Day 3-4: UX Improvements
- Add "Open in New Window" to project context menu
- Keyboard shortcut for opening new instance
- Window title includes project name prominently
Day 5: Documentation & Testing
- Update user documentation
- Edge case testing (same project in two instances)
- Memory and performance profiling
Week 3: Buffer & Release
- Bug fixes from testing
- Final QA pass
- Release notes preparation
- User feedback collection setup
Files to Modify (Option B)
Critical Path
packages/noodl-editor/src/main/main.js- Single instance lock, port configpackages/noodl-editor/src/main/src/preview-server.js(or equivalent) - Dynamic ports
Supporting Changes
packages/noodl-editor/src/main/src/StorageApi.js- Keyed storagepackages/noodl-editor/src/editor/src/pages/ProjectsPage/- "Open in New Window" option- UDP multicast function in main.js - Instance awareness
Open Questions
-
Same project in multiple instances?
- Recommend: Block with friendly message, or warn about conflicts
-
Instance limit?
- Recommend: No hard limit initially, monitor memory usage
-
macOS app icon behavior?
- Each instance shows in dock; standard behavior for multi-window apps
-
File locking?
- Noodl already handles project.json locking - verify behavior with multiple instances
Appendix: Code Snippets
Current Single-Instance Pattern (Likely)
// main.js - probable current implementation
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// Focus existing window when second instance attempted
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
}
});
}
Proposed Multi-Instance Support
// main.js - proposed modification
const allowMultipleInstances = true; // Could be a setting
if (!allowMultipleInstances) {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.quit();
return;
}
app.on('second-instance', (event, commandLine, workingDirectory) => {
if (win) {
if (win.isMinimized()) win.restore();
win.focus();
}
});
}
// Rest of initialization continues for each instance...
Dynamic Port Allocation
const net = require('net');
function findAvailablePort(startPort = 8574) {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(startPort, () => {
const port = server.address().port;
server.close(() => resolve(port));
});
server.on('error', () => {
// Port in use, try next
resolve(findAvailablePort(startPort + 1));
});
});
}