diff --git a/packages/minimongo/local_collection.js b/packages/minimongo/local_collection.js index 43877fb87e..9f0f4a9193 100644 --- a/packages/minimongo/local_collection.js +++ b/packages/minimongo/local_collection.js @@ -160,6 +160,10 @@ export default class LocalCollection { return id; } + insertAsync(doc, callback) { + return new Promise(resolve => resolve(this.insert(doc, callback))); + } + // Pause the observers. No callbacks from observers will fire until // 'resumeObservers' is called. pauseObservers() { @@ -273,6 +277,10 @@ export default class LocalCollection { return result; } + removeAsync(selector, callback) { + return new Promise(resolve => resolve(this.remove(selector, callback))); + } + // Resume the observers. Observers immediately receive change // notifications to bring them to the current state of the // database. Note that this is not just replaying all the changes that @@ -482,6 +490,10 @@ export default class LocalCollection { return result; } + updateAsync(selector, mod, options, callback) { + return new Promise(resolve => resolve(this.update(selector, mod, options, callback))); + } + // A convenience wrapper on update. LocalCollection.upsert(sel, mod) is // equivalent to LocalCollection.update(sel, mod, {upsert: true, // _returnObject: true}). diff --git a/packages/mongo/mongo_livedata_tests.js b/packages/mongo/mongo_livedata_tests.js index 62cf35fad0..eee820d8d8 100644 --- a/packages/mongo/mongo_livedata_tests.js +++ b/packages/mongo/mongo_livedata_tests.js @@ -3497,6 +3497,226 @@ if (Meteor.isServer) { }); } +testAsyncMulti('mongo-livedata - collection sync operations data persistence', [ + function (test) { // Using remote collection + const Collection = new Mongo.Collection( + `remotesyncop_persistence${test.runId()}`, + ); + + Collection.insert({ _id: 'a' }); + Collection.update({ _id: 'a' }, { $set: { num: 1 } }); + const insertedId = Collection.insert({ num: 2 }); + + let items = Collection.find().fetch(); + let itemIds = items.map(_item => _item._id); + test.equal(itemIds, ['a', insertedId]); // temporary data accessible (optimistic-ui) + + const aItem = items[0]; + const insertedItem = items[1]; + test.equal(aItem?.num, 1); + test.equal(insertedItem?.num, 2); + + Collection.remove({ _id: insertedId }); + + items = Collection.find().fetch(); + itemIds = items.map(_item => _item._id); + + test.equal(itemIds, ['a']); // temporary data accessible (optimistic-ui) + + if (Meteor.isClient) { + return new Promise(resolve => { + Meteor.setTimeout(async () => { + items = Collection.find().fetch(); + itemIds = items.map(_item => _item._id); + test.equal(itemIds, []); // data IS NOT persisted + resolve(); + }, 10); + }); + } + + return Promise.resolve(); + }, + async function (test) { // Using local collection + const Collection = new Mongo.Collection( + `localsyncop_persistence${test.runId()}`, + ); + + Collection._collection.insert({ _id: 'a' }); + Collection._collection.update({ _id: 'a' }, { $set: { num: 1 } }); + const insertedId = Collection._collection.insert({ num: 2 }); + + let items = Collection.find().fetch(); + let itemIds = items.map(_item => _item._id); + test.equal(itemIds, ['a', insertedId]); // temporary data accessible (optimistic-ui) + + const aItem = items[0]; + const insertedItem = items[1]; + test.equal(aItem?.num, 1); + test.equal(insertedItem?.num, 2); + + Collection._collection.remove({ _id: insertedId }); + + items = Collection.find().fetch(); + itemIds = items.map(_item => _item._id); + + test.equal(itemIds, ['a']); // temporary data accessible (optimistic-ui) + + if (Meteor.isClient) { + return new Promise(resolve => { + Meteor.setTimeout(() => { + items = Collection.find().fetch(); + itemIds = items.map(_item => _item._id); + test.equal(itemIds, ['a']); // data is persisted + resolve(); + }, 10); + }); + } + + return Promise.resolve(); + }, + function (test) { // Using methods + const Collection = new Mongo.Collection( + `methodsyncop_persistence${test.runId()}`, + ); + + Meteor.methods({ + [`insertSyncMethodPersistence${test.runId()}`]: async () => { + Collection.insert({ _id: 'a' }); + }, + }); + + Meteor.call(`insertSyncMethodPersistence${test.runId()}`); + + let items = Collection.find().fetch(); + let itemIds = items.map(_item => _item._id); + + test.equal(itemIds, ['a']); // temporary data accessible (optimistic-ui) + + if (Meteor.isClient) { + return new Promise(resolve => { + Meteor.setTimeout(() => { + items = Collection.find().fetch(); + itemIds = items.map(_item => _item._id); + test.equal(itemIds, []); // data IS NOT persisted + resolve(); + }, 10); + }); + } + + return Promise.resolve(); + }, +]); + +testAsyncMulti('mongo-livedata - collection async operations data persistence', [ + async function (test) { // Using remote collection + const Collection = new Mongo.Collection( + `remoteop_persistence${test.runId()}`, + ); + + await Collection.insertAsync({ _id: 'a' }); + await Collection.updateAsync({ _id: 'a' }, { $set: { num: 1 } }); + const insertedId = await Collection.insertAsync({ num: 2 }); + + let items = await Collection.find().fetchAsync(); + let itemIds = items.map(_item => _item._id); + test.equal(itemIds, ['a', insertedId]); // temporary data accessible (optimistic-ui) + + const aItem = items[0]; + const insertedItem = items[1]; + test.equal(aItem?.num, 1); + test.equal(insertedItem?.num, 2); + + await Collection.removeAsync({ _id: insertedId }); + + items = await Collection.find().fetchAsync(); + itemIds = items.map(_item => _item._id); + + test.equal(itemIds, ['a']); // temporary data accessible (optimistic-ui) + + if (Meteor.isClient) { + return new Promise(resolve => { + Meteor.setTimeout(async () => { + items = await Collection.find().fetchAsync(); + itemIds = items.map(_item => _item._id); + test.equal(itemIds, []); // data IS NOT persisted + resolve(); + }, 10); + }); + } + + return Promise.resolve(); + }, + async function (test) { // Using local collection + const Collection = new Mongo.Collection( + `localop_persistence${test.runId()}`, + ); + + await Collection._collection.insertAsync({ _id: 'a' }); + await Collection._collection.updateAsync({ _id: 'a' }, { $set: { num: 1 } }); + const insertedId = await Collection._collection.insertAsync({ num: 2 }); + + let items = await Collection.find().fetchAsync(); + let itemIds = items.map(_item => _item._id); + test.equal(itemIds, ['a', insertedId]); // temporary data accessible (optimistic-ui) + + const aItem = items[0]; + const insertedItem = items[1]; + test.equal(aItem?.num, 1); + test.equal(insertedItem?.num, 2); + + await Collection._collection.removeAsync({ _id: insertedId }); + + items = await Collection.find().fetchAsync(); + itemIds = items.map(_item => _item._id); + + test.equal(itemIds, ['a']); // temporary data accessible (optimistic-ui) + + if (Meteor.isClient) { + return new Promise(resolve => { + Meteor.setTimeout(async () => { + items = await Collection.find().fetchAsync(); + itemIds = items.map(_item => _item._id); + test.equal(itemIds, ['a']); // data is persisted + resolve(); + }, 10); + }); + } + + return Promise.resolve(); + }, + async function (test) { // Using methods + const Collection = new Mongo.Collection( + `methodop_persistence${test.runId()}`, + ); + + Meteor.methods({ + [`insertMethodPersistence${test.runId()}`]: async () => { + await Collection.insertAsync({ _id: 'a' }); + }, + }); + + Meteor.callAsync(`insertMethodPersistence${test.runId()}`); + + let items = await Collection.find().fetchAsync(); + let itemIds = items.map(_item => _item._id); + + test.equal(itemIds, ['a']); // temporary data accessible (optimistic-ui) + + if (Meteor.isClient) { + return new Promise(resolve => { + Meteor.setTimeout(async () => { + items = await Collection.find().fetchAsync(); + itemIds = items.map(_item => _item._id); + test.equal(itemIds, []); // data IS NOT persisted + resolve(); + }, 10); + }); + } + + return Promise.resolve(); + }, +]); + testAsyncMulti("mongo-livedata - support observeChangesAsync and observeAsync to keep isomorphism on client and server", [ async (test) => { const Collection = new Mongo.Collection(`observe_changes_async${test.runId()}`);