UNPKG

rxdb

Version:

A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/

287 lines (269 loc) 9.45 kB
import { firstValueFrom, filter, Observable, Subject, Subscription } from 'rxjs'; import type { BulkWriteRow, EventBulk, RxDocumentData, RxJsonSchema, RxStorage, RxStorageBulkWriteResponse, RxStorageChangeEvent, RxStorageCountResult, RxStorageInstance, RxStorageInstanceCreationParams, RxStorageQueryResult } from '../../types/index.d.ts'; import { RXDB_VERSION, randomToken } from '../../plugins/utils/index.ts'; import type { MessageFromRemote, MessageToRemote, RemoteMessageChannel, RxStorageRemoteInternals, RxStorageRemoteSettings } from './storage-remote-types.ts'; import { closeMessageChannel, getMessageChannel } from './message-channel-cache.ts'; export class RxStorageRemote implements RxStorage<RxStorageRemoteInternals, any> { public readonly name: string = 'remote'; public readonly rxdbVersion = RXDB_VERSION; private seed: string = randomToken(10); private lastRequestId: number = 0; public messageChannelIfOneMode?: Promise<RemoteMessageChannel>; constructor( public readonly settings: RxStorageRemoteSettings ) { if (settings.mode === 'one') { this.messageChannelIfOneMode = getMessageChannel( settings, [], true ); } } public getRequestId() { const newId = this.lastRequestId++; return this.seed + '|' + newId; } async createStorageInstance<RxDocType>( params: RxStorageInstanceCreationParams<RxDocType, any> ): Promise<RxStorageInstanceRemote<RxDocType>> { const connectionId = 'c|' + this.getRequestId(); const cacheKeys: string[] = [ 'mode-' + this.settings.mode ]; switch (this.settings.mode) { case 'collection': cacheKeys.push('collection-' + params.collectionName); // eslint-disable-next-line no-fallthrough case 'database': cacheKeys.push('database-' + params.databaseName); // eslint-disable-next-line no-fallthrough case 'storage': cacheKeys.push('seed-' + this.seed); } const messageChannel = await (this.messageChannelIfOneMode ? this.messageChannelIfOneMode : getMessageChannel( this.settings, cacheKeys ) ); const requestId = this.getRequestId(); const waitForOkPromise = firstValueFrom(messageChannel.messages$.pipe( filter(msg => msg.answerTo === requestId) )); messageChannel.send({ connectionId, method: 'create', version: RXDB_VERSION, requestId, params }); const waitForOkResult = await waitForOkPromise; if (waitForOkResult.error) { await closeMessageChannel(messageChannel); throw new Error('could not create instance ' + JSON.stringify(waitForOkResult.error)); } return new RxStorageInstanceRemote( this, params.databaseName, params.collectionName, params.schema, { params, connectionId, messageChannel }, params.options ); } async customRequest<In, Out>(data: In): Promise<Out> { const messageChannel = await this.settings.messageChannelCreator(); const requestId = this.getRequestId(); const connectionId = 'custom|request|' + requestId; const waitForAnswerPromise = firstValueFrom(messageChannel.messages$.pipe( filter(msg => msg.answerTo === requestId) )); messageChannel.send({ connectionId, method: 'custom', version: RXDB_VERSION, requestId, params: data }); const response = await waitForAnswerPromise; if (response.error) { await messageChannel.close(); throw new Error('could not run customRequest(): ' + JSON.stringify({ data, error: response.error })); } else { await messageChannel.close(); return response.return; } } } /** * Because postMessage() can be very slow on complex objects, * and some RxStorage implementations do need a JSON-string internally * anyway, it is allowed to transfer a string instead of an object * which must then be JSON.parse()-ed before RxDB can use it. * @link https://surma.dev/things/is-postmessage-slow/ */ function getMessageReturn( msg: MessageFromRemote ) { if (msg.method === 'getAttachmentData') { return msg.return; } else { if (typeof msg.return === 'string') { return JSON.parse(msg.return); } else { return msg.return; } } } export class RxStorageInstanceRemote<RxDocType> implements RxStorageInstance<RxDocType, RxStorageRemoteInternals, any, any> { private changes$: Subject<EventBulk<RxStorageChangeEvent<RxDocumentData<RxDocType>>, any>> = new Subject(); private subs: Subscription[] = []; private closed?: Promise<void>; messages$: Observable<MessageFromRemote>; constructor( public readonly storage: RxStorageRemote, public readonly databaseName: string, public readonly collectionName: string, public readonly schema: Readonly<RxJsonSchema<RxDocumentData<RxDocType>>>, public readonly internals: RxStorageRemoteInternals, public readonly options: Readonly<any> ) { this.messages$ = this.internals.messageChannel.messages$.pipe( filter(msg => msg.connectionId === this.internals.connectionId) ); this.subs.push( this.messages$.subscribe(msg => { if (msg.method === 'changeStream') { this.changes$.next(getMessageReturn(msg)); } }) ); } private async requestRemote( methodName: keyof RxStorageInstance<any, any, any>, params: any ) { const requestId = this.storage.getRequestId(); const responsePromise = firstValueFrom( this.messages$.pipe( filter(msg => msg.answerTo === requestId) ) ); const message: MessageToRemote = { connectionId: this.internals.connectionId, requestId, version: RXDB_VERSION, method: methodName, params }; this.internals.messageChannel.send(message); const response = await responsePromise; if (response.error) { throw new Error('could not requestRemote: ' + JSON.stringify({ methodName, params, error: response.error }, null, 4)); } else { return getMessageReturn(response); } } bulkWrite( documentWrites: BulkWriteRow<RxDocType>[], context: string ): Promise<RxStorageBulkWriteResponse<RxDocType>> { return this.requestRemote('bulkWrite', [documentWrites, context]); } findDocumentsById(ids: string[], deleted: boolean): Promise<RxDocumentData<RxDocType>[]> { return this.requestRemote('findDocumentsById', [ids, deleted]); } query(preparedQuery: any): Promise<RxStorageQueryResult<RxDocType>> { return this.requestRemote('query', [preparedQuery]); } count(preparedQuery: any): Promise<RxStorageCountResult> { return this.requestRemote('count', [preparedQuery]); } getAttachmentData(documentId: string, attachmentId: string, digest: string): Promise<string> { return this.requestRemote('getAttachmentData', [documentId, attachmentId, digest]); } getChangedDocumentsSince( limit: number, checkpoint?: any ): Promise< { documents: RxDocumentData<RxDocType>[]; checkpoint: any; }> { return this.requestRemote('getChangedDocumentsSince', [limit, checkpoint]); } changeStream(): Observable<EventBulk<RxStorageChangeEvent<RxDocumentData<RxDocType>>, any>> { return this.changes$.asObservable(); } cleanup(minDeletedTime: number): Promise<boolean> { return this.requestRemote('cleanup', [minDeletedTime]); } async close(): Promise<void> { if (this.closed) { return this.closed; } this.closed = (async () => { this.subs.forEach(sub => sub.unsubscribe()); this.changes$.complete(); await this.requestRemote('close', []); await closeMessageChannel(this.internals.messageChannel); })(); return this.closed; } async remove(): Promise<void> { if (this.closed) { throw new Error('already closed'); } this.closed = (async () => { await this.requestRemote('remove', []); await closeMessageChannel(this.internals.messageChannel); })(); return this.closed; } } export function getRxStorageRemote(settings: RxStorageRemoteSettings): RxStorageRemote { const withDefaults = Object.assign({ mode: 'storage' }, settings); return new RxStorageRemote(withDefaults); }