-
Notifications
You must be signed in to change notification settings - Fork 457
Add support for fetching a particular version of a snapshot #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Things that may need discussion:
Function signatureIn the original issue, we had discussed the following signature: Connection.prototype.getSnapshot(collection: string, id: string, { version: number, timestamp: Date }, (Error, Snapshot) => void): void;However, in this change I have implemented the slightly different: Connection.prototype.getSnapshot(collection: string, id: string, version: (number|Date), (Error, Snapshot) => void): void;This is because Treatment of deleted documentsFor now, I have added a
Anything I've missedI'm very new to this codebase, so I've doubtlessly missed something. I've tried to implement everything I could see (including projections, middleware and hooking into |
README.md
Outdated
| Collection name of the snapshot | ||
| * `id` _(String)_ | ||
| ID of the snapshot | ||
| * `version` _(number | Date)_ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you make a difference between a number that represent a version and one that represent a date ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By checking typeof for number and instanceof for Date: https://github.com/share/sharedb/pull/220/files#diff-adc2354b96b029a19a73805a6f3f7fb4R19
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you don't support a date passed has a number. Ok, from my understanding of the ticket this was supposed to be implemented but it's not an issue for me 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I don't think there's any way we could do that sensibly without having two separate methods (which was my original proposal).
There's no real way to tell the difference between a really, really, really high version number and a really, really, really old diff.
test/client/snapshot-request.js
Outdated
|
|
||
| it('applies the projection to a snapshot', function (done) { | ||
| backend.connect().getSnapshot('bookTitles', 'don-quixote', 2, function (error, snapshot) { | ||
| if (error) done(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you may call twice done
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch
test/client/snapshot-request.js
Outdated
| beforeEach(function (done) { | ||
| var doc = backend.connect().get('books', 'catch-22'); | ||
| doc.create({ title: 'Catch 22' }, function (error) { | ||
| if (error) done(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you may call twice done
gkubisa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alecgibson thanks for implementing this feature. It looks great in general although I found a number of small issues which will need to be resolved before I can merge this PR. Thanks again 👍
README.md
Outdated
| id: string; // ID of the snapshot | ||
| version: number; // version number of the snapshot | ||
| timestamp: number; // the UNIX timestamp of the snapshot | ||
| deleted: boolean; // true if the returned version is a deleted snapshot |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we replace deleted with type for consistency with Doc? If a document exists at a particular version, type would be populated as for Doc, otherwise it would be null.
Also, regarding deleted documents, what's the expected behaviour? If a document is currently deleted (ie its final op is a deletion op), then do we tell the client that the document is deleted, regardless of version they try to fetch?
In my opinion the last recorded operation should not affect a snapshot in this way because a snapshot is a read-only view of the document at a specific version, while the last recorded operation changes over time, eg deleting and re-creating a document.
lib/agent.js
Outdated
| var op = this._createOp(request); | ||
| if (!op) return callback({code: 4000, message: 'Invalid op message'}); | ||
| return this._submit(request.c, request.d, op, callback); | ||
| case 'sv': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sv - snapshot version? What do you think about sf - snapshot fetch? It's probably more consistent with the other actions.
Maybe eventually we could also update f -> df, s -> ds and u -> du (d for document).
lib/backend.js
Outdated
| if (op.create) { | ||
| type = types.map[op.create.type]; | ||
| if (!type) return callback({ code: 4008, message: 'Unknown type' }); | ||
| snapshot = op.create.data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be snapshot = type.create(op.create.data);
lib/backend.js
Outdated
| } | ||
| } | ||
|
|
||
| if (!snapshot && !deleted) return callback({ code: 4015, message: 'Document does not exist' }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think snapshot needs to be verified... the create and apply functions should guarantee that it's always correct for the given type. Additionally, in this context we're applying operations which were applied successfully before, which gives us even more confidence in the correctness of the result. Furthermore, null, 0 and false are all valid snapshots for ot-json.
lib/backend.js
Outdated
| op = ops[index]; | ||
|
|
||
| if (typeof timestamp === 'number' && op.m.ts > timestamp) { | ||
| op = ops[index - 1]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If index === 0, op will become undefined, which will cause a TypeError further down in this function. I'd actually prefer tracking version and timestamp as local variables (just like type, snapshot, etc) and limit the usage of op to within the loop.
lib/client/connection.js
Outdated
| * @param collection - the collection name of the snapshot | ||
| * @param id - the ID of the snapshot | ||
| * @param version - the version number, or Date of the snapshot to fetch. If an exact version or Date match is not made, | ||
| * then the next lowest version is returned. ie if a document has 6 versions, asking for v7 will return v6. If ops |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"then the next lowest version is returned" - should it be "then the next lower version is returned"?
lib/client/connection.js
Outdated
| * id: string; // ID of the snapshot | ||
| * version: number; // version number of the snapshot | ||
| * timestamp: number; // the UNIX timestamp of the snapshot | ||
| * deleted: boolean; // true if the returned version is a deleted snapshot |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace deleted with type, as in README.md.
lib/client/snapshot-request.js
Outdated
| this.version = version; | ||
| } else if (version instanceof Date) { | ||
| this.timestamp = version.getTime(); | ||
| } else if (!version) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would version == null be more appropriate?
| SnapshotRequest.prototype._onConnectionStateChanged = function () { | ||
| if (this.connection.canSend && !this.sent) { | ||
| this.send(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If !this.connection.canSend, then probably set this.sent to false, so that the request would be resent after re-connection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It starts as false, and is only changed to true at the end of send().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the following situation:
- Snapshot request is sent
- Connection is dropped
- Connection is restored
_onConnectionStateChanged will not re-send the request because this.sent is true. The server will not send a response to the new WebSocket connection because the request was not sent on that new WebSocket connection and the old WebSocket connection has been closed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha cool. Still trying to piece together all the data flows around this library! Some of it's pretty confusing.
lib/client/connection.js
Outdated
| * } | ||
| * | ||
| */ | ||
| Connection.prototype.getSnapshot = function(collection, id, version, callback) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could version be an optional param?
|
@dcharbonnier & @gkubisa I've addressed all of your comments. Could you please re-review? |
curran
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me!
Really looking forward to this feature landing.
Thank you @alecgibson for your work on this.
| All other options are passed through to the database adapter. | ||
|
|
||
| `connection.getSnapshot(collection, id, version, callback): void;` | ||
| Get a read-only snapshot of a document at the requested version. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice documentation.
lib/backend.js
Outdated
| var options = { metadata: true }; | ||
| var timestampIsNumber = typeof timestamp === 'number'; | ||
|
|
||
| this._getOps(agent, index, id, 0, version, options, function (error, ops) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like if you specify a Date rather than a version number, all the ops are loaded into memory, but if you specify a version number, only the ops until that version are loaded into memory, is that right?
If this is the case, it may be worth mentioning the overhead of passing in a Date in the docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's exactly right. As far as I'm aware, with the current definition for getOps, this is unavoidable, right? I'll update the docs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, with the current definition for getOps, it appears to be unavoidable.
Although, conceivably, one could optimize that case by passing the date into the database driver, so the database query itself could specify "less than this date" (as the timestamps are there on the ops and queryable). That feels like a rabbit hole though, and would require changing the database driver API, and then implementing the change in the actual drivers. Maybe something to think about for the future, once this first iteration lands.
Thanks for updating the docs!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, it looks like all ops are always loaded into memory, see https://github.com/share/sharedb-mongo/blob/master/index.js#L362, so I'd say we don't need a specific warning about getting a snapshot for a Date. https://github.com/share/sharedb-mongo will need to be optimized but that would be a separate piece of work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fine - will reset
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gkubisa Oh wow, that's surprising! I would have thought sharedb-mongo would query against the version. Good catch.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(As a side-note, is anyone currently maintaining sharedb-mongo? I've got a PR sitting around there, but it looks like nobody's touched anything in over a year?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alecgibson This fork is active https://github.com/teamwork/sharedb-mongo/ . I'm using that fork currently as my dependency, instead of the original https://github.com/share/sharedb-mongo.
I opened a new issue just now Optimization opportunity: use "to" in getOps query share/sharedb-mongo#61
gkubisa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for fixing the previous bunch of issues. I still discovered a few more but then that should be it. 👍
lib/backend.js
Outdated
| } | ||
| } | ||
|
|
||
| type = type ? { name: type.name, uri: type.uri} : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For consistency with the other messages and to limit the amount of data sent over the network, I think we should send only type.uri in the response, without the Object wrapper. type.uri will then need to be resolved into the full type object (with name, uri and all the functions) on the client side.
lib/backend.js
Outdated
| var type; | ||
| var snapshot; | ||
| var fetchedTimestamp; | ||
| var fetchedVersion; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could the variables be initialized?
var type = null;
var snapshot = undefined;
var fetchedTimestamp = timestamp;
var fetchedVersion = 0;There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we should initialise fetchedTimestamp and fetchedVersion to these values. If the version can't be found, then we'll erroneously tell the consumer that we fetched things with these values, when we didn't.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Also the linter calls me out for initialising to undefined, which I think is a fair thing for it to complain about)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var type = null;- I suggested it only to simplify the implicit type of the variablenull | OtType, rather thanundefined | null | OtType. Not a big deal either way.var snapshot = undefined;- just to be explicit. I don't mind - it can be left unassigned.var fetchedVersion = 0;- I think the version should be 0 for nonexistent snapshots for consistency withDocand https://github.com/share/sharedb#data-model, especially this part of the docs: "ShareDB implicitly has a record for every document you can access.". I think it's also a good practice to stick to a single data type for each variable whenever possible. In the case ofversionI think it is possible and quite natural to haveversion === 0for nonexistent snapshots - the same asDoc.versionfor nonexistent docs.var fetchedTimestamp = timestamp;- similar tofetchedVersion- keep the data type the same (number). The user would know that the snapshot does not exist by checking thatversion === 0. Actually, it might be better to dovar fetchedTimestamp = 0;, in casetimestampis not specified. Additionally,timestampwould be meaningless for nonexistent snapshots anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is possible and quite natural to have version === 0 for nonexistent snapshots
This feels weird to me - like how you were saying with fetching a version higher than the current version. It's possible to call this method and get two snapshots claiming to be version === 0, but with different data. But if that's how we already do it with Doc, then fine.
(Same thing for snapshots.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I think I get it now... op version X in the database means that the op can be applied to a doc at version X. After the op is applied, the doc is at version X + 1. I think Backend.prototype._getSnapshot will need to be updated to reflect that, then we should both be happy. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Riiiight. So creation is v1?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initial create is applied to a doc at version 0, so after it's applied, the doc is v1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great. Pushed a change that matches this logic. I'm now much happier about v0 being a non-existent doc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, thanks @alecgibson 👍
lib/backend.js
Outdated
| for (var index = 0; index < ops.length; index++) { | ||
| var op = ops[index]; | ||
| fetchedTimestamp = op.m.ts; | ||
| fetchedVersion = op.v; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd move fetchedTimestamp and fetchedVersion assignments below if (timestampIsNumber && fetchedTimestamp > timestamp), so that there would be no need to access previousOp and re-assign fetchedTimestamp and fetchedVersion, if we reach the operation past the requested timestamp.
| Connection.prototype._handleSnapshot = function (error, message) { | ||
| var snapshotRequest = this.snapshotRequests[message.id]; | ||
| if (!snapshotRequest) return; | ||
| snapshotRequest._handleResponse(error, message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
delete this.snapshotRequests[message.id];is missing here - it'll prevent a memory leak.
lib/client/snapshot-request.js
Outdated
| version: message.version, | ||
| data: message.data, | ||
| timestamp: message.timestamp, | ||
| type: message.type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please resolve the type's URI to a full type object.
test/client/snapshot-request.js
Outdated
| backend.connect().getSnapshot('books', 'don-quixote'); | ||
| }); | ||
|
|
||
| it('errors if the version is -1', function (done) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
returns an empty snapshot when trying to fetch a snapshot before the document existed
I think this is great. 👍
For consistency, shouldn't we also return an empty snapshot, if asked for a version before the document existed (negative version)?
|
@gkubisa I've addressed your comments (apart from the variable declaration - discussed above) |
|
I've been thinking about requesting snapshots for future timestamps and versions past the latest version, and I'm not sure, if the current approach is right... Currently we'd just return the latest available snapshot, however, that snapshot could be different on subsequent requests as more operations are recorded. In my opinion this is at odds with the idea of immutable snapshots. Could we return an error instead? I would still keep the current behaviour (return the latest snapshot), if the |
|
I would even remove the date possibility, this create confusion, there should be an other api to request |
|
I fully agree on this, @dcharbonnier. I'd be happy to drop the |
This change is completely useless to me if we don't have timestamps. I'm happy to break this apart into two separate methods, like
I realise this is odd. It's certainly not idempotent. However, I would fully expect to be able to request a snapshot for the current time, and get a valid snapshot back (even if an op hasn't been recorded in some time). How about this behaviour?
|
That's what I thought. :-)
I'd be ok with two separate methods too but could the one which takes in
Omitting the version/timestamp or setting it to
Yes, that makes sense. 👍 One thing to keep in mind though is that the client and server time might differ, so that if the client's clock is ahead of the server's clock, the server might return an error, if the client requests the snapshot for its current time. |
|
@gkubisa I've split it into two methods, but kept the internals basically the same. It also errors when asking for a snapshot after the current time or version. "Current time" is determined by the server. |
|
Great, thanks @alecgibson ! From my side, the only remaining issue is https://github.com/share/sharedb/pull/220/files#r201677449. Hopefully this PR will be merged soon. 👍 |
lib/client/connection.js
Outdated
| }; | ||
|
|
||
| /** | ||
| * Get a read-only snapshot at a given version or time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove timestamp references from the getSnapshot docs.
lib/client/snapshot-request.js
Outdated
| this.sent = false; | ||
| } | ||
|
|
||
| SnapshotRequest.prototype.hasPending = function () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ready and hasPending are not needed because all requests in Connection.snapshotRequests are pending.
|
@gkubisa I've removed the |
gkubisa
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@alecgibson thanks for bearing with me and making all the changes! In my opinion this feature is complete but I'd like to give @nateps and @ericyhwang a chance to comment on it before merging.
In particular I'd like to confirm that it's ok to:
- have the
Connection.getSnapshotByTimemethod, as there might be a better way to achieve the same result - get a snapshot given a timestamp, - have
Connection.getSnapshotandConnection.getSnapshotByTimeaccept a callback parameter. An alternative would be to return aSnapshotsynchronously, makeSnapshotinherit fromEventEmitterand emitloadanderrorevents. I think that would be more in line withDocandQuery.
No worries. Sorry if I seem impatient at times! It's challenging juggling this with my job, and I'm just eager to get this feature in.
I'm all ears. Especially if there's a more efficient way of doing it!
In principle I'm very okay with anything that removes callbacks, but I have these reservations:
|
|
Sorry, I meant returning the |
|
Aha I see. Yes, that would be more in keeping with |
|
I would slightly prefer returning a |
test/client/snapshot-request.js
Outdated
| }); | ||
|
|
||
| describe('a document with some simple versions a day apart', function () { | ||
| var emptySnapshot = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to update the variable names for clarity:
- emptySnapshot -> v0
- v0 -> v1
- v1 -> v2
- v2 -> v3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Fixed.
lib/backend.js
Outdated
| Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback) { | ||
| version = (typeof version === 'number' && isFinite(version)) ? Math.max(0, version) : null; | ||
| var options = { metadata: true }; | ||
| var timestampIsNumber = typeof timestamp === 'number'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are we covered when we send NaN ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we would have been covered by the if block, because 1 > NaN === false, but I've added an explicit check, too.
This change makes some tweaks to the `getSnapshot` feature after code
review:
- pass `type` around as just the URI, and resolve to the full object
client-side
- small tweak to variable declaration logic in `Backend._getSnapshot`
- prevent memory leak of snapshot requests
- update response for `-1` version fetch
This change splits `getSnapshot` apart into: - `getSnapshot` fetches by version number - `getSnapshotAtTime` fetches by timestamp It also updates the logic to error if requesting a version after the current version, or a timestamp after `Date.now()`, since either of these requests would not be idempotent.
To stay consistent with the `Doc` class, this change updates the results of `getSnapshot` to always have `version: 0` and `timestamp: 0` if no snapshot was found (rather than having them `undefined`).
All `SnapshotRequest`s are pending, so we don't need these methods. Instead, to check for pending, we just check if a `SnapshotRequest` is queued.
The version attached to an operation in the database is the version before the operation has been applied to (or the version to which the operation can be applied). That means that when applying op v1, the document is v1 before the op has been applied, and then v2 after the op has been applied. This change updates the `getSnapshot` method to return snapshots with versions consistent with this (ie the snapshots are now 1-based instead of 0-based).
This change adds a very simple `Snapshot` class, which currently has no methods attached to it. Its primary use at the moment is to contain the response to a snapshot request, and for use in `MemoryDB` in place of `MemorySnapshot`. As part of this change, the structure of the snapshot request return is slightly modified to fit this class, including renaming of `version` to `v`, and moving of `timestamp` to `m.ts`.
This change makes a couple of review markups. The biggest is the removal of the ability to fetch a snapshot at a given time. It has been agreed that this would be a useful API, but we need to think about the ramifications of exposing this API before doing so (eg adding more indexes on database adapters for lookup by time, etc.). In order to move the core functionality forward - that is the ability to fetch a snapshot by version - this change removes the ability to fetch by time, and it is left (for now) to the consumer to look up the version they need themselves. Alongside this change, `getSnapshot` has also been renamed to `fetchSnapshot` to imply that we touch the database layer, and the `Snapshot` class has had its `collections` property removed to be consistent with database adapters, whilst also making `m` a property that is always present.
This change adds some review markups:
- use `nf` instead of `sf` for "snapshot fetch" message type, to not
confuse with "subscribe"
- remove the internal `_getOps` method that was added for fetching
metadata, which is no longer needed
- add a `util.isInteger` method
- rename `connection.snapshotRequests` to `_snapshotRequests` to
signify that it's internal and the API could change
- make sure that snapshot requests emit an event that is captured in
`connection.whenNothingPending`
- make snapshot requests use an incremental ID instead of a random
one to be consistent with `Query`
- make `SnapshotRequest` callbacks mandatory
- make `SnapshotRequest` return a string type instead of the full
object
|
@nateps @ericyhwang I've made the changes we discussed in the meeting yesterday (see commit message for details). |
|
Great work guys, this looks extremely useful! 😊Any idea on when this may be merged? |
lib/backend.js
Outdated
| type: snapshot.type | ||
| }; | ||
|
|
||
| backend.trigger(backend.MIDDLEWARE_ACTIONS.readSnapshots, agent, request, function (error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should call:
var projection = ....;
backend._sanitizeSnapshots(agent, projection, collection, snapshots, cb)
lib/backend.js
Outdated
| return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' }); | ||
| } | ||
|
|
||
| callback(null, { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the Snapshot constructor here
| }; | ||
|
|
||
| Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) { | ||
| var backend = this; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add timing test around whole function. var start = Date.now() here and emit timing event in callback from sanitizeSnapshot() below, similar to backend.fetch()
lib/backend.js
Outdated
| }; | ||
|
|
||
| Backend.prototype._fetchSnapshot = function (agent, index, id, version, callback) { | ||
| this.getOps(agent, index, id, 0, version, function (error, ops) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
call a this._getOps() method that does not call _sanitizeOps internally, thus not projecting nor emitting an op middleware action
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NB: I'm directly using backend.db.getOps, because backend._getOps would have basically had the same signature and just been a wrapper.
lib/client/connection.js
Outdated
| this._firstDoc(hasPending) || | ||
| this._firstQuery(hasPending) | ||
| this._firstQuery(hasPending) || | ||
| this._firstSnapshotRequest(exists) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's remove the exists method, since we always call with this and it can simplify the _fristSnapshotRequest method below, which is an internal method
lib/client/connection.js
Outdated
| return this._handleBulkMessage(message, '_handleUnsubscribe'); | ||
|
|
||
| case 'nf': | ||
| return this._handleSnapshot(err, message); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's call this _handleSnapshotFetch consistent with the message
| }; | ||
|
|
||
| SnapshotRequest.prototype._handleResponse = function (error, message) { | ||
| if (error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's emit ready here first before error checking, same as query so that we make sure we eventually call back to whenNothingPending on the connection in all cases
lib/client/snapshot-request.js
Outdated
| this.connection = connection; | ||
| this.id = id; | ||
| this.collection = collection; | ||
| this.version = util.isInteger(version) ? Math.max(0, version) : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's throw an error for negative versions rather than silently fixing it
test/db.js
Outdated
| {type: 'json0', id: '1', v: 1, data: {foo: 2, bar: 1}}, | ||
| {type: 'json0', id: '2', v: 1, data: {foo: 1, bar: 2}}, | ||
| {type: 'json0', id: '3', v: 1, data: {foo: 2, bar: 2}} | ||
| { type: 'json0', id: '0', v: 1, data: {foo: 1, bar: 1}, m: null }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's keep no spacing inside objects for consistency
This change: - bypasses `_sanitizeOps` and instead uses `_sanitizeSnapshot` - adds timing to `Backend.fetchSnapshot` - throws for negative versions
|
@ericyhwang / @nateps I've addressed the issues we spoke about in yesterday's meeting. Could you please re-review? |
ericyhwang
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nate asked me to take a look at the last round of changes and to take care of the rest if it looked good.
They LGTM, so I'm going to go ahead with merge+publish! 🎉
Thanks for the contribution and bearing with us through the process. With what we've learned, it should make future changes faster.
|
This has been published as [email protected] |
This change follows on from discussion in this issue. Its primary
aim is to allow clients to fetch an historical version of a document
(a snapshot), by providing either:
number, orDateThe entry-point for this feature is added to the
Connectionclass,deliberately separate from the
Doc, becauseDocis concerned with"live" document actions such as subscribing and submitting ops,
whereas fetching an historical version of a document should not be
associated with these ideas.
The feature is called with:
The details of the interface are detailed in the README, and in the
code documentation.
This change includes support for projections, and use of the
readSnapshotsmiddleware. It also hooks intoConnection'shasPendingmethod.Performance optimisations are deemed out-of-scope for this change (see
the issue for more details).
Note that this change also adds a development dependency on
lolexwhich is used for mocking the time.