-
Notifications
You must be signed in to change notification settings - Fork 10
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
lib: implement API versioning #231
Conversation
cd089f0
to
5b3795d
Compare
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.
LGTM
lib/applications.js
Outdated
index.set(application.name, appVersions); | ||
} | ||
if (!application.version) { | ||
// no version means default 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.
I'd change default
to latest
, but it is not necessary.
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.
Sure.
It should be mentioned that this PR Fixes: #31. |
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 implementation differs from what we discussed it should be, using semver isn't possible at all.
lib/applications.js
Outdated
@@ -9,11 +9,13 @@ module.exports = apps; | |||
// your needs. | |||
// name - application name | |||
// api - application API |
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.
Although it is not related to this PR "application API" is a tautology, and this comment should provide more detailed information about the way API must be provided, instead of this one as it gives no relevant information.
lib/applications.js
Outdated
@@ -9,11 +9,13 @@ module.exports = apps; | |||
// your needs. | |||
// name - application name | |||
// api - application API | |||
// version - api version of 'name' application |
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 put "application version" or "application API version" here, this way it'll be easier to read.
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.
Sure.
lib/applications.js
Outdated
} | ||
if (!application.version) { | ||
// no version means default version | ||
if (appVersions.get('latest')) { |
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 should definitely use has
method here because you don't use the value.
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.
Oh, thanks.
lib/applications.js
Outdated
appVersions.set('latest', application); | ||
} | ||
} else { | ||
appVersions.set(application.version.toString(), application); |
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.
Can version be a non-string value?
I thought that we agreed on using the semver, which meant using strings as version values.
lib/applications.js
Outdated
|
||
// save latest version to fill missing latest versions later | ||
const latestApp = latestApps.get(application.name); | ||
if (!latestApp || application.version > latestApp.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.
Same here, it is incorrect to compare semver versions like that.
lib/connection.js
Outdated
let target; | ||
if (typeof app === 'string') { | ||
target = app; | ||
} else if (!app.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.
You should also add a possibility to provide app
as a string containing both name and version like that: appName@1.0.0
.
bc3473c
to
d99092e
Compare
@belochub as discussed I've added support for semver in application versions. Also I've updated tests to check that functionality. Btw I've changed that comment message (api parameter description) even though this PR doesn't directly modify API but it is related to the API, so I think it's okay to put those changes here. |
lib/server.js
Outdated
@@ -24,6 +26,15 @@ const initServer = function( | |||
applications = apps.createAppsIndex(applications); | |||
} | |||
|
|||
// versions cached for efficient search when provided version is a range | |||
this._cachedVersions = {}; |
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 my opinion, it is better to use Object.create(null)
or even Map
here.
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.
Oh, I again forgot about map, thanks.
lib/server.js
Outdated
_getApplication(name, version) { | ||
const appVersions = this.applications.get(name); | ||
if (!appVersions) return null; | ||
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.
You can invert the condition here to avoid a lot of indentation below.
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.
LGTM
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.
LGTM
lib/connection.js
Outdated
) | ||
}; | ||
const packet = {}; | ||
if (Array.isArray(target)) { |
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 like the idea of making this function polymorphic. It is a hot function, and passing arguments of different types will mess up with V8's ICs.
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.
@aqrln I guess I'll split this into 2 functions with one accepting an array and the other value, what do you think?
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, that would work if we needed it at all. See my other 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.
Why not always expect an array here though?
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.
@aqrln, to avoid creation of an array with one element in some cases, it should probably be better for performance.
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 I'd add that in most cases it is a single argument, as handshake is the only place with array and it's performed only once per 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.
@aqrln also what about this one?
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.
@lundibundi based on the conversation above, I though there was consensus about it. I have explicitly approved splitting this into two functions above, and neither me nor anyone else has objected to your last comment, so feel free to pick either approach.
test/node/api-versions.js
Outdated
} | ||
}; | ||
|
||
const interfacesVLatest = { |
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.
Wait... do I get it right that latest
is a version by itself and not just a pointer to the latest semver version (2.0.0
in this case)? That sounds wrong to me. latest
and the like are tags, they must not be independent versions.
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.
No, It was made to check specific behavior (when app has no version it's latest version). If every app has a version then the biggest version will be the latest, see https://github.com/metarhia/jstp/pull/231/files/66c24103eef081bdb56c42696a400182502634f5#diff-13dd866a30aedfb1fab7c3d9da9446ceR162.
@aqrln, ping. |
lib/connection.js
Outdated
@@ -112,15 +113,26 @@ class Connection extends EventEmitter { | |||
} | |||
|
|||
// Send a handshake packet over the connection | |||
// appName - name of an application to connect to | |||
// app - string or object, application to connect to as 'name' or | |||
// 'name@version' or { name, 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.
Do we really need all the three of these? No strong opinion on it, but I'd rather make name@version
the only format.
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.
Same here, I don't really care. @metarhia/jstp-core somebody wants to leave the third option here?
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, why not?
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.
No more comments means 1 for, 0 against, hence we left it as is.
lib/connection.js
Outdated
if (typeof app === 'string') { | ||
const [name, version] = app.split('@'); | ||
if (semver.valid(version)) target = [name, version]; | ||
else target = name; |
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 throw an error here instead of silently discarding an invalid 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.
@aqrln Yeah, that will be better.
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.
@lundibundi this is still unaddressed.
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.
@aqrln as you can see in the comments below I'm waiting to get your approval to make all changes at once as I don't want to perform rebase multiple times for naught.
lib/connection.js
Outdated
} else if (!app.version) { | ||
target = app.name; | ||
} else { | ||
target = [app.name, app.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.
Can you please stick to either a string (as in { handshake: [0, 'app@latest'] }
) or an array (as in { handshake: [0, 'app', 'latest'] }
) in both cases? As the parsed messages are plain objects, the current approach may suffer from the constantly changing object shapes, which will lead to either polymorphic IC in _processHandshake()
in the best-case scenario or to deoptimizing it in the worst-case one.
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.
@aqrln, as far as I understand, first option you provided isn't possible, because it is always an array, check out _createPacket
function. Sometimes version can just be omitted in the array, meaning that latest version should be used, and packet will look like this:
{ handshake: [0, 'app'] }
.
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.
Ah, I see. Right.
lib/server.js
Outdated
applications.forEach((appVersions, appName) => { | ||
const versions = Array.from(appVersions) | ||
.filter(app => app[0] !== 'latest') | ||
.map(version => [new semver.SemVer(version[0]), version[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.
Can you please use destructuring in the left-hand side of the lambda to give some descriptive names for these values (i.e., ([something, somethingElse]) => [new semver.SemVer(something), somethingElse]
)?
Aside from that, why do app
and version
identifiers refer to the same objects in this and previous lines?
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.
@aqrln, there's comment on line 111 in this file.
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 doesn't help much. The file is read top to bottom, and these variable names are somewhat misleading. Both app
and version
at lines 33 and 34 are actually arrays of [version, app]
, AFAIU.
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, but here it is obvious, because of line 32: this is an array created from the map with versions as keys, and applications as values, there is no need to make a destruction assignment here.
However, I do agree that arguments in lambdas should be renamed.
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.
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.
This is still unaddressed.
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.
@aqrln but there is no approval of yours to my proposed changes?
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.
@lundibundi I would still prefer ([version, app]) => ...
, but if you and @belochub are against it, then at least making the names consistent works for me too.
lib/server.js
Outdated
if (range.test(version[0])) return version[1]; | ||
} | ||
} catch (error) { | ||
// TODO (lundibundi): after loggers are implemented write using them |
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.
Should it return a new error code instead of ERR_APP_NOT_FOUND
in this case? 🤔
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.
@aqrln technically it's still missing application 😃, but it may indeed be clearer to actually add another 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.
@aqrln there is no response here as well.
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.
@lundibundi there haven't been any changes requested as well ;)
I'm fine with ERR_APP_NOT_FOUND
.
@lundibundi, can you address the comments and rebase this on master, please? |
@belochub I've already answered the comments and haven't seen any response since then, that's why I'm waiting. Also I didn't rebase it as I want to address the comments too at the same time. |
f018bef
to
56667aa
Compare
lib/connection.js
Outdated
@@ -364,7 +417,9 @@ class Connection extends EventEmitter { | |||
} | |||
|
|||
const applicationName = packet.handshake[1]; | |||
const application = this.server.applications[applicationName]; | |||
const applicationVersion = packet.handshake[2]; | |||
const application = |
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 am not sure about the style of this, didn't we change styling for such cases?
connectAndInspect(appName, client, interfaces, ...options), | ||
connect: (app, client, ...options) => | ||
connect(app, client, ...options), | ||
connectAndInspect: (app, client, interfaces, ...options) => |
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 should also rename this argument in lib/cli/command-processor.js
.
lib/applications.js
Outdated
constructor(name, api) { | ||
this.name = name; | ||
constructor(name, api, version) { | ||
[this.name, this.version] = name.split('@'); |
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.
Should it split from the right instead of from the left?
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 mean, @metarhia/application@1.0
is a valid package name, so I'd expect it to be parsed as an application name correctly too.
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.
@aqrln, in such cases you can provide an object with fields name
and version
set to corresponding values.
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.
@belochub I don't find it a valid justification. Why provide two ways of doing the same thing with one of them being subtly broken? Either the npm-ish format must work according to users' expectations, or the { name, version }
schema must be the only available contract.
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.
@aqrln and in the above example will the user have application name as @metarhia/application
? As we don't support scopes (they mean nothing in our implementation) is there any reason to support that? But anyway it's easy to implement so it's okay with 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.
@aqrln, by the way, are you sure that @metarhia/application@1.0
is a valid package name?
Trying to create the package with such name using npm
leads to the error:
Sorry, name can only contain URL-friendly characters.
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.
Did you try to make @1.0
a part of the package name or...?
lib/connection.js
Outdated
handshake(app, login, password, callback) { | ||
let name, version; | ||
if (typeof app === 'string') { | ||
[name, version] = app.split('@'); |
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.
Ditto.
c29d0cc
to
c11816d
Compare
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.
LGTM.
// | ||
class Application { | ||
constructor(name, api) { | ||
this.name = name; | ||
constructor(name, api, 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.
I'd prefer to swap parameters api
and version
but it's up to you.
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.
Even though I agree that it will look better first of all it will not be compatible and second in case version is specified in the name
you will have to pass null
to the version parameter, because api will come after it.
test/node/api-versions.js
Outdated
|
||
const port = server.address().port; | ||
|
||
// latest |
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 prefer this to be a subtest.
test/node/api-versions.js
Outdated
}); | ||
}); | ||
|
||
// 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.
Ditto.
test/node/api-versions.js
Outdated
}); | ||
}); | ||
|
||
// v2 |
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.
Ditto.
test/node/api-versions.js
Outdated
|
||
const port = server.address().port; | ||
|
||
// ^1.0.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.
Ditto.
test/node/api-versions.js
Outdated
}); | ||
}); | ||
|
||
// > v1.0.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.
Ditto.
* allow to specify API version during handshake * make createAppsIndex handle multiple API versions * add API version parameter to Application * add tests for interface versioning
40db3bb
to
e2e21ac
Compare
* Allow to specify API version during handshake. * Make createAppsIndex handle multiple API versions. * Add API version parameter to Application. * Add tests for interface versioning. Fixes: #31 PR-URL: #231 Reviewed-By: Dmytro Nechai <nechaido@gmail.com> Reviewed-By: Alexey Orlenko <eaglexrlnk@gmail.com> Reviewed-By: Mykola Bilochub <nbelochub@gmail.com>
Landed in 9116125. |
* Allow to specify API version during handshake. * Make createAppsIndex handle multiple API versions. * Add API version parameter to Application. * Add tests for interface versioning. Fixes: #31 PR-URL: #231 Reviewed-By: Dmytro Nechai <nechaido@gmail.com> Reviewed-By: Alexey Orlenko <eaglexrlnk@gmail.com> Reviewed-By: Mykola Bilochub <nbelochub@gmail.com>
* Allow to specify API version during handshake. * Make createAppsIndex handle multiple API versions. * Add API version parameter to Application. * Add tests for interface versioning. Fixes: #31 PR-URL: #231 Reviewed-By: Dmytro Nechai <nechaido@gmail.com> Reviewed-By: Alexey Orlenko <eaglexrlnk@gmail.com> Reviewed-By: Mykola Bilochub <nbelochub@gmail.com>
This is a new and shiny first major release for `metarhia-jstp`. Changes include API refactoring and improvements, implementations of CLI, sessions, and application versions, native addon build optimizations, lots of bug fixes, test coverage increase, and other, less notable changes. This release also denotes the bump of the protocol version to v1.0. The only difference from the previous version of the protocol is that "old" heartbeat messages (`{}`) are now deprecated and `ping`/`pong` messages must be used for this purpose instead. Notable changes: * **src,build:** improve the native module subsystem *(Alexey Orlenko)* [#36](#36) **\[semver-minor\]** * **build:** compile in ISO C++11 mode *(Alexey Orlenko)* [#37](#37) **\[semver-minor\]** * **build:** improve error handling *(Alexey Orlenko)* [#40](#40) **\[semver-minor\]** * **lib:** refactor record-serialization.js *(Alexey Orlenko)* [#41](#41) * **parser:** fix a possible memory leak *(Alexey Orlenko)* [#44](#44) **\[semver-minor\]** * **protocol:** change the format of handshake packets *(Alexey Orlenko)* [#54](#54) **\[semver-major\]** * **parser:** make parser single-pass *(Mykola Bilochub)* [#61](#61) * **parser:** remove special case for '\0' literal *(Mykola Bilochub)* [#68](#68) **\[semver-major\]** * **parser:** fix bug causing node to crash *(Mykola Bilochub)* [#75](#75) * **client:** drop redundant callback argument *(Alexey Orlenko)* [#104](#104) **\[semver-major\]** * **client:** handle errors in connectAndInspect *(Alexey Orlenko)* [#105](#105) **\[semver-major\]** * **socket,ws:** use socket.destroy() properly *(Alexey Orlenko)* [#84](#84) **\[semver-major\]** * **cli:** add basic implementation *(Mykola Bilochub)* [#107](#107) **\[semver-minor\]** * **connection:** fix error handling in optional cbs *(Alexey Orlenko)* [#147](#147) **\[semver-major\]** * **test:** add JSON5 specs test suite *(Alexey Orlenko)* [#158](#158) * **lib:** change event signature *(Denys Otrishko)* [#187](#187) **\[semver-major\]** * **lib:** add address method to Server *(Denys Otrishko)* [#190](#190) **\[semver-minor\]** * **parser:** implement NaN and Infinity parsing *(Mykola Bilochub)* [#201](#201) * **parser:** improve string parsing performance *(Mykola Bilochub)* [#220](#220) * **lib:** optimize connection events *(Denys Otrishko)* [#222](#222) **\[semver-major\]** * **lib:** refactor server and client API *(Denys Otrishko)* [#209](#209) **\[semver-major\]** * **lib,src:** rename term packet usages to message *(Denys Otrishko)* [#270](#270) **\[semver-major\]** * **lib:** emit events about connection messages *(Denys Otrishko)* [#252](#252) **\[semver-minor\]** * **lib:** implement API versioning *(Denys Otrishko)* [#231](#231) **\[semver-minor\]** * **lib:** allow to set event handlers in application *(Denys Otrishko)* [#286](#286) **\[semver-minor\]** * **lib:** allow to broadcast events from server *(Denys Otrishko)* [#287](#287) **\[semver-minor\]** * **connection:** make callback method private *(Alexey Orlenko)* [#306](#306) **\[semver-major\]** * **lib:** implement sessions *(Mykola Bilochub)* [#289](#289) **\[semver-major\]** * **connection:** use ping-pong instead of heartbeat *(Dmytro Nechai)* [#303](#303) **\[semver-major\]**
* Allow to specify API version during handshake. * Make createAppsIndex handle multiple API versions. * Add API version parameter to Application. * Add tests for interface versioning. Fixes: metarhia/jstp#31 PR-URL: metarhia/jstp#231 Reviewed-By: Dmytro Nechai <nechaido@gmail.com> Reviewed-By: Alexey Orlenko <eaglexrlnk@gmail.com> Reviewed-By: Mykola Bilochub <nbelochub@gmail.com>
Fixes: #31.