Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 53389ed3ad |
@@ -58,10 +58,6 @@ function buildSeededEndpointStorageState(
|
|||||||
function applySeededEndpointStorageState(storageState: SeededEndpointStorageState): void {
|
function applySeededEndpointStorageState(storageState: SeededEndpointStorageState): void {
|
||||||
try {
|
try {
|
||||||
const storage = window.localStorage;
|
const storage = window.localStorage;
|
||||||
const currentUserId = storage.getItem('metoyou_currentUserId')?.trim() || null;
|
|
||||||
const generalSettings = JSON.stringify({
|
|
||||||
reopenLastViewedChat: false
|
|
||||||
});
|
|
||||||
|
|
||||||
storage.setItem(storageState.key, JSON.stringify(storageState.endpoints));
|
storage.setItem(storageState.key, JSON.stringify(storageState.endpoints));
|
||||||
storage.setItem(storageState.removedKey, JSON.stringify([
|
storage.setItem(storageState.removedKey, JSON.stringify([
|
||||||
@@ -69,56 +65,11 @@ function applySeededEndpointStorageState(storageState: SeededEndpointStorageStat
|
|||||||
'toju-primary',
|
'toju-primary',
|
||||||
'toju-sweden'
|
'toju-sweden'
|
||||||
]));
|
]));
|
||||||
storage.setItem('metoyou_general_settings', generalSettings);
|
|
||||||
|
|
||||||
if (currentUserId) {
|
|
||||||
storage.setItem(`metoyou_general_settings__${encodeURIComponent(currentUserId)}`, generalSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
const keysToRemove: string[] = [];
|
|
||||||
|
|
||||||
for (let index = 0; index < storage.length; index += 1) {
|
|
||||||
const key = storage.key(index);
|
|
||||||
|
|
||||||
if (key === 'metoyou_lastViewedChat' || key?.startsWith('metoyou_lastViewedChat__')) {
|
|
||||||
keysToRemove.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of keysToRemove) {
|
|
||||||
storage.removeItem(key);
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// about:blank and some Playwright UI pages deny localStorage access.
|
// about:blank and some Playwright UI pages deny localStorage access.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function disableLastViewedChatResume(page: Page): Promise<void> {
|
|
||||||
await page.evaluate(() => {
|
|
||||||
const currentUserId = localStorage.getItem('metoyou_currentUserId')?.trim() || null;
|
|
||||||
const generalSettings = JSON.stringify({ reopenLastViewedChat: false });
|
|
||||||
const keysToRemove: string[] = [];
|
|
||||||
|
|
||||||
localStorage.setItem('metoyou_general_settings', generalSettings);
|
|
||||||
|
|
||||||
if (currentUserId) {
|
|
||||||
localStorage.setItem(`metoyou_general_settings__${encodeURIComponent(currentUserId)}`, generalSettings);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let index = 0; index < localStorage.length; index += 1) {
|
|
||||||
const key = localStorage.key(index);
|
|
||||||
|
|
||||||
if (key === 'metoyou_lastViewedChat' || key?.startsWith('metoyou_lastViewedChat__')) {
|
|
||||||
keysToRemove.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of keysToRemove) {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function installTestServerEndpoint(
|
export async function installTestServerEndpoint(
|
||||||
context: BrowserContext,
|
context: BrowserContext,
|
||||||
port: number = Number(process.env.TEST_SERVER_PORT) || 3099
|
port: number = Number(process.env.TEST_SERVER_PORT) || 3099
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
import { RegisterPage } from '../../pages/register.page';
|
import { RegisterPage } from '../../pages/register.page';
|
||||||
import { ServerSearchPage } from '../../pages/server-search.page';
|
import { ServerSearchPage } from '../../pages/server-search.page';
|
||||||
import { ChatMessagesPage } from '../../pages/chat-messages.page';
|
import { ChatMessagesPage } from '../../pages/chat-messages.page';
|
||||||
import { disableLastViewedChatResume } from '../../helpers/seed-test-endpoint';
|
|
||||||
|
|
||||||
test.describe('Direct message flow', () => {
|
test.describe('Direct message flow', () => {
|
||||||
test.describe.configure({ timeout: 180_000 });
|
test.describe.configure({ timeout: 180_000 });
|
||||||
@@ -38,7 +37,6 @@ test.describe('Direct message flow', () => {
|
|||||||
test('shows friend and message actions on the search people list', async ({ createClient }) => {
|
test('shows friend and message actions on the search people list', async ({ createClient }) => {
|
||||||
const scenario = await createDmScenario(createClient);
|
const scenario = await createDmScenario(createClient);
|
||||||
|
|
||||||
await disableLastViewedChatResume(scenario.alice.page);
|
|
||||||
await scenario.alice.page.goto('/search', { waitUntil: 'domcontentloaded' });
|
await scenario.alice.page.goto('/search', { waitUntil: 'domcontentloaded' });
|
||||||
await expect(scenario.alice.page).toHaveURL(/\/search/, { timeout: 20_000 });
|
await expect(scenario.alice.page).toHaveURL(/\/search/, { timeout: 20_000 });
|
||||||
await expect(scenario.alice.page.locator('app-server-search')).toBeVisible({ timeout: 20_000 });
|
await expect(scenario.alice.page.locator('app-server-search')).toBeVisible({ timeout: 20_000 });
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ const NETSCAPE_LOOP_EXTENSION = Buffer.from([
|
|||||||
]);
|
]);
|
||||||
const CLIENT_LAUNCH_ARGS = ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'];
|
const CLIENT_LAUNCH_ARGS = ['--use-fake-device-for-media-stream', '--use-fake-ui-for-media-stream'];
|
||||||
const VOICE_CHANNEL = 'General';
|
const VOICE_CHANNEL = 'General';
|
||||||
const AVATAR_SYNC_TIMEOUT_MS = 45_000;
|
|
||||||
|
|
||||||
test.describe('Profile avatar sync', () => {
|
test.describe('Profile avatar sync', () => {
|
||||||
test.describe.configure({ timeout: 240_000 });
|
test.describe.configure({ timeout: 240_000 });
|
||||||
@@ -599,7 +598,7 @@ async function expectSidebarAvatar(page: Page, displayName: string, expectedData
|
|||||||
|
|
||||||
return image.getAttribute('src');
|
return image.getAttribute('src');
|
||||||
}, {
|
}, {
|
||||||
timeout: AVATAR_SYNC_TIMEOUT_MS,
|
timeout: 20_000,
|
||||||
message: `${displayName} avatar src should update`
|
message: `${displayName} avatar src should update`
|
||||||
}).toBe(expectedDataUrl);
|
}).toBe(expectedDataUrl);
|
||||||
|
|
||||||
@@ -616,7 +615,7 @@ async function expectSidebarAvatar(page: Page, displayName: string, expectedData
|
|||||||
return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0;
|
return img.complete && img.naturalWidth > 0 && img.naturalHeight > 0;
|
||||||
});
|
});
|
||||||
}, {
|
}, {
|
||||||
timeout: AVATAR_SYNC_TIMEOUT_MS,
|
timeout: 20_000,
|
||||||
message: `${displayName} avatar image should load`
|
message: `${displayName} avatar image should load`
|
||||||
}).toBe(true);
|
}).toBe(true);
|
||||||
}
|
}
|
||||||
@@ -636,7 +635,7 @@ async function expectChatMessageAvatar(page: Page, messageText: string, expected
|
|||||||
|
|
||||||
return image.getAttribute('src');
|
return image.getAttribute('src');
|
||||||
}, {
|
}, {
|
||||||
timeout: AVATAR_SYNC_TIMEOUT_MS,
|
timeout: 20_000,
|
||||||
message: `Chat message avatar for "${messageText}" should update`
|
message: `Chat message avatar for "${messageText}" should update`
|
||||||
}).toBe(expectedDataUrl);
|
}).toBe(expectedDataUrl);
|
||||||
}
|
}
|
||||||
@@ -663,7 +662,7 @@ async function expectVoiceControlsAvatar(page: Page, expectedDataUrl: string): P
|
|||||||
|
|
||||||
return image.getAttribute('src');
|
return image.getAttribute('src');
|
||||||
}, {
|
}, {
|
||||||
timeout: AVATAR_SYNC_TIMEOUT_MS,
|
timeout: 20_000,
|
||||||
message: 'Voice controls avatar should update'
|
message: 'Voice controls avatar should update'
|
||||||
}).toBe(expectedDataUrl);
|
}).toBe(expectedDataUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,24 +26,6 @@ let dbBackupPath = '';
|
|||||||
|
|
||||||
// SQLite files start with this 16-byte header string.
|
// SQLite files start with this 16-byte header string.
|
||||||
const SQLITE_MAGIC = 'SQLite format 3\0';
|
const SQLITE_MAGIC = 'SQLite format 3\0';
|
||||||
const SAVE_RETRY_DELAYS_MS = [25, 75, 150, 300, 600];
|
|
||||||
const RETRYABLE_SAVE_ERROR_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
||||||
|
|
||||||
let saveQueue: Promise<void> = Promise.resolve();
|
|
||||||
|
|
||||||
function wait(ms: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRetryableSaveError(error: unknown): boolean {
|
|
||||||
if (!error || typeof error !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = (error as NodeJS.ErrnoException).code;
|
|
||||||
|
|
||||||
return typeof code === 'string' && RETRYABLE_SAVE_ERROR_CODES.has(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDataSource(): DataSource | undefined {
|
export function getDataSource(): DataSource | undefined {
|
||||||
return applicationDataSource;
|
return applicationDataSource;
|
||||||
@@ -105,47 +87,18 @@ function safeguardDbFile(): Uint8Array | undefined {
|
|||||||
* then rename it over the real file. rename() is atomic on the same
|
* then rename it over the real file. rename() is atomic on the same
|
||||||
* filesystem, so a crash mid-write can never leave a half-written DB.
|
* filesystem, so a crash mid-write can never leave a half-written DB.
|
||||||
*/
|
*/
|
||||||
async function replaceDatabaseFile(tmpPath: string): Promise<void> {
|
async function atomicSave(data: Uint8Array): Promise<void> {
|
||||||
for (let attempt = 0; ; attempt++) {
|
|
||||||
try {
|
|
||||||
await fsp.rename(tmpPath, dbFilePath);
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
|
||||||
const delay = SAVE_RETRY_DELAYS_MS[attempt];
|
|
||||||
|
|
||||||
if (!isRetryableSaveError(error) || delay === undefined) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
await wait(delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeDatabaseSnapshot(snapshot: Buffer): Promise<void> {
|
|
||||||
const tmpPath = dbFilePath + '.tmp-' + randomBytes(6).toString('hex');
|
const tmpPath = dbFilePath + '.tmp-' + randomBytes(6).toString('hex');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fsp.writeFile(tmpPath, snapshot);
|
await fsp.writeFile(tmpPath, Buffer.from(data));
|
||||||
await replaceDatabaseFile(tmpPath);
|
await fsp.rename(tmpPath, dbFilePath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await fsp.unlink(tmpPath).catch(() => {});
|
await fsp.unlink(tmpPath).catch(() => {});
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function atomicSave(data: Uint8Array): Promise<void> {
|
|
||||||
const snapshot = Buffer.from(data);
|
|
||||||
const saveTask = saveQueue.then(
|
|
||||||
() => writeDatabaseSnapshot(snapshot),
|
|
||||||
() => writeDatabaseSnapshot(snapshot)
|
|
||||||
);
|
|
||||||
|
|
||||||
saveQueue = saveTask.catch(() => {});
|
|
||||||
|
|
||||||
return saveTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initializeDatabase(): Promise<void> {
|
export async function initializeDatabase(): Promise<void> {
|
||||||
const userDataPath = app.getPath('userData');
|
const userDataPath = app.getPath('userData');
|
||||||
const dbDir = path.join(userDataPath, 'metoyou');
|
const dbDir = path.join(userDataPath, 'metoyou');
|
||||||
|
|||||||
@@ -49,25 +49,8 @@ const DB_BACKUP = DB_FILE + '.bak';
|
|||||||
const DATA_DIR = path.dirname(DB_FILE);
|
const DATA_DIR = path.dirname(DB_FILE);
|
||||||
// SQLite files start with this 16-byte header string.
|
// SQLite files start with this 16-byte header string.
|
||||||
const SQLITE_MAGIC = 'SQLite format 3\0';
|
const SQLITE_MAGIC = 'SQLite format 3\0';
|
||||||
const SAVE_RETRY_DELAYS_MS = [25, 75, 150, 300, 600];
|
|
||||||
const RETRYABLE_SAVE_ERROR_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']);
|
|
||||||
|
|
||||||
let applicationDataSource: DataSource | undefined;
|
let applicationDataSource: DataSource | undefined;
|
||||||
let saveQueue: Promise<void> = Promise.resolve();
|
|
||||||
|
|
||||||
function wait(ms: number): Promise<void> {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRetryableSaveError(error: unknown): boolean {
|
|
||||||
if (!error || typeof error !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = (error as NodeJS.ErrnoException).code;
|
|
||||||
|
|
||||||
return typeof code === 'string' && RETRYABLE_SAVE_ERROR_CODES.has(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreFromBackup(reason: string): Uint8Array | undefined {
|
function restoreFromBackup(reason: string): Uint8Array | undefined {
|
||||||
if (!fs.existsSync(DB_BACKUP)) {
|
if (!fs.existsSync(DB_BACKUP)) {
|
||||||
@@ -177,47 +160,18 @@ function resolveSqlJsConfig(): { locateFile: (file: string) => string } {
|
|||||||
* then rename it over the real file. rename() is atomic on the same
|
* then rename it over the real file. rename() is atomic on the same
|
||||||
* filesystem, so a crash mid-write can never leave a half-written DB.
|
* filesystem, so a crash mid-write can never leave a half-written DB.
|
||||||
*/
|
*/
|
||||||
async function replaceDatabaseFile(tmpPath: string): Promise<void> {
|
async function atomicSave(data: Uint8Array): Promise<void> {
|
||||||
for (let attempt = 0; ; attempt++) {
|
|
||||||
try {
|
|
||||||
await fsp.rename(tmpPath, DB_FILE);
|
|
||||||
return;
|
|
||||||
} catch (error) {
|
|
||||||
const delay = SAVE_RETRY_DELAYS_MS[attempt];
|
|
||||||
|
|
||||||
if (!isRetryableSaveError(error) || delay === undefined) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
await wait(delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeDatabaseSnapshot(snapshot: Buffer): Promise<void> {
|
|
||||||
const tmpPath = DB_FILE + '.tmp-' + randomBytes(6).toString('hex');
|
const tmpPath = DB_FILE + '.tmp-' + randomBytes(6).toString('hex');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fsp.writeFile(tmpPath, snapshot);
|
await fsp.writeFile(tmpPath, Buffer.from(data));
|
||||||
await replaceDatabaseFile(tmpPath);
|
await fsp.rename(tmpPath, DB_FILE);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await fsp.unlink(tmpPath).catch(() => {});
|
await fsp.unlink(tmpPath).catch(() => {});
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function atomicSave(data: Uint8Array): Promise<void> {
|
|
||||||
const snapshot = Buffer.from(data);
|
|
||||||
const saveTask = saveQueue.then(
|
|
||||||
() => writeDatabaseSnapshot(snapshot),
|
|
||||||
() => writeDatabaseSnapshot(snapshot)
|
|
||||||
);
|
|
||||||
|
|
||||||
saveQueue = saveTask.catch(() => {});
|
|
||||||
|
|
||||||
return saveTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDataSource(): DataSource {
|
export function getDataSource(): DataSource {
|
||||||
if (!applicationDataSource?.isInitialized) {
|
if (!applicationDataSource?.isInitialized) {
|
||||||
throw new Error('DataSource not initialised');
|
throw new Error('DataSource not initialised');
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import {
|
|||||||
} from './broadcast';
|
} from './broadcast';
|
||||||
import { handleWebSocketMessage } from './handler';
|
import { handleWebSocketMessage } from './handler';
|
||||||
|
|
||||||
type IncomingWebSocketMessage = Parameters<typeof handleWebSocketMessage>[1];
|
|
||||||
|
|
||||||
/** How often to ping all connected clients (ms). */
|
/** How often to ping all connected clients (ms). */
|
||||||
const PING_INTERVAL_MS = 30_000;
|
const PING_INTERVAL_MS = 30_000;
|
||||||
/** Maximum time a client can go without a pong before we consider it dead (ms). */
|
/** Maximum time a client can go without a pong before we consider it dead (ms). */
|
||||||
@@ -91,20 +89,12 @@ export function setupWebSocket(server: Server<typeof IncomingMessage, typeof Ser
|
|||||||
});
|
});
|
||||||
|
|
||||||
ws.on('message', async (data) => {
|
ws.on('message', async (data) => {
|
||||||
let message: IncomingWebSocketMessage;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(data.toString()) as IncomingWebSocketMessage;
|
const message = JSON.parse(data.toString());
|
||||||
} catch (err) {
|
|
||||||
console.error('Invalid WebSocket message:', err);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await handleWebSocketMessage(connectionId, message);
|
await handleWebSocketMessage(connectionId, message);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('WebSocket message handler failed:', err);
|
console.error('Invalid WebSocket message:', err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -173,8 +173,7 @@ export class GameActivityService implements OnDestroy {
|
|||||||
const matchedGame = await this.matchRunningGame(processNames);
|
const matchedGame = await this.matchRunningGame(processNames);
|
||||||
|
|
||||||
this.ngZone.run(() => this.applyMatchedGame(matchedGame));
|
this.ngZone.run(() => this.applyMatchedGame(matchedGame));
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.warn('[GameActivity] Failed to scan running processes', error);
|
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
this.scanInFlight = false;
|
this.scanInFlight = false;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function setupDataChannel(
|
|||||||
channel: RTCDataChannel,
|
channel: RTCDataChannel,
|
||||||
remotePeerId: string
|
remotePeerId: string
|
||||||
): void {
|
): void {
|
||||||
const { logger, state } = context;
|
const { logger } = context;
|
||||||
|
|
||||||
channel.onopen = () => {
|
channel.onopen = () => {
|
||||||
logger.info('[data-channel] Data channel open', {
|
logger.info('[data-channel] Data channel open', {
|
||||||
@@ -40,8 +40,6 @@ export function setupDataChannel(
|
|||||||
protocol: channel.protocol || null
|
protocol: channel.protocol || null
|
||||||
});
|
});
|
||||||
|
|
||||||
state.peerConnected$.next(remotePeerId);
|
|
||||||
|
|
||||||
sendCurrentStatesToChannel(context, channel, remotePeerId);
|
sendCurrentStatesToChannel(context, channel, remotePeerId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user