feat: signal server tag
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import {
|
||||
applyBrowserDatabaseSchema,
|
||||
ensureObjectStoreDuringUpgrade,
|
||||
ensureStoreIndex
|
||||
} from './browser-database-schema';
|
||||
|
||||
describe('browser-database-schema', () => {
|
||||
it('reuses the upgrade transaction when an object store already exists', () => {
|
||||
const existingStore = { indexNames: { contains: () => false } };
|
||||
const database = {
|
||||
objectStoreNames: { contains: (name: string) => name === 'messages' },
|
||||
createObjectStore: vi.fn(),
|
||||
transaction: vi.fn()
|
||||
};
|
||||
const upgradeTransaction = {
|
||||
objectStore: vi.fn(() => existingStore)
|
||||
};
|
||||
const store = ensureObjectStoreDuringUpgrade(
|
||||
database as unknown as IDBDatabase,
|
||||
upgradeTransaction as unknown as IDBTransaction,
|
||||
'messages',
|
||||
{ keyPath: 'id' }
|
||||
);
|
||||
|
||||
expect(store).toBe(existingStore);
|
||||
expect(upgradeTransaction.objectStore).toHaveBeenCalledWith('messages');
|
||||
expect(database.createObjectStore).not.toHaveBeenCalled();
|
||||
expect(database.transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates missing object stores during upgrade', () => {
|
||||
const createdStore = { indexNames: { contains: () => false } };
|
||||
const database = {
|
||||
objectStoreNames: { contains: () => false },
|
||||
createObjectStore: vi.fn(() => createdStore),
|
||||
transaction: vi.fn()
|
||||
};
|
||||
const upgradeTransaction = {
|
||||
objectStore: vi.fn()
|
||||
};
|
||||
const store = ensureObjectStoreDuringUpgrade(
|
||||
database as unknown as IDBDatabase,
|
||||
upgradeTransaction as unknown as IDBTransaction,
|
||||
'customEmojis',
|
||||
{ keyPath: 'id' }
|
||||
);
|
||||
|
||||
expect(store).toBe(createdStore);
|
||||
expect(database.createObjectStore).toHaveBeenCalledWith('customEmojis', { keyPath: 'id' });
|
||||
expect(upgradeTransaction.objectStore).not.toHaveBeenCalled();
|
||||
expect(database.transaction).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates missing indexes on an existing store', () => {
|
||||
const store = {
|
||||
indexNames: { contains: () => false },
|
||||
createIndex: vi.fn()
|
||||
};
|
||||
|
||||
ensureStoreIndex(store as unknown as IDBObjectStore, 'roomId', 'roomId');
|
||||
|
||||
expect(store.createIndex).toHaveBeenCalledWith('roomId', 'roomId', { unique: false });
|
||||
});
|
||||
|
||||
it('applies the full schema through the upgrade transaction only', () => {
|
||||
const stores = new Map<string, { indexNames: { contains: (name: string) => boolean }; createIndex: ReturnType<typeof vi.fn> }>();
|
||||
const database = {
|
||||
objectStoreNames: {
|
||||
contains: (name: string) => stores.has(name)
|
||||
},
|
||||
createObjectStore: vi.fn((name: string) => {
|
||||
const store = {
|
||||
indexNames: { contains: () => false },
|
||||
createIndex: vi.fn()
|
||||
};
|
||||
|
||||
stores.set(name, store);
|
||||
|
||||
return store;
|
||||
}),
|
||||
transaction: vi.fn()
|
||||
};
|
||||
const upgradeTransaction = {
|
||||
objectStore: vi.fn((name: string) => {
|
||||
const store = stores.get(name);
|
||||
|
||||
if (!store) {
|
||||
throw new Error(`Missing store ${name}`);
|
||||
}
|
||||
|
||||
return store;
|
||||
})
|
||||
};
|
||||
|
||||
stores.set('messages', {
|
||||
indexNames: { contains: () => true },
|
||||
createIndex: vi.fn()
|
||||
});
|
||||
|
||||
stores.set('users', {
|
||||
indexNames: { contains: () => true },
|
||||
createIndex: vi.fn()
|
||||
});
|
||||
|
||||
expect(() => applyBrowserDatabaseSchema(
|
||||
database as unknown as IDBDatabase,
|
||||
upgradeTransaction as unknown as IDBTransaction
|
||||
)).not.toThrow();
|
||||
|
||||
expect(database.transaction).not.toHaveBeenCalled();
|
||||
expect(upgradeTransaction.objectStore).toHaveBeenCalledWith('messages');
|
||||
expect(database.createObjectStore).toHaveBeenCalledWith('customEmojis', { keyPath: 'id' });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/** IndexedDB schema version - bump when adding/changing object stores. */
|
||||
export const BROWSER_DATABASE_VERSION = 3;
|
||||
|
||||
const STORE_MESSAGES = 'messages';
|
||||
const STORE_USERS = 'users';
|
||||
const STORE_ROOMS = 'rooms';
|
||||
const STORE_REACTIONS = 'reactions';
|
||||
const STORE_BANS = 'bans';
|
||||
const STORE_META = 'meta';
|
||||
const STORE_ATTACHMENTS = 'attachments';
|
||||
const STORE_CUSTOM_EMOJIS = 'customEmojis';
|
||||
|
||||
export function ensureObjectStoreDuringUpgrade(
|
||||
database: IDBDatabase,
|
||||
upgradeTransaction: IDBTransaction,
|
||||
name: string,
|
||||
options?: IDBObjectStoreParameters
|
||||
): IDBObjectStore {
|
||||
if (database.objectStoreNames.contains(name)) {
|
||||
return upgradeTransaction.objectStore(name);
|
||||
}
|
||||
|
||||
return database.createObjectStore(name, options);
|
||||
}
|
||||
|
||||
export function ensureStoreIndex(store: IDBObjectStore, name: string, keyPath: string): void {
|
||||
if (!store.indexNames.contains(name)) {
|
||||
store.createIndex(name, keyPath, { unique: false });
|
||||
}
|
||||
}
|
||||
|
||||
export function applyBrowserDatabaseSchema(
|
||||
database: IDBDatabase,
|
||||
upgradeTransaction: IDBTransaction
|
||||
): void {
|
||||
const messagesStore = ensureObjectStoreDuringUpgrade(database, upgradeTransaction, STORE_MESSAGES, { keyPath: 'id' });
|
||||
|
||||
ensureStoreIndex(messagesStore, 'roomId', 'roomId');
|
||||
ensureStoreIndex(messagesStore, 'timestamp', 'timestamp');
|
||||
|
||||
ensureObjectStoreDuringUpgrade(database, upgradeTransaction, STORE_USERS, { keyPath: 'id' });
|
||||
|
||||
const roomsStore = ensureObjectStoreDuringUpgrade(database, upgradeTransaction, STORE_ROOMS, { keyPath: 'id' });
|
||||
|
||||
ensureStoreIndex(roomsStore, 'timestamp', 'timestamp');
|
||||
|
||||
const reactionsStore = ensureObjectStoreDuringUpgrade(database, upgradeTransaction, STORE_REACTIONS, { keyPath: 'id' });
|
||||
|
||||
ensureStoreIndex(reactionsStore, 'messageId', 'messageId');
|
||||
ensureStoreIndex(reactionsStore, 'userId', 'userId');
|
||||
|
||||
const bansStore = ensureObjectStoreDuringUpgrade(database, upgradeTransaction, STORE_BANS, { keyPath: 'oderId' });
|
||||
|
||||
ensureStoreIndex(bansStore, 'roomId', 'roomId');
|
||||
ensureStoreIndex(bansStore, 'expiresAt', 'expiresAt');
|
||||
|
||||
ensureObjectStoreDuringUpgrade(database, upgradeTransaction, STORE_META, { keyPath: 'id' });
|
||||
|
||||
const attachmentsStore = ensureObjectStoreDuringUpgrade(database, upgradeTransaction, STORE_ATTACHMENTS, { keyPath: 'id' });
|
||||
|
||||
ensureStoreIndex(attachmentsStore, 'messageId', 'messageId');
|
||||
|
||||
const customEmojisStore = ensureObjectStoreDuringUpgrade(database, upgradeTransaction, STORE_CUSTOM_EMOJIS, { keyPath: 'id' });
|
||||
|
||||
ensureStoreIndex(customEmojisStore, 'updatedAt', 'updatedAt');
|
||||
ensureStoreIndex(customEmojisStore, 'creatorUserId', 'creatorUserId');
|
||||
}
|
||||
@@ -11,12 +11,11 @@ import {
|
||||
import type { ChatAttachmentMeta, CustomEmoji } from '../../shared-kernel';
|
||||
import { getStoredCurrentUserId } from '../../core/storage/current-user-storage';
|
||||
import type { RoomMessageStats } from './database.service';
|
||||
import { applyBrowserDatabaseSchema, BROWSER_DATABASE_VERSION } from './browser-database-schema';
|
||||
|
||||
/** IndexedDB database name for the MetoYou application. */
|
||||
const DATABASE_NAME = 'metoyou';
|
||||
const ANONYMOUS_DATABASE_SCOPE = 'anonymous';
|
||||
/** IndexedDB schema version - bump when adding/changing object stores. */
|
||||
const DATABASE_VERSION = 3;
|
||||
/** Names of every object store used by the application. */
|
||||
const STORE_MESSAGES = 'messages';
|
||||
const STORE_USERS = 'users';
|
||||
@@ -432,10 +431,26 @@ export class BrowserDatabaseService {
|
||||
|
||||
private openDatabase(databaseName: string): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(databaseName, DATABASE_VERSION);
|
||||
const request = indexedDB.open(databaseName, BROWSER_DATABASE_VERSION);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onupgradeneeded = () => this.setupSchema(request.result);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const upgradeTransaction = (event.target as IDBOpenDBRequest).transaction;
|
||||
|
||||
if (!upgradeTransaction) {
|
||||
reject(new Error('IndexedDB upgrade transaction is unavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
applyBrowserDatabaseSchema(request.result, upgradeTransaction);
|
||||
} catch (error) {
|
||||
upgradeTransaction.abort();
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
@@ -446,58 +461,6 @@ export class BrowserDatabaseService {
|
||||
this.activeDatabaseName = null;
|
||||
}
|
||||
|
||||
private setupSchema(database: IDBDatabase): void {
|
||||
const messagesStore = this.ensureStore(database, STORE_MESSAGES, { keyPath: 'id' });
|
||||
|
||||
this.ensureIndex(messagesStore, 'roomId', 'roomId');
|
||||
this.ensureIndex(messagesStore, 'timestamp', 'timestamp');
|
||||
|
||||
this.ensureStore(database, STORE_USERS, { keyPath: 'id' });
|
||||
|
||||
const roomsStore = this.ensureStore(database, STORE_ROOMS, { keyPath: 'id' });
|
||||
|
||||
this.ensureIndex(roomsStore, 'timestamp', 'timestamp');
|
||||
|
||||
const reactionsStore = this.ensureStore(database, STORE_REACTIONS, { keyPath: 'id' });
|
||||
|
||||
this.ensureIndex(reactionsStore, 'messageId', 'messageId');
|
||||
this.ensureIndex(reactionsStore, 'userId', 'userId');
|
||||
|
||||
const bansStore = this.ensureStore(database, STORE_BANS, { keyPath: 'oderId' });
|
||||
|
||||
this.ensureIndex(bansStore, 'roomId', 'roomId');
|
||||
this.ensureIndex(bansStore, 'expiresAt', 'expiresAt');
|
||||
|
||||
this.ensureStore(database, STORE_META, { keyPath: 'id' });
|
||||
|
||||
const attachmentsStore = this.ensureStore(database, STORE_ATTACHMENTS, { keyPath: 'id' });
|
||||
|
||||
this.ensureIndex(attachmentsStore, 'messageId', 'messageId');
|
||||
|
||||
const customEmojisStore = this.ensureStore(database, STORE_CUSTOM_EMOJIS, { keyPath: 'id' });
|
||||
|
||||
this.ensureIndex(customEmojisStore, 'updatedAt', 'updatedAt');
|
||||
this.ensureIndex(customEmojisStore, 'creatorUserId', 'creatorUserId');
|
||||
}
|
||||
|
||||
private ensureStore(
|
||||
database: IDBDatabase,
|
||||
name: string,
|
||||
options?: IDBObjectStoreParameters
|
||||
): IDBObjectStore {
|
||||
if (database.objectStoreNames.contains(name)) {
|
||||
return (database.transaction(name, 'readonly') as IDBTransaction).objectStore(name);
|
||||
}
|
||||
|
||||
return database.createObjectStore(name, options);
|
||||
}
|
||||
|
||||
private ensureIndex(store: IDBObjectStore, name: string, keyPath: string): void {
|
||||
if (!store.indexNames.contains(name)) {
|
||||
store.createIndex(name, keyPath, { unique: false });
|
||||
}
|
||||
}
|
||||
|
||||
private createTransaction(
|
||||
storeNames: string | string[],
|
||||
mode: IDBTransactionMode
|
||||
|
||||
Reference in New Issue
Block a user