-
Notifications
You must be signed in to change notification settings - Fork 450
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
Add support for fetching a particular version of a snapshot #220
Changes from 27 commits
f691f48
d29b26d
b7b3abe
0f22355
cf9bd25
7cfc134
c94ae65
c9141b0
5edd003
1c934a1
d25ee6e
dc2d022
4a90170
9640d73
fff922c
b030e27
ce64e8a
dd83d07
c855933
afb7784
16d6eaa
12e1bcc
b57273d
1ae5680
0f3d526
a39c141
b4764ac
e80fe46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,9 @@ | |
# Emacs | ||
\#*\# | ||
|
||
# VS Code | ||
.vscode/ | ||
|
||
# Logs | ||
logs | ||
*.log | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,8 @@ var projections = require('./projections'); | |
var QueryEmitter = require('./query-emitter'); | ||
var StreamSocket = require('./stream-socket'); | ||
var SubmitRequest = require('./submit-request'); | ||
var types = require('./types'); | ||
|
||
var warnDeprecatedDoc = true; | ||
var warnDeprecatedAfterSubmit = true; | ||
|
||
|
@@ -580,6 +582,68 @@ Backend.prototype.getChannels = function(collection, id) { | |
]; | ||
}; | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Add timing test around whole function. |
||
this._fetchSnapshot(agent, index, id, version, function (error, snapshot) { | ||
if (error) return callback(error); | ||
|
||
var request = { | ||
collection: index, | ||
id: id, | ||
v: snapshot.v, | ||
snapshots: snapshot.data ? [snapshot.data] : [], | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. We should call:
|
||
if (error) return callback(error); | ||
callback(null, { | ||
data: request.snapshots[0], | ||
v: request.v, | ||
type: request.type | ||
}); | ||
}); | ||
}); | ||
}; | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. call a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NB: I'm directly using |
||
if (error) return callback(error); | ||
|
||
var type = null; | ||
var snapshot; | ||
var fetchedVersion = 0; | ||
|
||
for (var index = 0; index < ops.length; index++) { | ||
var op = ops[index]; | ||
fetchedVersion = op.v + 1; | ||
|
||
if (op.create) { | ||
type = types.map[op.create.type]; | ||
if (!type) return callback({ code: 4008, message: 'Unknown type' }); | ||
snapshot = type.create(op.create.data); | ||
} else if (op.del) { | ||
snapshot = undefined; | ||
type = null; | ||
} else { | ||
snapshot = type.apply(snapshot, op.op); | ||
} | ||
} | ||
|
||
type = type ? type.uri : null; | ||
|
||
if (version > fetchedVersion) { | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Use the Snapshot constructor here |
||
data: snapshot, | ||
v: fetchedVersion, | ||
type: type | ||
}); | ||
}); | ||
}; | ||
|
||
function pluckIds(snapshots) { | ||
var ids = []; | ||
for (var i = 0; i < snapshots.length; i++) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
var Doc = require('./doc'); | ||
var Query = require('./query'); | ||
var SnapshotRequest = require('./snapshot-request'); | ||
var emitter = require('../emitter'); | ||
var ShareDBError = require('../error'); | ||
var types = require('../types'); | ||
|
@@ -33,13 +34,17 @@ function Connection(socket) { | |
// (created documents MUST BE UNIQUE) | ||
this.collections = {}; | ||
|
||
// Each query is created with an id that the server uses when it sends us | ||
// info about the query (updates, etc) | ||
// Each query and snapshot request is created with an id that the server uses when it sends us | ||
// info about the request (updates, etc) | ||
this.nextQueryId = 1; | ||
this.nextSnapshotRequestId = 1; | ||
|
||
// Map from query ID -> query object. | ||
this.queries = {}; | ||
|
||
// Map from snapshot request ID -> snapshot request | ||
this._snapshotRequests = {}; | ||
|
||
// A unique message number for the given id | ||
this.seq = 1; | ||
|
||
|
@@ -226,6 +231,9 @@ Connection.prototype.handleMessage = function(message) { | |
case 'bu': | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. let's call this |
||
|
||
case 'f': | ||
var doc = this.getExisting(message.c, message.d); | ||
if (doc) doc._handleFetch(err, message.data); | ||
|
@@ -310,6 +318,11 @@ Connection.prototype._setState = function(newState, reason) { | |
docs[id]._onConnectionStateChanged(); | ||
} | ||
} | ||
// Emit the event to all snapshots | ||
for (var id in this._snapshotRequests) { | ||
var snapshotRequest = this._snapshotRequests[id]; | ||
snapshotRequest._onConnectionStateChanged(); | ||
} | ||
this.endBulk(); | ||
|
||
this.emit(newState, reason); | ||
|
@@ -523,12 +536,16 @@ Connection.prototype.createSubscribeQuery = function(collection, q, options, cal | |
Connection.prototype.hasPending = function() { | ||
return !!( | ||
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 commentThe 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 |
||
); | ||
}; | ||
function hasPending(object) { | ||
return object.hasPending(); | ||
} | ||
function exists(object) { | ||
return !!object; | ||
} | ||
|
||
Connection.prototype.hasWritePending = function() { | ||
return !!this._firstDoc(hasWritePending); | ||
|
@@ -552,6 +569,11 @@ Connection.prototype.whenNothingPending = function(callback) { | |
query.once('ready', this._nothingPendingRetry(callback)); | ||
return; | ||
} | ||
var snapshotRequest = this._firstSnapshotRequest(exists); | ||
if (snapshotRequest) { | ||
snapshotRequest.once('ready', this._nothingPendingRetry(callback)); | ||
return; | ||
} | ||
// Call back when no pending operations | ||
process.nextTick(callback); | ||
}; | ||
|
@@ -584,3 +606,47 @@ Connection.prototype._firstQuery = function(fn) { | |
} | ||
} | ||
}; | ||
|
||
Connection.prototype._firstSnapshotRequest = function (fn) { | ||
for (var id in this._snapshotRequests) { | ||
var snapshotRequest = this._snapshotRequests[id]; | ||
if (fn(snapshotRequest)) { | ||
return snapshotRequest; | ||
} | ||
} | ||
}; | ||
|
||
/** | ||
* Fetch a read-only snapshot at a given version | ||
* | ||
* @param collection - the collection name of the snapshot | ||
* @param id - the ID of the snapshot | ||
* @param version (optional) - the version number to fetch | ||
* @param callback - (error, snapshot) => void, where snapshot takes the following schema: | ||
* | ||
* { | ||
* id: string; // ID of the snapshot | ||
* v: number; // version number of the snapshot | ||
* type: string; // the OT type of the snapshot, or null if it doesn't exist or is deleted | ||
* data: any; // the snapshot | ||
* } | ||
* | ||
*/ | ||
Connection.prototype.fetchSnapshot = function(collection, id, version, callback) { | ||
if (typeof version === 'function') { | ||
callback = version; | ||
version = null; | ||
} | ||
|
||
var requestId = this.nextSnapshotRequestId++; | ||
var snapshotRequest = new SnapshotRequest(this, requestId, collection, id, version, callback); | ||
this._snapshotRequests[snapshotRequest.requestId] = snapshotRequest; | ||
snapshotRequest.send(); | ||
}; | ||
|
||
Connection.prototype._handleSnapshot = function (error, message) { | ||
var snapshotRequest = this._snapshotRequests[message.id]; | ||
if (!snapshotRequest) return; | ||
delete this._snapshotRequests[message.id]; | ||
snapshotRequest._handleResponse(error, message); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
var Snapshot = require('../snapshot'); | ||
var util = require('../util'); | ||
var emitter = require('../emitter'); | ||
|
||
module.exports = SnapshotRequest; | ||
|
||
function SnapshotRequest(connection, requestId, collection, id, version, callback) { | ||
emitter.EventEmitter.call(this); | ||
|
||
if (typeof callback !== 'function') { | ||
throw new Error('Callback is required for SnapshotRequest'); | ||
} | ||
|
||
if (version != null && !util.isInteger(version)) { | ||
throw new Error('Snapshot version must be an integer'); | ||
} | ||
|
||
this.requestId = requestId; | ||
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 commentThe 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 |
||
this.callback = callback; | ||
|
||
this.sent = false; | ||
} | ||
emitter.mixin(SnapshotRequest); | ||
|
||
SnapshotRequest.prototype.send = function () { | ||
if (!this.connection.canSend) { | ||
return; | ||
} | ||
|
||
var message = { | ||
a: 'nf', | ||
id: this.requestId, | ||
c: this.collection, | ||
d: this.id, | ||
v: this.version, | ||
}; | ||
|
||
this.connection.send(message); | ||
this.sent = true; | ||
}; | ||
|
||
SnapshotRequest.prototype._onConnectionStateChanged = function () { | ||
if (this.connection.canSend && !this.sent) { | ||
this.send(); | ||
} else if (!this.connection.canSend) { | ||
this.sent = false; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It starts as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about the following situation:
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
}; | ||
|
||
SnapshotRequest.prototype._handleResponse = function (error, message) { | ||
if (error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's emit |
||
return this.callback(error); | ||
} | ||
|
||
var snapshot = new Snapshot(this.id, message.v, message.type, message.data, null); | ||
this.callback(null, snapshot); | ||
this.emit('ready'); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
module.exports = Snapshot; | ||
function Snapshot(id, version, type, data, meta) { | ||
this.id = id; | ||
this.v = version; | ||
this.type = type; | ||
this.data = data; | ||
this.m = meta; | ||
} |
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.