rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
289 lines (260 loc) • 11.3 kB
text/typescript
import {
RxDBLeaderElectionPlugin
} from '../leader-election/index.ts';
import type {
RxCollection,
ReplicationPullOptions,
ReplicationPushOptions,
RxReplicationWriteToMasterRow,
RxReplicationPullStreamItem
} from '../../types/index.d.ts';
import {
RxReplicationState,
startReplicationOnLeaderShip
} from '../replication/index.ts';
import {
addRxPlugin
} from '../../index.ts';
import type {
GoogleDriveCheckpointType,
GoogleDriveOptions,
GoogleDriveOptionsWithDefaults,
SyncOptionsGoogleDrive
} from './google-drive-types.ts';
import { Subject } from 'rxjs';
import { DriveStructure, initDriveStructure } from './init.ts';
import { handleUpstreamBatch } from './upstream.ts';
import { fetchChanges } from './downstream.ts';
import { commitTransaction, runInTransaction, startTransaction } from './transaction.ts';
import {
ensureProcessNextTickIsSet
} from '../replication-webrtc/connection-handler-simple-peer.ts';
import { SignalingOptions, SignalingState } from './signaling.ts';
import {
deserializeDocAttachments,
serializeDocAttachments
} from './document-handling.ts';
export * from './google-drive-types.ts';
export * from './google-drive-helper.ts';
export * from './transaction.ts';
export * from './init.ts';
export * from './document-handling.ts';
export * from './multipart.ts';
export * from './downstream.ts';
export * from './upstream.ts';
export * from './signaling.ts';
export const DEFAULT_TRANSACTION_TIMEOUT = 60 * 1000;
export class RxGoogleDriveReplicationState<RxDocType> extends RxReplicationState<RxDocType, GoogleDriveCheckpointType> {
/**
* Only exists on live replication
*/
public signalingState?: SignalingState;
constructor(
public readonly googleDrive: GoogleDriveOptionsWithDefaults,
public readonly driveStructure: DriveStructure,
public readonly replicationIdentifierHash: string,
public readonly collection: RxCollection<RxDocType, any, any, any>,
public readonly pull?: ReplicationPullOptions<RxDocType, GoogleDriveCheckpointType>,
public readonly push?: ReplicationPushOptions<RxDocType>,
public readonly signalingOptions?: SignalingOptions,
public readonly live: boolean = true,
public retryTime: number = 1000 * 5,
public autoStart: boolean = true
) {
super(
replicationIdentifierHash,
collection,
'_deleted',
pull,
push,
live,
retryTime,
autoStart
);
}
/**
* Notify other peers that something
* has or might have changed so that
* they can pull from their checkpoints.
*/
async notifyPeers() {
if (this.signalingState) {
await this.signalingState.pingPeers('RESYNC');
}
}
}
export async function replicateGoogleDrive<RxDocType>(
options: SyncOptionsGoogleDrive<RxDocType>
): Promise<RxGoogleDriveReplicationState<RxDocType>> {
const collection: RxCollection<RxDocType, any, any> = options.collection;
addRxPlugin(RxDBLeaderElectionPlugin);
const googleDriveOptionsWithDefaults: GoogleDriveOptionsWithDefaults = Object.assign(
{
apiEndpoint: 'https://www.googleapis.com',
transactionTimeout: DEFAULT_TRANSACTION_TIMEOUT,
space: 'drive',
folderPath: ''
},
options.googleDrive
);
if (typeof googleDriveOptionsWithDefaults.folderPath !== 'string') {
googleDriveOptionsWithDefaults.folderPath = '';
}
const driveStructure = await initDriveStructure(googleDriveOptionsWithDefaults);
/**
* When true, attachment binary data is serialised as base64 inside the
* document JSON file stored on Google Drive. Defaults to true only when
* the collection schema has `attachments: {}` defined. Can be disabled
* explicitly by passing `attachments: false` in the options.
*/
const replicateAttachments = options.attachments !== false && !!collection.schema.jsonSchema.attachments;
let replicationState: RxGoogleDriveReplicationState<RxDocType>;
const pullStream$: Subject<RxReplicationPullStreamItem<RxDocType, GoogleDriveCheckpointType>> = new Subject();
let replicationPrimitivesPull: ReplicationPullOptions<RxDocType, GoogleDriveCheckpointType> | undefined;
options.live = typeof options.live === 'undefined' ? true : options.live;
options.waitForLeadership = typeof options.waitForLeadership === 'undefined' ? true : options.waitForLeadership;
if (options.pull) {
replicationPrimitivesPull = {
async handler(
lastPulledCheckpoint: GoogleDriveCheckpointType | undefined,
batchSize: number
) {
return runInTransaction(
googleDriveOptionsWithDefaults,
driveStructure,
collection.schema.primaryPath,
async () => {
const changes = await fetchChanges<RxDocType>(
googleDriveOptionsWithDefaults,
driveStructure,
lastPulledCheckpoint,
batchSize
);
/**
* Convert base64 attachment data that was stored in the
* Google Drive JSON file back to Blobs so that the
* downstream replication protocol can write them to the
* fork storage instance correctly.
*/
if (replicateAttachments) {
await Promise.all(
changes.documents.map(doc => deserializeDocAttachments(doc as any))
);
}
return changes as any;
}
);
},
batchSize: options.pull.batchSize,
modifier: options.pull.modifier,
stream$: pullStream$.asObservable(),
initialCheckpoint: options.pull.initialCheckpoint
};
}
let replicationPrimitivesPush: ReplicationPushOptions<RxDocType> | undefined;
if (options.push) {
replicationPrimitivesPush = {
async handler(
rows: RxReplicationWriteToMasterRow<RxDocType>[]
) {
/**
* Convert Blob attachment data to base64 strings before the
* rows are written to the WAL file or stored as document JSON
* in Google Drive. We create new row objects so the originals
* (which the replication protocol still uses for meta updates)
* are not mutated.
* When attachments are disabled we still run the conversion so
* that Blob values are stripped rather than serialised as `{}`
* by JSON.stringify.
*/
const rowsForDrive: RxReplicationWriteToMasterRow<RxDocType>[] =
await Promise.all(
rows.map(async row => {
const newState = await serializeDocAttachments(
row.newDocumentState,
replicateAttachments
);
return { ...row, newDocumentState: newState };
})
);
return runInTransaction(
googleDriveOptionsWithDefaults,
driveStructure,
collection.schema.primaryPath,
async () => {
const conflicts = await handleUpstreamBatch(
googleDriveOptionsWithDefaults,
driveStructure,
options.collection.schema.primaryPath as any,
rowsForDrive
);
/**
* When attachment replication is enabled, deserialise the
* base64 attachment data that Google Drive stored in the
* `_attachments_data` field back into Blobs on each conflict
* document before returning them to the replication protocol.
*
* The protocol passes these as `realMasterState` to
* `resolveConflictError`, which (with the fix in
* `conflicts.ts`) then writes the resolved state — including
* the correct attachment Blob — to the fork instance.
* Without this deserialisation the fork would receive stubs
* with no binary data and the attachment would be lost.
*/
if (replicateAttachments && conflicts.length > 0) {
await Promise.all(
conflicts.map(c => deserializeDocAttachments(c as any))
);
}
return conflicts;
},
() => replicationState.notifyPeers().catch(() => { })
);
},
batchSize: options.push.batchSize,
modifier: options.push.modifier
};
}
replicationState = new RxGoogleDriveReplicationState<RxDocType>(
googleDriveOptionsWithDefaults,
driveStructure,
options.replicationIdentifier,
collection,
replicationPrimitivesPull,
replicationPrimitivesPush,
options.signalingOptions,
options.live,
options.retryTime,
options.autoStart
);
/**
* Google drive has no websocket or server-send-events
* to observe file changes. Therefore we use WebRTC to
* connect clients which then can ping each other on changes.
* Instead of a signaling server, we use the google-drive itself
* to exchange signaling data.
*/
if (options.live && options.pull) {
ensureProcessNextTickIsSet();
const startBefore = replicationState.start.bind(replicationState);
const cancelBefore = replicationState.cancel.bind(replicationState);
replicationState.start = () => {
replicationState.signalingState = new SignalingState(
replicationState.googleDrive,
replicationState.driveStructure,
options.signalingOptions ? options.signalingOptions : {}
);
const sub = replicationState.signalingState.resync$.subscribe(() => {
replicationState.reSync();
});
replicationState.cancel = () => {
sub.unsubscribe();
replicationState.signalingState?.close();
return cancelBefore();
};
return startBefore();
};
}
startReplicationOnLeaderShip(options.waitForLeadership, replicationState);
return replicationState;
}