rxdb
Version:
A local-first realtime NoSQL Database for JavaScript applications - https://rxdb.info/
239 lines (232 loc) • 8.57 kB
JavaScript
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.RxSupabaseReplicationState = void 0;
exports.replicateSupabase = replicateSupabase;
var _inheritsLoose2 = _interopRequireDefault(require("@babel/runtime/helpers/inheritsLoose"));
var _index = require("../replication/index.js");
var _plugin = require("../../plugin.js");
var _index2 = require("../leader-election/index.js");
var _rxjs = require("rxjs");
var _helper = require("./helper.js");
var _index3 = require("../utils/index.js");
var RxSupabaseReplicationState = exports.RxSupabaseReplicationState = /*#__PURE__*/function (_RxReplicationState) {
function RxSupabaseReplicationState(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;
}
(0, _inheritsLoose2.default)(RxSupabaseReplicationState, _RxReplicationState);
return RxSupabaseReplicationState;
}(_index.RxReplicationState);
function replicateSupabase(options) {
options = (0, _index3.flatClone)(options);
(0, _plugin.addRxPlugin)(_index2.RxDBLeaderElectionPlugin);
var collection = options.collection;
var primaryPath = collection.schema.primaryPath;
// set defaults
options.waitForLeadership = typeof options.waitForLeadership === 'undefined' ? true : options.waitForLeadership;
options.live = typeof options.live === 'undefined' ? true : options.live;
var modifiedField = options.modifiedField ? options.modifiedField : _helper.DEFAULT_MODIFIED_FIELD;
var deletedField = options.deletedField ? options.deletedField : _helper.DEFAULT_DELETED_FIELD;
var pullStream$ = new _rxjs.Subject();
var replicationPrimitivesPull;
function rowToDoc(row) {
var deleted = !!row[deletedField];
var modified = row[modifiedField];
var doc = (0, _index3.flatClone)(row);
delete doc[deletedField];
delete doc[modifiedField];
doc._deleted = deleted;
/**
* Only keep the modified value if that field is defined
* in the schema.
*/
if (collection.schema.jsonSchema.properties[modifiedField]) {
doc[modifiedField] = modified;
}
return doc;
}
async function fetchById(id) {
var {
data,
error
} = await options.client.from(options.tableName).select().eq(primaryPath, id).limit(1);
if (error) throw error;
if (data.length != 1) throw new Error('doc not found ' + id);
return rowToDoc(data[0]);
}
if (options.pull) {
replicationPrimitivesPull = {
async handler(lastPulledCheckpoint, batchSize) {
var query = options.client.from(options.tableName).select('*');
if (options.pull?.queryBuilder) {
var maybeNewQuery = options.pull.queryBuilder({
query,
lastPulledCheckpoint,
batchSize
});
if (maybeNewQuery) {
query = maybeNewQuery;
}
}
if (lastPulledCheckpoint) {
var {
modified,
id
} = lastPulledCheckpoint;
// WHERE modified > :m OR (modified = :m AND id > :id)
// PostgREST or() takes comma-separated disjuncts; use nested and() for the tie-breaker.
// Wrap identifiers with double quotes to be safe if they're mixed-case.
query = query.or("\"" + modifiedField + "\".gt." + modified + ",and(\"" + modifiedField + "\".eq." + modified + ",\"" + primaryPath + "\".gt." + id + ")");
}
// deterministic order & batch size
query = query.order(modifiedField, {
ascending: true
}).order(primaryPath, {
ascending: true
}).limit(batchSize);
var {
data,
error
} = await query;
if (error) {
throw error;
}
var lastDoc = (0, _index3.lastOfArray)(data);
var newCheckpoint = lastDoc ? {
id: lastDoc[primaryPath],
modified: lastDoc[modifiedField]
} : undefined;
var docs = data.map(row => rowToDoc(row));
return {
documents: docs,
checkpoint: newCheckpoint
};
},
batchSize: (0, _index3.ensureNotFalsy)(options.pull).batchSize,
modifier: (0, _index3.ensureNotFalsy)(options.pull).modifier,
stream$: pullStream$.asObservable(),
initialCheckpoint: options.pull.initialCheckpoint
};
}
var replicationPrimitivesPush = options.push ? {
batchSize: options.push.batchSize,
initialCheckpoint: options.push.initialCheckpoint,
modifier: options.push.modifier,
async handler(rows) {
async function insertOrReturnConflict(doc) {
var id = doc[primaryPath];
var {
error
} = await options.client.from(options.tableName).insert(doc);
if (!error) {
return;
} else if (error.code == _helper.POSTGRES_INSERT_CONFLICT_CODE) {
// conflict!
var conflict = await fetchById(id);
return conflict;
} else {
throw error;
}
}
async function updateOrReturnConflict(doc, assumedMasterState) {
(0, _index3.ensureNotFalsy)(assumedMasterState);
var id = doc[primaryPath];
var toRow = (0, _index3.flatClone)(doc);
if (doc._deleted) {
toRow[deletedField] = !!doc._deleted;
if (deletedField !== '_deleted') {
delete toRow._deleted;
}
}
// modified field will be set server-side
delete toRow[modifiedField];
var query = options.client.from(options.tableName).update(toRow);
query = (0, _helper.addDocEqualityToQuery)(collection.schema.jsonSchema, deletedField, modifiedField, assumedMasterState, query);
var {
data,
error
} = await query.select();
if (error) {
throw error;
}
if (data && data.length > 0) {
return;
} else {
// no match -> conflict
return await fetchById(id);
}
}
var conflicts = [];
await Promise.all(rows.map(async row => {
var newDoc = row.newDocumentState;
if (!row.assumedMasterState) {
var c = await insertOrReturnConflict(newDoc);
if (c) conflicts.push(c);
} else {
var _c = await updateOrReturnConflict(newDoc, row.assumedMasterState);
if (_c) conflicts.push(_c);
}
}));
return conflicts;
}
} : undefined;
var replicationState = new RxSupabaseReplicationState(options.replicationIdentifier, collection, replicationPrimitivesPull, replicationPrimitivesPush, options.live, options.retryTime, options.autoStart);
/**
* Subscribe to changes for the pull.stream$
*/
if (options.live && options.pull) {
var startBefore = replicationState.start.bind(replicationState);
var cancelBefore = replicationState.cancel.bind(replicationState);
replicationState.start = () => {
var sub = options.client.channel('realtime:' + options.tableName).on('postgres_changes', {
event: '*',
schema: 'public',
table: options.tableName
}, payload => {
/**
* We assume soft-deletes in supabase
* and therefore cleanup-hard-deletes
* are not relevant for the sync.
*/
if (payload.eventType === 'DELETE') {
return;
}
var row = payload.new;
var doc = rowToDoc(row);
pullStream$.next({
checkpoint: {
id: doc[primaryPath],
modified: row[modifiedField]
},
documents: [doc]
});
}).subscribe(status => {
/**
* Trigger resync flag on reconnects
*/
if (status === 'SUBSCRIBED') {
pullStream$.next('RESYNC');
}
});
replicationState.cancel = () => {
sub.unsubscribe();
return cancelBefore();
};
return startBefore();
};
}
(0, _index.startReplicationOnLeaderShip)(options.waitForLeadership, replicationState);
return replicationState;
}
//# sourceMappingURL=index.js.map