Files
OpenNoodl/dev-docs/future-projects/MULTI-PROJECT.md

383 lines
11 KiB
Markdown

# 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
```typescript
// 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 data
- `NodeLibrary.instance` - Node definitions (registers/unregisters per project)
- `CloudService.instance` - Cloud backend per project
- `ViewerConnection.instance` - Single preview connection
- `SidebarModel.instance`, `UndoQueue.instance`, etc.
#### 2. Router Enforces Single Project
The router explicitly disposes the old project when switching:
```typescript
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:
```javascript
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
1. **Regression Risk**: Touching ProjectModel singleton affects nearly every feature
2. **Memory Pressure**: Multiple full projects in RAM
3. **State Isolation**: Ensuring complete isolation between projects
4. **Performance**: Managing multiple preview servers
5. **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
```javascript
// 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:**
```javascript
// 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:**
```javascript
// 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
1. **Multiple dock icons**: May confuse some users
2. **Memory duplication**: Each instance loads full editor
3. **No cross-project features**: Can't drag nodes between projects
4. **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
1. `packages/noodl-editor/src/main/main.js` - Single instance lock, port config
2. `packages/noodl-editor/src/main/src/preview-server.js` (or equivalent) - Dynamic ports
### Supporting Changes
3. `packages/noodl-editor/src/main/src/StorageApi.js` - Keyed storage
4. `packages/noodl-editor/src/editor/src/pages/ProjectsPage/` - "Open in New Window" option
5. UDP multicast function in main.js - Instance awareness
---
## Open Questions
1. **Same project in multiple instances?**
- Recommend: Block with friendly message, or warn about conflicts
2. **Instance limit?**
- Recommend: No hard limit initially, monitor memory usage
3. **macOS app icon behavior?**
- Each instance shows in dock; standard behavior for multi-window apps
4. **File locking?**
- Noodl already handles project.json locking - verify behavior with multiple instances
---
## Appendix: Code Snippets
### Current Single-Instance Pattern (Likely)
```javascript
// 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
```javascript
// 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
```javascript
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));
});
});
}
```