UNPKG

rxdb

Version:

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

199 lines (195 loc) 7.69 kB
import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose"; import { ensureNotFalsy, errorToPlainJson } from "../../plugins/utils/index.js"; import { RxDBLeaderElectionPlugin } from "../leader-election/index.js"; import { RxReplicationState, startReplicationOnLeaderShip } from "../replication/index.js"; import { addRxPlugin, newRxError } from "../../index.js"; import { Subject } from 'rxjs'; import { connect, DeliverPolicy, JSONCodec, ReplayPolicy } from 'nats'; import { getNatsServerDocumentState } from "./nats-helper.js"; import { awaitRetry } from "../replication/replication-helper.js"; export * from "./nats-types.js"; export * from "./nats-helper.js"; export var RxNatsReplicationState = /*#__PURE__*/function (_RxReplicationState) { function RxNatsReplicationState(replicationIdentifier, collection, pull, push, live = true, retryTime = 1000 * 5, autoStart = true) { var _this; _this = _RxReplicationState.call(this, replicationIdentifier, collection, '_deleted', pull, push, live, retryTime, autoStart) || this; _this.replicationIdentifier = replicationIdentifier; _this.collection = collection; _this.pull = pull; _this.push = push; _this.live = live; _this.retryTime = retryTime; _this.autoStart = autoStart; return _this; } _inheritsLoose(RxNatsReplicationState, _RxReplicationState); return RxNatsReplicationState; }(RxReplicationState); export function replicateNats(options) { options.live = typeof options.live === 'undefined' ? true : options.live; options.waitForLeadership = typeof options.waitForLeadership === 'undefined' ? true : options.waitForLeadership; var collection = options.collection; var primaryPath = collection.schema.primaryPath; addRxPlugin(RxDBLeaderElectionPlugin); var jc = JSONCodec(); var connectionStatePromise = (async () => { var nc = await connect(options.connection); var jetstreamClient = nc.jetstream(); var jsm = await nc.jetstreamManager(); await jsm.streams.add({ name: options.streamName, subjects: [options.subjectPrefix + '.*'] }); var natsStream = await jetstreamClient.streams.get(options.streamName); return { nc, jetstreamClient, jsm, natsStream }; })(); var pullStream$ = new Subject(); var replicationPrimitivesPull; if (options.pull) { replicationPrimitivesPull = { async handler(lastPulledCheckpoint, batchSize) { var cn = await connectionStatePromise; var newCheckpoint = { sequence: lastPulledCheckpoint ? lastPulledCheckpoint.sequence : 0 }; var consumer = await cn.natsStream.getConsumer({ opt_start_seq: lastPulledCheckpoint ? lastPulledCheckpoint.sequence : 0, deliver_policy: DeliverPolicy.LastPerSubject, replay_policy: ReplayPolicy.Instant }); var fetchedMessages = await consumer.fetch({ max_messages: batchSize }); await fetchedMessages.signal; await fetchedMessages.close(); var useMessages = []; for await (var m of fetchedMessages) { useMessages.push(m.json()); newCheckpoint.sequence = m.seq; m.ack(); } return { documents: useMessages, checkpoint: newCheckpoint }; }, batchSize: ensureNotFalsy(options.pull).batchSize, modifier: ensureNotFalsy(options.pull).modifier, stream$: pullStream$.asObservable() }; } var replicationPrimitivesPush; if (options.push) { replicationPrimitivesPush = { async handler(rows) { var cn = await connectionStatePromise; var conflicts = []; await Promise.all(rows.map(async writeRow => { var docId = writeRow.newDocumentState[primaryPath]; /** * first get the current state of the documents from the server * so that we have the sequence number for conflict detection. */ var remoteDocState; try { remoteDocState = await getNatsServerDocumentState(cn.natsStream, options.subjectPrefix, docId); } catch (err) { if (!err.message.includes('no message found')) { throw err; } } if (remoteDocState && (!writeRow.assumedMasterState || collection.conflictHandler.isEqual(remoteDocState.json(), writeRow.assumedMasterState, 'replication-nats-push') === false)) { // conflict conflicts.push(remoteDocState.json()); } else { // no conflict (yet) var pushDone = false; while (!pushDone) { try { await cn.jetstreamClient.publish(options.subjectPrefix + '.' + docId, jc.encode(writeRow.newDocumentState), { expect: remoteDocState ? { streamName: options.streamName, lastSubjectSequence: remoteDocState.seq } : undefined }); pushDone = true; } catch (err) { if (err.message.includes('wrong last sequence')) { // A write happened while we are doing our write -> handle conflict var newServerState = await getNatsServerDocumentState(cn.natsStream, options.subjectPrefix, docId); conflicts.push(ensureNotFalsy(newServerState).json()); pushDone = true; } else { replicationState.subjects.error.next(newRxError('RC_STREAM', { document: writeRow.newDocumentState, error: errorToPlainJson(err) })); // -> retry after wait await awaitRetry(collection, replicationState.retryTime); } } } } })); return conflicts; }, batchSize: options.push.batchSize, modifier: options.push.modifier }; } var replicationState = new RxNatsReplicationState(options.replicationIdentifier, collection, replicationPrimitivesPull, replicationPrimitivesPush, options.live, options.retryTime, options.autoStart); /** * Use long polling to get live changes for the pull.stream$ */ if (options.live && options.pull) { var startBefore = replicationState.start.bind(replicationState); var cancelBefore = replicationState.cancel.bind(replicationState); replicationState.start = async () => { var cn = await connectionStatePromise; /** * First get the last sequence so that we can * laster only fetch 'newer' messages. */ var lastSeq = 0; try { var lastDocState = await cn.natsStream.getMessage({ last_by_subj: options.subjectPrefix + '.*' }); lastSeq = lastDocState.seq; } catch (err) { if (!err.message.includes('no message found')) { throw err; } } var consumer = await cn.natsStream.getConsumer({ opt_start_seq: lastSeq }); var newMessages = await consumer.consume(); (async () => { for await (var m of newMessages) { var docData = m.json(); pullStream$.next({ documents: [docData], checkpoint: { sequence: m.seq } }); m.ack(); } })(); replicationState.cancel = () => { newMessages.close(); return cancelBefore(); }; return startBefore(); }; } startReplicationOnLeaderShip(options.waitForLeadership, replicationState); return replicationState; } //# sourceMappingURL=index.js.map