mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-09-03 06:27:56 +00:00
fix(release): close remaining PR review gaps
This commit is contained in:
@@ -2172,6 +2172,9 @@ async function handleServerEvent(event, data, expectedConnectionGeneration = con
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EVENTS.CONTROL_MODE:
|
case EVENTS.CONTROL_MODE:
|
||||||
|
// Terminal room teardown is authoritative. Older/custom relays may
|
||||||
|
// still emit a trailing role update after ROOM_CLOSED.
|
||||||
|
if (!currentRoom) break;
|
||||||
// Host Control Mode changed (toggle or host-leave fallback).
|
// Host Control Mode changed (toggle or host-leave fallback).
|
||||||
controlMode = data.controlMode || CONTROL_MODES.EVERYONE;
|
controlMode = data.controlMode || CONTROL_MODES.EVERYONE;
|
||||||
hostPeerId = data.hostPeerId || null;
|
hostPeerId = data.hostPeerId || null;
|
||||||
@@ -2239,7 +2242,7 @@ async function handleServerEvent(event, data, expectedConnectionGeneration = con
|
|||||||
await chatReceiveQueue;
|
await chatReceiveQueue;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case EVENTS.ERROR:
|
case EVENTS.ERROR: {
|
||||||
isConnecting = false;
|
isConnecting = false;
|
||||||
const terminalRoomError = data.code === ERROR_CODES.ROOM_CLOSED
|
const terminalRoomError = data.code === ERROR_CODES.ROOM_CLOSED
|
||||||
|| data.code === ERROR_CODES.PEER_TIMED_OUT
|
|| data.code === ERROR_CODES.PEER_TIMED_OUT
|
||||||
@@ -2274,6 +2277,7 @@ async function handleServerEvent(event, data, expectedConnectionGeneration = con
|
|||||||
const errStatusMsg = { type: 'JOIN_STATUS', success: false, message: data.message };
|
const errStatusMsg = { type: 'JOIN_STATUS', success: false, message: data.message };
|
||||||
await broadcastJoinStatus(errStatusMsg);
|
await broadcastJoinStatus(errStatusMsg);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case EVENTS.PLAY:
|
case EVENTS.PLAY:
|
||||||
case EVENTS.PAUSE:
|
case EVENTS.PAUSE:
|
||||||
case EVENTS.SEEK:
|
case EVENTS.SEEK:
|
||||||
|
|||||||
@@ -116,11 +116,17 @@ describe('target tab lifecycle', () => {
|
|||||||
expect(backgroundSource).toContain("data.message === 'Removed from room after inactivity'");
|
expect(backgroundSource).toContain("data.message === 'Removed from room after inactivity'");
|
||||||
expect(backgroundSource).toContain('await endRoomSession({ reason: `Room session ended: ${data.message}` });');
|
expect(backgroundSource).toContain('await endRoomSession({ reason: `Room session ended: ${data.message}` });');
|
||||||
|
|
||||||
|
const controlModeStart = backgroundSource.indexOf('case EVENTS.CONTROL_MODE:');
|
||||||
|
const controlModeEnd = backgroundSource.indexOf('case EVENTS.ROOM_LIST:', controlModeStart);
|
||||||
|
expect(backgroundSource.slice(controlModeStart, controlModeEnd)).toContain('if (!currentRoom) break;');
|
||||||
|
|
||||||
expect(sharedConstantsSource).toContain("ROOM_CLOSED: 'room_closed'");
|
expect(sharedConstantsSource).toContain("ROOM_CLOSED: 'room_closed'");
|
||||||
expect(sharedConstantsSource).toContain("PEER_TIMED_OUT: 'peer_timed_out'");
|
expect(sharedConstantsSource).toContain("PEER_TIMED_OUT: 'peer_timed_out'");
|
||||||
expect(serverSource).toContain('code: ERROR_CODES.ROOM_CLOSED');
|
expect(serverSource).toContain('code: ERROR_CODES.ROOM_CLOSED');
|
||||||
expect(serverSource).toContain('code: ERROR_CODES.PEER_TIMED_OUT');
|
expect(serverSource).toContain('code: ERROR_CODES.PEER_TIMED_OUT');
|
||||||
expect(serverSource).toContain("removePeerFromRoom(sid, roomId, 'room-timeout')");
|
expect(serverSource).toContain(
|
||||||
|
"removePeerFromRoom(sid, roomId, 'room-timeout', { notifyRemainingPeers: false })"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not promote a nested media target without confirmed parent visibility', () => {
|
it('does not promote a nested media target without confirmed parent visibility', () => {
|
||||||
|
|||||||
+29
-15
@@ -13,67 +13,81 @@ export function replaceExactly(text, pattern, replacement, label) {
|
|||||||
return text.replace(pattern, replacement);
|
return text.replace(pattern, replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeJson(root, relativePath, update) {
|
function stageJson(stagedUpdates, root, relativePath, update) {
|
||||||
const absolutePath = path.join(root, relativePath);
|
const absolutePath = path.join(root, relativePath);
|
||||||
const value = JSON.parse(fs.readFileSync(absolutePath, 'utf8'));
|
const current = stagedUpdates.has(absolutePath)
|
||||||
|
? stagedUpdates.get(absolutePath)
|
||||||
|
: fs.readFileSync(absolutePath, 'utf8');
|
||||||
|
const value = JSON.parse(current);
|
||||||
update(value);
|
update(value);
|
||||||
fs.writeFileSync(absolutePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
stagedUpdates.set(absolutePath, `${JSON.stringify(value, null, 2)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateText(root, relativePath, pattern, replacement, label) {
|
function stageText(stagedUpdates, root, relativePath, pattern, replacement, label) {
|
||||||
const absolutePath = path.join(root, relativePath);
|
const absolutePath = path.join(root, relativePath);
|
||||||
const current = fs.readFileSync(absolutePath, 'utf8');
|
const current = stagedUpdates.has(absolutePath)
|
||||||
fs.writeFileSync(absolutePath, replaceExactly(current, pattern, replacement, label), 'utf8');
|
? stagedUpdates.get(absolutePath)
|
||||||
|
: fs.readFileSync(absolutePath, 'utf8');
|
||||||
|
stagedUpdates.set(absolutePath, replaceExactly(current, pattern, replacement, label));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareRelease(version, date = new Date(), root = repoRoot) {
|
export function prepareRelease(version, date = new Date(), root = repoRoot) {
|
||||||
versionFromTag(`v${version}`);
|
versionFromTag(`v${version}`);
|
||||||
const timestamp = date.toISOString().replace(/\.\d{3}Z$/u, 'Z');
|
const timestamp = date.toISOString().replace(/\.\d{3}Z$/u, 'Z');
|
||||||
writeJson(root, 'package.json', value => { value.version = version; });
|
const stagedUpdates = new Map();
|
||||||
writeJson(root, 'package-lock.json', value => {
|
stageJson(stagedUpdates, root, 'package.json', value => { value.version = version; });
|
||||||
|
stageJson(stagedUpdates, root, 'package-lock.json', value => {
|
||||||
value.version = version;
|
value.version = version;
|
||||||
value.packages[''].version = version;
|
value.packages[''].version = version;
|
||||||
});
|
});
|
||||||
writeJson(root, 'extension/manifest.base.json', value => { value.version = version; });
|
stageJson(stagedUpdates, root, 'extension/manifest.base.json', value => { value.version = version; });
|
||||||
writeJson(root, 'website/version.json', value => {
|
stageJson(stagedUpdates, root, 'website/version.json', value => {
|
||||||
value.version = version;
|
value.version = version;
|
||||||
value.date = timestamp;
|
value.date = timestamp;
|
||||||
});
|
});
|
||||||
updateText(
|
stageText(
|
||||||
|
stagedUpdates,
|
||||||
root,
|
root,
|
||||||
'shared/constants.js',
|
'shared/constants.js',
|
||||||
/export const APP_VERSION = ["'][^"']+["'];/gu,
|
/export const APP_VERSION = ["'][^"']+["'];/gu,
|
||||||
`export const APP_VERSION = "${version}";`,
|
`export const APP_VERSION = "${version}";`,
|
||||||
'shared/constants.js'
|
'shared/constants.js'
|
||||||
);
|
);
|
||||||
updateText(
|
stageText(
|
||||||
|
stagedUpdates,
|
||||||
root,
|
root,
|
||||||
'website/template.html',
|
'website/template.html',
|
||||||
/"softwareVersion": "[^"]+"/gu,
|
/"softwareVersion": "[^"]+"/gu,
|
||||||
`"softwareVersion": "${version}"`,
|
`"softwareVersion": "${version}"`,
|
||||||
'website/template.html'
|
'website/template.html'
|
||||||
);
|
);
|
||||||
updateText(
|
stageText(
|
||||||
|
stagedUpdates,
|
||||||
root,
|
root,
|
||||||
'website/llms.txt',
|
'website/llms.txt',
|
||||||
/Current website release: .+/gu,
|
/Current website release: .+/gu,
|
||||||
`Current website release: ${version}`,
|
`Current website release: ${version}`,
|
||||||
'website/llms.txt'
|
'website/llms.txt'
|
||||||
);
|
);
|
||||||
updateText(
|
stageText(
|
||||||
|
stagedUpdates,
|
||||||
root,
|
root,
|
||||||
'README.md',
|
'README.md',
|
||||||
/Release-v\d+\.\d+\.\d+-blue/gu,
|
/Release-v\d+\.\d+\.\d+-blue/gu,
|
||||||
`Release-v${version}-blue`,
|
`Release-v${version}-blue`,
|
||||||
'README.md release badge'
|
'README.md release badge'
|
||||||
);
|
);
|
||||||
updateText(
|
stageText(
|
||||||
|
stagedUpdates,
|
||||||
root,
|
root,
|
||||||
'README.md',
|
'README.md',
|
||||||
/New v\d+\.\d+\.\d+ Release!/gu,
|
/New v\d+\.\d+\.\d+ Release!/gu,
|
||||||
`New v${version} Release!`,
|
`New v${version} Release!`,
|
||||||
'README.md release banner'
|
'README.md release banner'
|
||||||
);
|
);
|
||||||
|
for (const [absolutePath, content] of stagedUpdates) {
|
||||||
|
fs.writeFileSync(absolutePath, content, 'utf8');
|
||||||
|
}
|
||||||
console.log(`Prepared release v${version} at ${timestamp}`);
|
console.log(`Prepared release v${version} at ${timestamp}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,4 +87,16 @@ describe('release preparation helpers', () => {
|
|||||||
.toThrow('vMAJOR.MINOR.PATCH');
|
.toThrow('vMAJOR.MINOR.PATCH');
|
||||||
expect(readFixture(root)).toEqual(before);
|
expect(readFixture(root)).toEqual(before);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not partially update release sources when a later marker is invalid', () => {
|
||||||
|
const root = createReleaseFixture();
|
||||||
|
const llmsPath = path.join(root, 'website/llms.txt');
|
||||||
|
fs.writeFileSync(llmsPath, fs.readFileSync(llmsPath, 'utf8')
|
||||||
|
.replace(/Current website release: .+/u, 'Release marker intentionally missing'), 'utf8');
|
||||||
|
const before = readFixture(root);
|
||||||
|
|
||||||
|
expect(() => prepareRelease('9.8.7', new Date('2030-01-01T00:00:00Z'), root))
|
||||||
|
.toThrow('website/llms.txt must contain exactly one release-version marker');
|
||||||
|
expect(readFixture(root)).toEqual(before);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -575,17 +575,22 @@ try {
|
|||||||
assert.equal(mod.rooms.has(msgateRid), false, 'empty-room cleanup removes canonical state with the room');
|
assert.equal(mod.rooms.has(msgateRid), false, 'empty-room cleanup removes canonical state with the room');
|
||||||
// --- Terminal room timeout: coded error + complete membership cleanup ---
|
// --- Terminal room timeout: coded error + complete membership cleanup ---
|
||||||
const timeoutClient = await c();
|
const timeoutClient = await c();
|
||||||
|
const timeoutPeer = await c();
|
||||||
const timeoutRoomId = 'timeout-'+Date.now();
|
const timeoutRoomId = 'timeout-'+Date.now();
|
||||||
await j(timeoutClient, timeoutRoomId, 'timeout-peer');
|
await j(timeoutClient, timeoutRoomId, 'timeout-peer');
|
||||||
timeoutClient._m.length = 0;
|
await j(timeoutPeer, timeoutRoomId, 'timeout-peer-2');
|
||||||
|
timeoutClient._m.length = timeoutPeer._m.length = 0;
|
||||||
mod.rooms.get(timeoutRoomId).lastActivity = 0;
|
mod.rooms.get(timeoutRoomId).lastActivity = 0;
|
||||||
mod.cleanupInactiveRooms(Date.now());
|
mod.cleanupInactiveRooms(Date.now());
|
||||||
const [timeoutEvent, timeoutData] = await a(timeoutClient);
|
const timeoutData = await w(timeoutClient, 'error');
|
||||||
assert.equal(timeoutEvent, 'error');
|
const timeoutPeerData = await w(timeoutPeer, 'error');
|
||||||
assert.equal(timeoutData.code, 'room_closed');
|
assert.equal(timeoutData.code, 'room_closed');
|
||||||
assert.equal(timeoutData.message, 'Room closed');
|
assert.equal(timeoutData.message, 'Room closed');
|
||||||
|
assert.equal(timeoutPeerData.code, 'room_closed');
|
||||||
|
await delay(80);
|
||||||
|
assert.deepEqual(timeoutClient._m, [], 'terminal room cleanup emits nothing after room_closed');
|
||||||
|
assert.deepEqual(timeoutPeer._m, [], 'terminal room cleanup emits nothing after room_closed');
|
||||||
assert.equal(mod.rooms.has(timeoutRoomId), false, 'inactive room is deleted');
|
assert.equal(mod.rooms.has(timeoutRoomId), false, 'inactive room is deleted');
|
||||||
timeoutClient._m.length = 0;
|
|
||||||
|
|
||||||
// The same connected socket must be able to join that room again. This
|
// The same connected socket must be able to join that room again. This
|
||||||
// proves timeout cleanup removed its stale socketToRoom membership.
|
// proves timeout cleanup removed its stale socketToRoom membership.
|
||||||
|
|||||||
+7
-5
@@ -250,8 +250,10 @@ function log(type, message, details = '') {
|
|||||||
* @param {string} socketId - The socket.id being removed.
|
* @param {string} socketId - The socket.id being removed.
|
||||||
* @param {string} roomId - The room it belongs to.
|
* @param {string} roomId - The room it belongs to.
|
||||||
* @param {string} reason - Log label ('disconnect', 'leave', 'reaper', 'dedupe', 'room-switch').
|
* @param {string} reason - Log label ('disconnect', 'leave', 'reaper', 'dedupe', 'room-switch').
|
||||||
|
* @param {object} options
|
||||||
|
* @param {boolean} options.notifyRemainingPeers - Whether to emit room-state updates after removal.
|
||||||
*/
|
*/
|
||||||
function removePeerFromRoom(socketId, roomId, reason) {
|
function removePeerFromRoom(socketId, roomId, reason, { notifyRemainingPeers = true } = {}) {
|
||||||
const room = rooms.get(roomId);
|
const room = rooms.get(roomId);
|
||||||
if (!room) return;
|
if (!room) return;
|
||||||
|
|
||||||
@@ -275,7 +277,7 @@ function removePeerFromRoom(socketId, roomId, reason) {
|
|||||||
// 3. Notify remaining peers (use io.to so the removed socket itself
|
// 3. Notify remaining peers (use io.to so the removed socket itself
|
||||||
// doesn't receive it — it has already left or is disconnecting)
|
// doesn't receive it — it has already left or is disconnecting)
|
||||||
const isPeerStillConnected = Array.from(room.peerData.values()).some(data => data.peerId === peerId);
|
const isPeerStillConnected = Array.from(room.peerData.values()).some(data => data.peerId === peerId);
|
||||||
if (!isPeerStillConnected) {
|
if (notifyRemainingPeers && !isPeerStillConnected) {
|
||||||
io.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
|
io.to(roomId).emit(EVENTS.PEER_STATUS, { peerId, status: 'left' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,11 +327,11 @@ function removePeerFromRoom(socketId, roomId, reason) {
|
|||||||
room.hostPeerId = nextPeerData ? nextPeerData.peerId : null;
|
room.hostPeerId = nextPeerData ? nextPeerData.peerId : null;
|
||||||
room.controlMode = CONTROL_MODES.EVERYONE;
|
room.controlMode = CONTROL_MODES.EVERYONE;
|
||||||
room.controllers = new Set(room.hostPeerId ? [room.hostPeerId] : []);
|
room.controllers = new Set(room.hostPeerId ? [room.hostPeerId] : []);
|
||||||
io.to(roomId).emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
if (notifyRemainingPeers) io.to(roomId).emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||||
log('ROOM', `Owner left room ${roomId.substring(0, 3)}*** — fell back to 'everyone', new owner: ${room.hostPeerId}`);
|
log('ROOM', `Owner left room ${roomId.substring(0, 3)}*** — fell back to 'everyone', new owner: ${room.hostPeerId}`);
|
||||||
} else if (wasController) {
|
} else if (wasController) {
|
||||||
// A co-host left → keep the mode, just broadcast the updated controller list.
|
// A co-host left → keep the mode, just broadcast the updated controller list.
|
||||||
io.to(roomId).emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
if (notifyRemainingPeers) io.to(roomId).emit(EVENTS.CONTROL_MODE, controlModePayload(room));
|
||||||
log('ROOM', `Controller ${peerId} left room ${roomId.substring(0, 3)}***`);
|
log('ROOM', `Controller ${peerId} left room ${roomId.substring(0, 3)}***`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1111,7 +1113,7 @@ export function cleanupInactiveRooms(now = Date.now()) {
|
|||||||
for (const sid of Array.from(currentRoom.peers)) {
|
for (const sid of Array.from(currentRoom.peers)) {
|
||||||
const memberSocket = io.sockets?.sockets?.get(sid);
|
const memberSocket = io.sockets?.sockets?.get(sid);
|
||||||
if (memberSocket) memberSocket.leave(roomId);
|
if (memberSocket) memberSocket.leave(roomId);
|
||||||
removePeerFromRoom(sid, roomId, 'room-timeout');
|
removePeerFromRoom(sid, roomId, 'room-timeout', { notifyRemainingPeers: false });
|
||||||
}
|
}
|
||||||
rooms.delete(roomId);
|
rooms.delete(roomId);
|
||||||
log('CLEANUP', `Deleted room ${roomId.substring(0, 3)}*** (Empty/Inactive)`);
|
log('CLEANUP', `Deleted room ${roomId.substring(0, 3)}*** (Empty/Inactive)`);
|
||||||
|
|||||||
Reference in New Issue
Block a user