mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c9cf7c49dc | |||
| bcbd46d658 | |||
| 4d489ec992 |
@@ -52,19 +52,17 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Sync Protocol Constants
|
||||
- name: Build Extensions
|
||||
run: |
|
||||
chmod +x ./scripts/sync-constants.sh
|
||||
./scripts/sync-constants.sh
|
||||
|
||||
- name: Create Extension Zip
|
||||
run: |
|
||||
zip -r koala-sync-extension.zip extension/ -x "*.DS_Store*"
|
||||
npm install
|
||||
npm run build:extension
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: koala-sync-extension.zip
|
||||
files: |
|
||||
dist/koalasync-chrome.zip
|
||||
dist/koalasync-firefox.zip
|
||||
name: Release ${{ github.ref_name }}
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
|
||||
+3
-2
@@ -23,7 +23,7 @@ KoalaSync is a specialized tool for **synchronized video playback** across multi
|
||||
- `docker-compose.yml`: Root-level orchestration for the relay server.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Single Source of Truth**: `shared/constants.js` and `shared/blacklist.js` are the master files. They must be synchronized to the `extension/shared/` directory using `.\scripts\sync-constants.bat` or `./scripts/sync-constants.sh`.
|
||||
> **Single Source of Truth**: `shared/constants.js` and `shared/blacklist.js` are the master files. They must be synchronized to the `extension/shared/` directory using `node scripts/build-extension.js`.
|
||||
> - **Extension Modules** (`background.js`, `popup.js`) import directly from `./shared/constants.js`.
|
||||
> - **Content Scripts** (`content.js`) use a **manual synchronous mirror** to prevent race conditions during page load. Always verify parity after sync.
|
||||
|
||||
@@ -56,6 +56,7 @@ The popup UI follows a strict design system. Do not modify these variables or th
|
||||
## 5. Non-Negotiables (Core Logic)
|
||||
The following features are critical and must not be removed or fundamentally altered:
|
||||
- **Two-Phase Force Sync**: The `Prepare` → `ACK` → `Execute` flow ensures all peers are buffered before playback resumes.
|
||||
- **Episode Auto-Sync**: Ensures series binges stay perfectly synced. A lobby initiates during title transitions, freezing peers until everyone is ready.
|
||||
- **Dual Heartbeat**:
|
||||
- **Background Heartbeat (30s)**: Ensures session persistence even without a video element.
|
||||
- **Content Heartbeat (15s)**: Transmits current video metadata (time, title).
|
||||
@@ -84,7 +85,7 @@ The following features are critical and must not be removed or fundamentally alt
|
||||
|
||||
### Adding a Protocol Event
|
||||
1. Add the event name to `shared/constants.js`.
|
||||
2. Run the sync script (`.\scripts\sync-constants.bat` or `./scripts/sync-constants.sh`).
|
||||
2. Run the build script (`node scripts/build-extension.js`).
|
||||
3. Implement the handler in `server/index.js` and `background.js`.
|
||||
|
||||
### Testing Locally
|
||||
|
||||
+13
-5
@@ -25,25 +25,33 @@ When a user interacts with a video:
|
||||
Ensures all peers are frame-perfect and buffered before resuming:
|
||||
1. **Prepare**: Initiator sends `FORCE_SYNC_PREPARE` with the target timestamp.
|
||||
2. **Buffer**: Peers seek and pause. Once buffered (`readyState >= 3`), they send a `FORCE_SYNC_ACK`.
|
||||
3. **Execute**: Once the Initiator collects ACKs (or after a 5s timeout), they send `FORCE_SYNC_EXECUTE`.
|
||||
3. **Execute**: Once the Initiator collects ACKs (or after an 8.5s timeout), they send `FORCE_SYNC_EXECUTE`.
|
||||
4. **Resume**: All peers call `play()` simultaneously.
|
||||
|
||||
## 4. Peer Lifecycle & Dual Heartbeat
|
||||
## 4. Episode Auto-Sync
|
||||
Maintains continuous synchronized viewing when watching series:
|
||||
1. **Detection**: `content.js` monitors the Media Session API for title changes.
|
||||
2. **Lobby Creation**: When a new title is detected, the peer initiates an `EPISODE_LOBBY` and broadcasts the new title.
|
||||
3. **Wait State**: All peers freeze their video until they have also loaded the exact same title.
|
||||
4. **Mid-Lobby Joins**: If a new user joins the room during an active lobby, the lobby initiator broadcasts the active lobby state so the newcomer can sync up.
|
||||
5. **Resume**: Once all peers report `EPISODE_READY`, the lobby is resolved and playback resumes perfectly.
|
||||
|
||||
## 5. Peer Lifecycle & Dual Heartbeat
|
||||
To maintain a clean room state and eliminate "Ghost Peers":
|
||||
- **Session Heartbeat (Background)**: Every 30 seconds, `background.js` sends an "I'm alive" signal to the server. This keeps you in the room even if no video is playing.
|
||||
- **Video Heartbeat (Content)**: Every 15 seconds, `content.js` sends current playback metadata (time, title, state) if a video is found.
|
||||
- **Server Pruning**: The server runs a "Reaper" every 2 minutes. If a peer has sent **zero** activity (no events and no heartbeats) for 5 minutes, they are forcefully disconnected.
|
||||
- **Immediate Cleanup**: Rooms are deleted instantly when the last peer leaves or disconnects.
|
||||
|
||||
## 5. Security & Stability
|
||||
## 6. Security & Stability
|
||||
- **Service Worker Lifecycle**: Uses `chrome.alarms` to prevent the Manifest V3 service worker from suspending while in an active room.
|
||||
- **Rate Limiting**: Server-side per-socket and per-IP rate limits to prevent sync-spamming or DoS.
|
||||
- **Noise Filtering**: Uses a curated blacklist of domains (Search Engines, Social Media) to declutter the "Target Tab" selector in the popup.
|
||||
- **Diagnostics**: A "Dev" tab provides real-time access to the underlying `<video>` state (`readyState`, `paused`, `currentTime`) for easier troubleshooting.
|
||||
|
||||
## 6. Constant Synchronization & Consistency
|
||||
## 7. Constant Synchronization & Consistency
|
||||
To maintain a "Single Source of Truth" across the server and extension without using a bundler:
|
||||
- **Relay Server & Extension Modules**: `background.js` and `popup.js` import constants directly from `shared/constants.js`.
|
||||
- **Content Scripts**: To ensure zero-latency execution, `content.js` uses a manual mirror of `EVENTS`.
|
||||
- **Synchronization**: The `./scripts/sync-constants.sh` script ensures that the `shared/` folder within the `extension/` directory is kept up-to-date with the root `shared/` source.
|
||||
- **Synchronization**: The `node scripts/build-extension.js` script ensures that the `shared/` folder within the `extension/` directory is kept up-to-date with the root `shared/` source while packaging artifacts.
|
||||
- **Verification**: Any protocol change requires a manual verification sweep across all three constant locations (Shared, Server, and Content Script Mirror).
|
||||
|
||||
@@ -41,9 +41,9 @@ docker-compose up -d --build
|
||||
The server will be available at `ws://localhost:3000`.
|
||||
|
||||
### 2. Chrome Extension
|
||||
1. **Synchronize Protocol**: From the root directory, run the sync script to copy the master constants to the extension folder:
|
||||
1. **Synchronize Protocol**: From the root directory, run the build script to copy the master constants and prepare the extension:
|
||||
```bash
|
||||
./scripts/sync-constants.sh
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
2. Open Chrome and go to `chrome://extensions/`.
|
||||
3. Enable **Developer mode** (top right).
|
||||
|
||||
+10
-17
@@ -5,8 +5,8 @@ KoalaSync uses a "Single Source of Truth" for its communication protocol constan
|
||||
|
||||
To ensure that the extension and the relay server are always using the exact same event names and protocol versions, we maintain a mirrored copy of the shared files within the `extension/shared/` folder.
|
||||
|
||||
## When should you run the sync script?
|
||||
You MUST run the synchronization script in any of the following scenarios:
|
||||
## When should you run the build script?
|
||||
You MUST run the build script in any of the following scenarios:
|
||||
1. **After a fresh `git clone` or `git pull`** (as the synced files are ignored by git).
|
||||
2. **After modifying** `shared/constants.js`.
|
||||
3. **After modifying** `shared/blacklist.js`.
|
||||
@@ -15,29 +15,22 @@ You MUST run the synchronization script in any of the following scenarios:
|
||||
|
||||
## How to sync
|
||||
|
||||
### On Windows
|
||||
Run the batch script from the repository root:
|
||||
```powershell
|
||||
.\scripts\sync-constants.bat
|
||||
```
|
||||
|
||||
### On macOS / Linux
|
||||
Run the shell script from the repository root:
|
||||
Run the Node.js build script from the repository root:
|
||||
```bash
|
||||
./scripts/sync-constants.sh
|
||||
node scripts/build-extension.js
|
||||
```
|
||||
|
||||
## What does it do?
|
||||
The script performs the following actions:
|
||||
1. Ensures the `extension/shared/` directory exists.
|
||||
2. Copies `shared/constants.js` to `extension/shared/constants.js`.
|
||||
3. Copies `shared/blacklist.js` to `extension/shared/blacklist.js`.
|
||||
The build script performs the following actions:
|
||||
1. Synchronizes protocol constants by copying `shared/constants.js` and `shared/blacklist.js` into `extension/shared/`.
|
||||
2. Compiles browser-specific manifest files.
|
||||
3. Packages the final ready-to-publish extension artifacts for Chrome and Firefox into the `dist/` directory.
|
||||
|
||||
## Protocol Versioning
|
||||
As of v1.0.0-RC5, the system enforces a strict `protocolVersion` check during the `JOIN_ROOM` handshake.
|
||||
- The version is defined in `shared/constants.js`.
|
||||
- If the extension and server versions mismatch, the server will reject the connection with an `Incompatible protocol version` error.
|
||||
- **Always run the sync script** after bumping the version number to ensure both components are updated.
|
||||
- **Always run the build script** after bumping the version number to ensure both components are updated.
|
||||
|
||||
> [!CAUTION]
|
||||
> **NEVER** edit the files inside `extension/shared/` directly. They will be overwritten the next time the sync script is run. Always edit the files in the root `shared/` directory and then run the sync script.
|
||||
> **NEVER** edit the files inside `extension/shared/` directly. They will be overwritten the next time the build script is run. Always edit the files in the root `shared/` directory and then run the build script.
|
||||
|
||||
+15
-4
@@ -542,6 +542,10 @@ function handleServerEvent(event, data) {
|
||||
currentRoom.peers.push(createPeerData(data));
|
||||
if (storageInitialized) chrome.storage.session.set({ currentRoom });
|
||||
chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
|
||||
|
||||
if (episodeLobby && episodeLobby.initiatorPeerId === peerId) {
|
||||
emit(EVENTS.EPISODE_LOBBY, { peerId, expectedTitle: episodeLobby.expectedTitle });
|
||||
}
|
||||
}
|
||||
} else if (data.status === 'left') {
|
||||
currentRoom.peers = currentRoom.peers.filter(p => (p.peerId || p) !== data.peerId);
|
||||
@@ -552,6 +556,13 @@ function handleServerEvent(event, data) {
|
||||
if (episodeLobby) {
|
||||
checkEpisodeLobbyPeerDeparture();
|
||||
}
|
||||
|
||||
if (isForceSyncInitiator) {
|
||||
const peerCount = currentRoom.peers ? currentRoom.peers.length : 1;
|
||||
if (forceSyncAcks.size >= peerCount) {
|
||||
executeForceSync();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Heartbeat/Update: Update tabTitle for matching
|
||||
const peer = currentRoom.peers.find(p => (p.peerId || p) === data.peerId);
|
||||
@@ -691,7 +702,7 @@ function executeEpisodeLobby() {
|
||||
// Trigger a standard Force Sync at targetTime 0.0
|
||||
isForceSyncInitiator = true;
|
||||
forceSyncAcks.clear();
|
||||
const deadline = Date.now() + 5000;
|
||||
const deadline = Date.now() + 8500;
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: true,
|
||||
forceSyncAcks: [],
|
||||
@@ -707,7 +718,7 @@ function executeEpisodeLobby() {
|
||||
addLog('Force Sync (Episode): Timeout waiting for ACKs, executing anyway...', 'warn');
|
||||
executeForceSync();
|
||||
}
|
||||
}, 5000);
|
||||
}, 8500);
|
||||
}
|
||||
|
||||
function checkEpisodeLobbyCompletion() {
|
||||
@@ -943,7 +954,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
if (message.action === EVENTS.FORCE_SYNC_PREPARE) {
|
||||
isForceSyncInitiator = true;
|
||||
forceSyncAcks.clear();
|
||||
const deadline = Date.now() + 5000;
|
||||
const deadline = Date.now() + 8500;
|
||||
chrome.storage.session.set({
|
||||
isForceSyncInitiator: true,
|
||||
forceSyncAcks: [],
|
||||
@@ -958,7 +969,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
||||
addLog('Force Sync: Timeout waiting for ACKs, executing anyway...', 'warn');
|
||||
executeForceSync();
|
||||
}
|
||||
}, 5000);
|
||||
}, 8500);
|
||||
}
|
||||
addToHistory(message.action, 'You');
|
||||
emit(message.action, { ...message.payload, peerId });
|
||||
|
||||
@@ -22,10 +22,6 @@
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["https://koalasync.shik3i.net/*"],
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "koalasync",
|
||||
"version": "1.2.1",
|
||||
"description": "KoalaSync Build Scripts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build:extension": "node scripts/build-extension.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
"fs-extra": "^11.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const archiver = require('archiver');
|
||||
|
||||
const rootDir = path.join(__dirname, '..');
|
||||
const extDir = path.join(rootDir, 'extension');
|
||||
const distDir = path.join(rootDir, 'dist');
|
||||
const baseManifestPath = path.join(extDir, 'manifest.base.json');
|
||||
|
||||
// Ensure dist directory exists
|
||||
if (fs.existsSync(distDir)) {
|
||||
fs.rmSync(distDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
|
||||
// Sync shared constants from root /shared to /extension/shared
|
||||
console.log('Syncing protocol constants...');
|
||||
const masterSharedDir = path.join(rootDir, 'shared');
|
||||
const extSharedDir = path.join(extDir, 'shared');
|
||||
|
||||
if (!fs.existsSync(extSharedDir)) {
|
||||
fs.mkdirSync(extSharedDir, { recursive: true });
|
||||
}
|
||||
|
||||
const sharedFiles = ['constants.js', 'blacklist.js'];
|
||||
for (const file of sharedFiles) {
|
||||
const src = path.join(masterSharedDir, file);
|
||||
const dest = path.join(extSharedDir, file);
|
||||
if (!fs.existsSync(src)) {
|
||||
throw new Error(`CRITICAL: Source shared file missing: ${src}. Aborting build to prevent broken artifacts.`);
|
||||
}
|
||||
fs.copyFileSync(src, dest);
|
||||
}
|
||||
console.log('✓ constants.js and blacklist.js synced to extension/shared/');
|
||||
|
||||
// Read the base manifest
|
||||
const baseManifest = JSON.parse(fs.readFileSync(baseManifestPath, 'utf8'));
|
||||
|
||||
// Helper to copy files, ignoring manifest.json and manifest.base.json
|
||||
function copyExtensionFiles(targetDir) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
const items = fs.readdirSync(extDir);
|
||||
for (const item of items) {
|
||||
if (item === 'manifest.json' || item === 'manifest.base.json') continue;
|
||||
const srcPath = path.join(extDir, item);
|
||||
const destPath = path.join(targetDir, item);
|
||||
if (fs.lstatSync(srcPath).isDirectory()) {
|
||||
fs.cpSync(srcPath, destPath, { recursive: true });
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to zip a directory
|
||||
function zipDirectory(sourceDir, outPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
const stream = fs.createWriteStream(outPath);
|
||||
|
||||
archive
|
||||
.directory(sourceDir, false)
|
||||
.on('error', err => reject(err))
|
||||
.pipe(stream);
|
||||
|
||||
stream.on('close', () => resolve());
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
|
||||
async function buildBrowser(browserName, manifestModifier) {
|
||||
console.log(`Building for ${browserName}...`);
|
||||
const browserDistDir = path.join(distDir, browserName);
|
||||
|
||||
// 1. Copy files
|
||||
copyExtensionFiles(browserDistDir);
|
||||
|
||||
// 2. Modify and write manifest
|
||||
const browserManifest = manifestModifier(JSON.parse(JSON.stringify(baseManifest)));
|
||||
fs.writeFileSync(
|
||||
path.join(browserDistDir, 'manifest.json'),
|
||||
JSON.stringify(browserManifest, null, 2)
|
||||
);
|
||||
|
||||
// 3. Zip it
|
||||
const zipPath = path.join(distDir, `koalasync-${browserName}.zip`);
|
||||
await zipDirectory(browserDistDir, zipPath);
|
||||
console.log(`Successfully built and zipped ${browserName} -> ${zipPath}`);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// Build Chrome
|
||||
await buildBrowser('chrome', (manifest) => {
|
||||
manifest.background = {
|
||||
service_worker: "background.js",
|
||||
type: "module"
|
||||
};
|
||||
return manifest;
|
||||
});
|
||||
|
||||
// Build Firefox
|
||||
await buildBrowser('firefox', (manifest) => {
|
||||
manifest.background = {
|
||||
scripts: ["background.js"],
|
||||
type: "module"
|
||||
};
|
||||
manifest.browser_specific_settings = {
|
||||
gecko: {
|
||||
id: "koalasync@shik3i.net"
|
||||
}
|
||||
};
|
||||
return manifest;
|
||||
});
|
||||
|
||||
console.log('Build complete!');
|
||||
} catch (error) {
|
||||
console.error('Build failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -1,12 +0,0 @@
|
||||
@echo off
|
||||
REM KoalaSync - Protocol Synchronization Script (Windows)
|
||||
REM
|
||||
REM This script copies the master constants.js file from the shared directory
|
||||
REM to the extension directory. Since Chrome Extensions cannot load files
|
||||
REM outside their root, this manual sync is required after any changes to
|
||||
REM the shared protocol.
|
||||
|
||||
if not exist extension\shared mkdir extension\shared
|
||||
copy /y shared\constants.js extension\shared\constants.js
|
||||
copy /y shared\blacklist.js extension\shared\blacklist.js
|
||||
echo ✓ constants.js and blacklist.js synced to extension\shared\
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
# KoalaSync - Protocol Synchronization Script (Linux/macOS)
|
||||
#
|
||||
# This script copies the master constants.js file from the shared directory
|
||||
# to the extension directory. Since Chrome Extensions cannot load files
|
||||
# outside their root, this manual sync is required after any changes to
|
||||
# the shared protocol.
|
||||
|
||||
mkdir -p extension/shared
|
||||
cp shared/constants.js extension/shared/constants.js
|
||||
cp shared/blacklist.js extension/shared/blacklist.js
|
||||
echo "✓ constants.js and blacklist.js synced to extension/shared/"
|
||||
@@ -370,6 +370,7 @@ io.on('connection', (socket) => {
|
||||
});
|
||||
|
||||
socket.on(EVENTS.EVENT_ACK, (data) => {
|
||||
if (!data || typeof data !== 'object') return;
|
||||
if (!data.targetId) return;
|
||||
|
||||
const senderMapping = socketToRoom.get(socket.id);
|
||||
|
||||
Reference in New Issue
Block a user