feat: update to new cluster protocol
This commit is contained in:
56
.cursor/rules/cleancode.mdc
Normal file
56
.cursor/rules/cleancode.mdc
Normal file
@@ -0,0 +1,56 @@
|
||||
---
|
||||
description: Guidelines for writing clean, maintainable, and human-readable code. Apply these rules when writing or reviewing code to ensure consistency and quality.
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Clean Code Guidelines
|
||||
|
||||
## Constants Over Magic Numbers
|
||||
- Replace hard-coded values with named constants
|
||||
- Use descriptive constant names that explain the value's purpose
|
||||
- Keep constants at the top of the file or in a dedicated constants file
|
||||
|
||||
## Meaningful Names
|
||||
- Variables, functions, and classes should reveal their purpose
|
||||
- Names should explain why something exists and how it's used
|
||||
- Avoid abbreviations unless they're universally understood
|
||||
|
||||
## Smart Comments
|
||||
- Don't comment on what the code does - make the code self-documenting
|
||||
- Use comments to explain why something is done a certain way
|
||||
- Document APIs, complex algorithms, and non-obvious side effects
|
||||
|
||||
## Single Responsibility
|
||||
- Each function should do exactly one thing
|
||||
- Functions should be small and focused
|
||||
- If a function needs a comment to explain what it does, it should be split
|
||||
|
||||
## DRY (Don't Repeat Yourself)
|
||||
- Extract repeated code into reusable functions
|
||||
- Share common logic through proper abstraction
|
||||
- Maintain single sources of truth
|
||||
|
||||
## Clean Structure
|
||||
- Keep related code together
|
||||
- Organize code in a logical hierarchy
|
||||
- Use consistent file and folder naming conventions
|
||||
|
||||
## Encapsulation
|
||||
- Hide implementation details
|
||||
- Expose clear interfaces
|
||||
- Move nested conditionals into well-named functions
|
||||
|
||||
## Code Quality Maintenance
|
||||
- Refactor continuously
|
||||
- Fix technical debt early
|
||||
- Leave code cleaner than you found it
|
||||
|
||||
## Testing
|
||||
- Write tests before fixing bugs
|
||||
- Keep tests readable and maintainable
|
||||
- Test edge cases and error conditions
|
||||
|
||||
## Version Control
|
||||
- Write clear commit messages
|
||||
- Make small, focused commits
|
||||
- Use meaningful branch names
|
||||
111
.cursor/rules/gitflow.mdc
Normal file
111
.cursor/rules/gitflow.mdc
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
description: Gitflow Workflow Rules. These rules should be applied when performing git operations.
|
||||
---
|
||||
# Gitflow Workflow Rules
|
||||
|
||||
## Main Branches
|
||||
|
||||
### main (or master)
|
||||
- Contains production-ready code
|
||||
- Never commit directly to main
|
||||
- Only accepts merges from:
|
||||
- hotfix/* branches
|
||||
- release/* branches
|
||||
- Must be tagged with version number after each merge
|
||||
|
||||
### develop
|
||||
- Main development branch
|
||||
- Contains latest delivered development changes
|
||||
- Source branch for feature branches
|
||||
- Never commit directly to develop
|
||||
|
||||
## Supporting Branches
|
||||
|
||||
### feature/*
|
||||
- Branch from: develop
|
||||
- Merge back into: develop
|
||||
- Naming convention: feature/[issue-id]-descriptive-name
|
||||
- Example: feature/123-user-authentication
|
||||
- Must be up-to-date with develop before creating PR
|
||||
- Delete after merge
|
||||
|
||||
### release/*
|
||||
- Branch from: develop
|
||||
- Merge back into:
|
||||
- main
|
||||
- develop
|
||||
- Naming convention: release/vX.Y.Z
|
||||
- Example: release/v1.2.0
|
||||
- Only bug fixes, documentation, and release-oriented tasks
|
||||
- No new features
|
||||
- Delete after merge
|
||||
|
||||
### hotfix/*
|
||||
- Branch from: main
|
||||
- Merge back into:
|
||||
- main
|
||||
- develop
|
||||
- Naming convention: hotfix/vX.Y.Z
|
||||
- Example: hotfix/v1.2.1
|
||||
- Only for urgent production fixes
|
||||
- Delete after merge
|
||||
|
||||
## Commit Messages
|
||||
|
||||
- Format: `type(scope): description`
|
||||
- Types:
|
||||
- feat: New feature
|
||||
- fix: Bug fix
|
||||
- docs: Documentation changes
|
||||
- style: Formatting, missing semicolons, etc.
|
||||
- refactor: Code refactoring
|
||||
- test: Adding tests
|
||||
- chore: Maintenance tasks
|
||||
|
||||
## Version Control
|
||||
|
||||
### Semantic Versioning
|
||||
- MAJOR version for incompatible API changes
|
||||
- MINOR version for backwards-compatible functionality
|
||||
- PATCH version for backwards-compatible bug fixes
|
||||
|
||||
## Pull Request Rules
|
||||
|
||||
1. All changes must go through Pull Requests
|
||||
2. Required approvals: minimum 1
|
||||
3. CI checks must pass
|
||||
4. No direct commits to protected branches (main, develop)
|
||||
5. Branch must be up to date before merging
|
||||
6. Delete branch after merge
|
||||
|
||||
## Branch Protection Rules
|
||||
|
||||
### main & develop
|
||||
- Require pull request reviews
|
||||
- Require status checks to pass
|
||||
- Require branches to be up to date
|
||||
- Include administrators in restrictions
|
||||
- No force pushes
|
||||
- No deletions
|
||||
|
||||
## Release Process
|
||||
|
||||
1. Create release branch from develop
|
||||
2. Bump version numbers
|
||||
3. Fix any release-specific issues
|
||||
4. Create PR to main
|
||||
5. After merge to main:
|
||||
- Tag release
|
||||
- Merge back to develop
|
||||
- Delete release branch
|
||||
|
||||
## Hotfix Process
|
||||
|
||||
1. Create hotfix branch from main
|
||||
2. Fix the issue
|
||||
3. Bump patch version
|
||||
4. Create PR to main
|
||||
5. After merge to main:
|
||||
- Tag release
|
||||
- Merge back to develop
|
||||
- Delete hotfix branch
|
||||
119
package-lock.json
generated
119
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dgram": "^1.0.1",
|
||||
"axios": "^1.6.0",
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.14.2"
|
||||
}
|
||||
@@ -33,6 +33,23 @@
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
"integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.4",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
@@ -95,6 +112,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
@@ -140,6 +169,15 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -159,13 +197,6 @@
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dgram": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dgram/-/dgram-1.0.1.tgz",
|
||||
"integrity": "sha512-zJVFL1EWfKtE0z2VN6qfpn/a+qG1viEzcwJA0EjtzS76ONSE3sEyWBwEbo32hS4IFw/EWVuWN+8b89aPW6It2A==",
|
||||
"deprecated": "npm is holding this package for security reasons. As it's a core Node module, we will not transfer it over to other users. You may safely remove the package from your dependencies.",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@@ -225,6 +256,21 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -304,6 +350,42 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -392,6 +474,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
@@ -583,6 +680,12 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
|
||||
@@ -462,6 +462,7 @@ class LEDLabServer {
|
||||
const frameData = stream.preset.generateFrame();
|
||||
if (frameData) {
|
||||
// Send to specific node
|
||||
// frameData format: "RAW:FF0000FF0000..." (RAW prefix + hex pixel data)
|
||||
this.udpDiscovery.sendToNode(nodeIp, frameData);
|
||||
|
||||
// Send frame data to WebSocket clients for preview
|
||||
|
||||
@@ -91,50 +91,110 @@ class UdpDiscovery extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update node last seen time
|
||||
// Handle different message types
|
||||
if (message.startsWith('cluster/heartbeat:')) {
|
||||
// Extract hostname from heartbeat: "cluster/heartbeat:hostname"
|
||||
const hostname = message.substring('cluster/heartbeat:'.length);
|
||||
this.handleHeartbeat(hostname, nodeIp, rinfo.port);
|
||||
} else if (message.startsWith('node/update:')) {
|
||||
// Extract hostname and JSON from update: "node/update:hostname:{json}"
|
||||
const parts = message.substring('node/update:'.length).split(':');
|
||||
if (parts.length >= 2) {
|
||||
const hostname = parts[0];
|
||||
const jsonStr = parts.slice(1).join(':'); // Rejoin in case JSON contains colons
|
||||
this.handleNodeUpdate(hostname, jsonStr, nodeIp, rinfo.port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleHeartbeat(hostname, nodeIp, port) {
|
||||
console.log(`Heartbeat from ${hostname} @ ${nodeIp}`);
|
||||
|
||||
// Update or add node
|
||||
const existingNode = this.nodes.get(nodeIp);
|
||||
this.nodes.set(nodeIp, {
|
||||
lastSeen: Date.now(),
|
||||
status: 'connected',
|
||||
address: nodeIp,
|
||||
port: rinfo.port
|
||||
port: port,
|
||||
hostname: hostname
|
||||
});
|
||||
|
||||
// Emit node discovered/updated event
|
||||
// Only emit if this is a new node or if we need to update
|
||||
if (!existingNode || existingNode.hostname !== hostname) {
|
||||
this.emit('nodeDiscovered', {
|
||||
ip: nodeIp,
|
||||
port: rinfo.port,
|
||||
hostname: hostname,
|
||||
port: port,
|
||||
status: 'connected'
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up stale nodes periodically
|
||||
this.cleanupStaleNodes();
|
||||
}
|
||||
|
||||
startDiscoveryBroadcast() {
|
||||
// Broadcast discovery message every 5 seconds
|
||||
this.discoveryInterval = setInterval(() => {
|
||||
this.broadcastDiscovery();
|
||||
}, 5000);
|
||||
handleNodeUpdate(hostname, jsonStr, nodeIp, port) {
|
||||
console.log(`Node update from ${hostname} @ ${nodeIp}`);
|
||||
|
||||
// Send initial broadcast
|
||||
this.broadcastDiscovery();
|
||||
// Try to parse JSON to extract additional info
|
||||
let nodeInfo = {};
|
||||
try {
|
||||
nodeInfo = JSON.parse(jsonStr);
|
||||
} catch (e) {
|
||||
console.warn(`Failed to parse node update JSON: ${e.message}`);
|
||||
}
|
||||
|
||||
broadcastDiscovery() {
|
||||
// Update node with hostname and any additional info
|
||||
const existingNode = this.nodes.get(nodeIp);
|
||||
this.nodes.set(nodeIp, {
|
||||
lastSeen: Date.now(),
|
||||
status: 'connected',
|
||||
address: nodeIp,
|
||||
port: port,
|
||||
hostname: hostname || nodeInfo.hostname || existingNode?.hostname,
|
||||
...nodeInfo
|
||||
});
|
||||
|
||||
// Emit update event
|
||||
this.emit('nodeDiscovered', {
|
||||
ip: nodeIp,
|
||||
hostname: hostname || nodeInfo.hostname || existingNode?.hostname,
|
||||
port: port,
|
||||
status: 'connected'
|
||||
});
|
||||
}
|
||||
|
||||
startDiscoveryBroadcast() {
|
||||
// With the new protocol, SPORE nodes automatically broadcast heartbeats
|
||||
// LEDLab passively listens for these heartbeats, so we don't need to broadcast.
|
||||
// However, we can optionally send a heartbeat to prompt nodes to respond faster.
|
||||
// For now, we just listen for incoming heartbeats from nodes.
|
||||
|
||||
// Optional: send initial heartbeat to prompt nodes to announce themselves
|
||||
this.broadcastHeartbeat();
|
||||
|
||||
// Send periodic heartbeats to prompt node announcements (every 10 seconds)
|
||||
this.discoveryInterval = setInterval(() => {
|
||||
this.broadcastHeartbeat();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
broadcastHeartbeat() {
|
||||
if (!this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
const discoveryMessage = 'SPORE_DISCOVERY';
|
||||
// Send heartbeat using the new protocol format: "cluster/heartbeat:hostname"
|
||||
const hostname = 'ledlab-client';
|
||||
const discoveryMessage = `cluster/heartbeat:${hostname}`;
|
||||
const message = Buffer.from(discoveryMessage, 'utf8');
|
||||
|
||||
// Broadcast to all nodes on the network (broadcast already enabled in bind callback)
|
||||
|
||||
this.socket.send(message, 0, message.length, this.port, '255.255.255.255', (err) => {
|
||||
if (err) {
|
||||
console.error('Error broadcasting discovery message:', err);
|
||||
console.error('Error broadcasting heartbeat:', err);
|
||||
} else {
|
||||
console.log('Discovery message broadcasted');
|
||||
console.log('Discovery heartbeat broadcasted');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -154,6 +214,7 @@ class UdpDiscovery extends EventEmitter {
|
||||
getNodes() {
|
||||
const nodes = Array.from(this.nodes.entries()).map(([ip, node]) => ({
|
||||
ip,
|
||||
hostname: node.hostname || ip,
|
||||
...node
|
||||
}));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user