particle-api-js 9.4.1 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.circleci/config.yml +7 -5
- package/CHANGELOG.md +11 -0
- package/{test/EventStream-e2e-browser.html → EventStream-e2e-browser.html} +0 -1
- package/{test/EventStream-e2e-node.js → EventStream-e2e-node.js} +2 -3
- package/README.md +2 -2
- package/RELEASE.md +1 -1
- package/dist/particle.min.js +1 -399
- package/dist/particle.min.js.map +1 -1
- package/docs/api.md +5223 -115
- package/fs.js +2 -0
- package/karma.conf.js +18 -6
- package/package.json +23 -26
- package/src/Agent.js +407 -0
- package/src/Client.js +170 -0
- package/src/Defaults.js +7 -0
- package/src/EventStream.js +263 -0
- package/src/Library.js +33 -0
- package/src/Particle.js +2644 -0
- package/test/Agent.integration.js +5 -4
- package/test/Agent.spec.js +174 -291
- package/test/Client.spec.js +7 -7
- package/test/Defaults.spec.js +2 -2
- package/test/EventStream.spec.js +6 -4
- package/test/FakeAgent.js +2 -2
- package/test/Library.spec.js +2 -2
- package/test/Particle.integration.js +7 -7
- package/test/Particle.spec.js +332 -18
- package/test/fixtures/index.js +4 -18
- package/test/support/FixtureHttpServer.js +5 -3
- package/test/test-setup.js +5 -5
- package/tsconfig.json +14 -0
- package/webpack.config.js +45 -0
- package/.babelrc +0 -4
- package/lib/Agent.js +0 -516
- package/lib/Agent.js.map +0 -1
- package/lib/Client.js +0 -312
- package/lib/Client.js.map +0 -1
- package/lib/Defaults.js +0 -14
- package/lib/Defaults.js.map +0 -1
- package/lib/EventStream.js +0 -335
- package/lib/EventStream.js.map +0 -1
- package/lib/Library.js +0 -67
- package/lib/Library.js.map +0 -1
- package/lib/Particle.js +0 -3248
- package/lib/Particle.js.map +0 -1
- package/lib/superagent-binary-parser.js +0 -20
- package/lib/superagent-binary-parser.js.map +0 -1
- package/test/Client.integration.js +0 -69
- package/test/fixtures/tarball.tar.gz +0 -0
- package/test/fixtures/test-library-publish-0.0.1.tar.gz +0 -0
- package/test/fixtures/test-library-publish-0.0.2.tar.gz +0 -0
package/src/Client.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const Library = require('./Library');
|
|
2
|
+
let Particle;
|
|
3
|
+
|
|
4
|
+
class Client {
|
|
5
|
+
constructor({ auth, api = new Particle() }){
|
|
6
|
+
this.auth = auth;
|
|
7
|
+
this.api = api;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
ready(){
|
|
11
|
+
return Boolean(this.auth);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get firmware library objects
|
|
16
|
+
* @param {Object} query The query parameters for libraries. See Particle.listLibraries
|
|
17
|
+
* @returns {Promise} A promise
|
|
18
|
+
*/
|
|
19
|
+
libraries(query = {}){
|
|
20
|
+
return this.api.listLibraries(Object.assign({}, query, { auth: this.auth }))
|
|
21
|
+
.then(payload => {
|
|
22
|
+
const libraries = payload.body.data || [];
|
|
23
|
+
return libraries.map(l => new Library(this, l));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get one firmware library object
|
|
29
|
+
* @param {String} name Name of the library to fetch
|
|
30
|
+
* @param {Object} query The query parameters for libraries. See Particle.getLibrary
|
|
31
|
+
* @returns {Promise} A promise
|
|
32
|
+
*/
|
|
33
|
+
library(name, query = {}){
|
|
34
|
+
return this.api.getLibrary(Object.assign({}, query, { name, auth: this.auth }))
|
|
35
|
+
.then(payload => {
|
|
36
|
+
const library = payload.body.data || {};
|
|
37
|
+
return new Library(this, library);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get list of library versions
|
|
43
|
+
* @param {String} name Name of the library to fetch
|
|
44
|
+
* @param {Object} query The query parameters for versions. See Particle.getLibraryVersions
|
|
45
|
+
* @returns {Promise} A promise
|
|
46
|
+
*/
|
|
47
|
+
libraryVersions(name, query = {}){
|
|
48
|
+
return this.api.getLibraryVersions(Object.assign({}, query, { name, auth: this.auth }))
|
|
49
|
+
.then(payload => {
|
|
50
|
+
const libraries = payload.body.data || [];
|
|
51
|
+
return libraries.map(l => new Library(this, l));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Contribute a new library version
|
|
57
|
+
* @param {Buffer} archive The compressed archive with the library source
|
|
58
|
+
* @returns {Promise} A promise
|
|
59
|
+
*/
|
|
60
|
+
contributeLibrary(archive){
|
|
61
|
+
return this.api.contributeLibrary({ archive, auth: this.auth })
|
|
62
|
+
.then(payload => {
|
|
63
|
+
const library = payload.body.data || {};
|
|
64
|
+
return new Library(this, library);
|
|
65
|
+
}, error => {
|
|
66
|
+
this._throwError(error);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Make the the most recent private library version public
|
|
72
|
+
* @param {string} name The name of the library to publish
|
|
73
|
+
* @return {Promise} To publish the library
|
|
74
|
+
*/
|
|
75
|
+
publishLibrary(name){
|
|
76
|
+
return this.api.publishLibrary({ name, auth: this.auth })
|
|
77
|
+
.then(payload => {
|
|
78
|
+
const library = payload.body.data || {};
|
|
79
|
+
return new Library(this, library);
|
|
80
|
+
}, error => {
|
|
81
|
+
this._throwError(error);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delete an entire published library
|
|
87
|
+
* @param {object} params Specific params of the library to delete
|
|
88
|
+
* @param {string} params.name Name of the library to delete
|
|
89
|
+
* @param {string} params.force Key to force deleting a public library
|
|
90
|
+
* @returns {Promise} A promise
|
|
91
|
+
*/
|
|
92
|
+
deleteLibrary({ name, force }){
|
|
93
|
+
return this.api.deleteLibrary({ name, force, auth: this.auth })
|
|
94
|
+
.then(() => true, error => this._throwError(error));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_throwError(error){
|
|
98
|
+
if (error.body && error.body.errors){
|
|
99
|
+
const errorMessages = error.body.errors.map((e) => e.message).join('\n');
|
|
100
|
+
throw new Error(errorMessages);
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
downloadFile(uri){
|
|
106
|
+
return this.api.downloadFile({ uri });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {Object} files Object containing files to be compiled
|
|
111
|
+
* @param {Number} platformId Platform id number of the device you are compiling for
|
|
112
|
+
* @param {String} targetVersion System firmware version to compile against
|
|
113
|
+
* @returns {Promise} A promise
|
|
114
|
+
* @deprecated Will be removed in 6.5
|
|
115
|
+
*/
|
|
116
|
+
compileCode(files, platformId, targetVersion){
|
|
117
|
+
return this.api.compileCode({ files, platformId, targetVersion, auth: this.auth });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {object} params
|
|
122
|
+
* @param {string} params.deviceId Device ID or Name
|
|
123
|
+
* @param {boolean} params.signal Signal on or off
|
|
124
|
+
* @returns {Promise} A promise
|
|
125
|
+
* @deprecated Will be removed in 6.5
|
|
126
|
+
*/
|
|
127
|
+
signalDevice({ signal, deviceId }){
|
|
128
|
+
return this.api.signalDevice({ signal, deviceId, auth: this.auth });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @returns {Promise} A promise
|
|
133
|
+
* @deprecated Will be removed in 6.5
|
|
134
|
+
*/
|
|
135
|
+
listDevices(){
|
|
136
|
+
return this.api.listDevices({ auth: this.auth });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @returns {Promise} A promise
|
|
141
|
+
* @deprecated Will be removed in 6.5
|
|
142
|
+
*/
|
|
143
|
+
listBuildTargets(){
|
|
144
|
+
return this.api.listBuildTargets({ onlyFeatured: true, auth: this.auth })
|
|
145
|
+
.then(payload => {
|
|
146
|
+
let targets = [];
|
|
147
|
+
for (let target of payload.body.targets){
|
|
148
|
+
for (let platform of target.platforms){
|
|
149
|
+
targets.push({
|
|
150
|
+
version: target.version,
|
|
151
|
+
platform: platform,
|
|
152
|
+
prerelease: target.prereleases.indexOf(platform) > -1,
|
|
153
|
+
firmware_vendor: target.firmware_vendor
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return targets;
|
|
158
|
+
}, () => {});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
trackingIdentity({ full = false, context = undefined }={}){
|
|
162
|
+
return this.api.trackingIdentity({ full, context, auth: this.auth })
|
|
163
|
+
.then(payload => {
|
|
164
|
+
return payload.body;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = Client;
|
|
170
|
+
Particle = require('./Particle'); // Move it to after the export to avoid issue with circular reference
|
package/src/Defaults.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/* eslint max-depth: 0 */
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const url = require('url');
|
|
5
|
+
const { EventEmitter } = require('events');
|
|
6
|
+
|
|
7
|
+
class EventStream extends EventEmitter {
|
|
8
|
+
constructor(uri, token) {
|
|
9
|
+
super();
|
|
10
|
+
this.uri = uri;
|
|
11
|
+
this.token = token;
|
|
12
|
+
this.reconnectInterval = 2000;
|
|
13
|
+
this.timeout = 13000; // keep alive can be sent up to 12 seconds after last event
|
|
14
|
+
this.data = '';
|
|
15
|
+
this.buf = '';
|
|
16
|
+
|
|
17
|
+
this.parse = this.parse.bind(this);
|
|
18
|
+
this.end = this.end.bind(this);
|
|
19
|
+
this.idleTimeoutExpired = this.idleTimeoutExpired.bind(this);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
connect() {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const { hostname, protocol, port, path } = url.parse(this.uri);
|
|
25
|
+
this.origin = `${protocol}//${hostname}${port ? (':' + port) : ''}`;
|
|
26
|
+
|
|
27
|
+
const isSecure = protocol === 'https:';
|
|
28
|
+
const requestor = isSecure ? https : http;
|
|
29
|
+
const req = requestor.request({
|
|
30
|
+
hostname,
|
|
31
|
+
protocol,
|
|
32
|
+
path: `${path}?access_token=${this.token}`,
|
|
33
|
+
method: 'get',
|
|
34
|
+
port: parseInt(port, 10) || (isSecure ? 443 : 80),
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
mode: 'prefer-streaming'
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
this.req = req;
|
|
40
|
+
|
|
41
|
+
let connected = false;
|
|
42
|
+
let connectionTimeout = setTimeout(() => {
|
|
43
|
+
if (this.req) {
|
|
44
|
+
this.req.abort();
|
|
45
|
+
}
|
|
46
|
+
reject({ error: new Error('Timeout'), errorDescription: `Timeout connecting to ${this.uri}` });
|
|
47
|
+
}, this.timeout);
|
|
48
|
+
|
|
49
|
+
req.on('error', e => {
|
|
50
|
+
clearTimeout(connectionTimeout);
|
|
51
|
+
|
|
52
|
+
if (connected) {
|
|
53
|
+
this.end();
|
|
54
|
+
} else {
|
|
55
|
+
reject({ error: e, errorDescription: `Network error from ${this.uri}` });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
req.on('response', res => {
|
|
60
|
+
clearTimeout(connectionTimeout);
|
|
61
|
+
|
|
62
|
+
const statusCode = res.statusCode;
|
|
63
|
+
if (statusCode !== 200) {
|
|
64
|
+
let body = '';
|
|
65
|
+
res.on('data', chunk => body += chunk);
|
|
66
|
+
res.on('end', () => {
|
|
67
|
+
try {
|
|
68
|
+
body = JSON.parse(body);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
// don't bother doing anything special if the JSON.parse fails
|
|
71
|
+
// since we are already about to reject the promise anyway
|
|
72
|
+
} finally {
|
|
73
|
+
let errorDescription = `HTTP error ${statusCode} from ${this.uri}`;
|
|
74
|
+
// @ts-ignore
|
|
75
|
+
if (body && body.error_description) {
|
|
76
|
+
// @ts-ignore
|
|
77
|
+
errorDescription += ' - ' + body.error_description;
|
|
78
|
+
}
|
|
79
|
+
reject({ statusCode, errorDescription, body });
|
|
80
|
+
this.req = undefined;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.data = '';
|
|
87
|
+
this.buf = '';
|
|
88
|
+
|
|
89
|
+
connected = true;
|
|
90
|
+
res.on('data', this.parse);
|
|
91
|
+
res.once('end', this.end);
|
|
92
|
+
this.startIdleTimeout();
|
|
93
|
+
resolve(this);
|
|
94
|
+
});
|
|
95
|
+
req.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
abort() {
|
|
100
|
+
if (this.req) {
|
|
101
|
+
this.req.abort();
|
|
102
|
+
this.req = undefined;
|
|
103
|
+
}
|
|
104
|
+
this.removeAllListeners();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/* Private methods */
|
|
108
|
+
|
|
109
|
+
emitSafe(event, param) {
|
|
110
|
+
try {
|
|
111
|
+
this.emit(event, param);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (event !== 'error') {
|
|
114
|
+
this.emitSafe('error', error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
end() {
|
|
120
|
+
this.stopIdleTimeout();
|
|
121
|
+
|
|
122
|
+
if (!this.req) {
|
|
123
|
+
// request was ended intentionally by abort
|
|
124
|
+
// do not auto reconnect.
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this.req = undefined;
|
|
129
|
+
this.emitSafe('disconnect');
|
|
130
|
+
this.reconnect();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
reconnect() {
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (this.isOffline()) {
|
|
136
|
+
this.reconnect();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.emitSafe('reconnect');
|
|
141
|
+
this.connect().then(() => {
|
|
142
|
+
this.emitSafe('reconnect-success');
|
|
143
|
+
}).catch(err => {
|
|
144
|
+
this.emitSafe('reconnect-error', err);
|
|
145
|
+
this.reconnect();
|
|
146
|
+
});
|
|
147
|
+
}, this.reconnectInterval);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
isOffline() {
|
|
151
|
+
if (typeof navigator === 'undefined' || navigator.hasOwnProperty('onLine')) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return !navigator.onLine;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
startIdleTimeout() {
|
|
158
|
+
this.stopIdleTimeout();
|
|
159
|
+
this.idleTimeout = setTimeout(this.idleTimeoutExpired, this.timeout);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
stopIdleTimeout() {
|
|
163
|
+
if (this.idleTimeout) {
|
|
164
|
+
clearTimeout(this.idleTimeout);
|
|
165
|
+
this.idleTimeout = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
idleTimeoutExpired() {
|
|
170
|
+
if (this.req) {
|
|
171
|
+
this.req.abort();
|
|
172
|
+
this.end();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
parse(chunk) {
|
|
177
|
+
this.startIdleTimeout();
|
|
178
|
+
|
|
179
|
+
this.buf += chunk;
|
|
180
|
+
let pos = 0;
|
|
181
|
+
let length = this.buf.length;
|
|
182
|
+
let discardTrailingNewline = false;
|
|
183
|
+
|
|
184
|
+
while (pos < length) {
|
|
185
|
+
if (discardTrailingNewline) {
|
|
186
|
+
if (this.buf[pos] === '\n') {
|
|
187
|
+
++pos;
|
|
188
|
+
}
|
|
189
|
+
discardTrailingNewline = false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let lineLength = -1;
|
|
193
|
+
let fieldLength = -1;
|
|
194
|
+
|
|
195
|
+
for (let i = pos; lineLength < 0 && i < length; ++i) {
|
|
196
|
+
const c = this.buf[i];
|
|
197
|
+
if (c === ':') {
|
|
198
|
+
if (fieldLength < 0) {
|
|
199
|
+
fieldLength = i - pos;
|
|
200
|
+
}
|
|
201
|
+
} else if (c === '\r') {
|
|
202
|
+
discardTrailingNewline = true;
|
|
203
|
+
lineLength = i - pos;
|
|
204
|
+
} else if (c === '\n') {
|
|
205
|
+
lineLength = i - pos;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (lineLength < 0) {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.parseEventStreamLine(pos, fieldLength, lineLength);
|
|
214
|
+
|
|
215
|
+
pos += lineLength + 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (pos === length) {
|
|
219
|
+
this.buf = '';
|
|
220
|
+
} else if (pos > 0) {
|
|
221
|
+
this.buf = this.buf.slice(pos);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
parseEventStreamLine(pos, fieldLength, lineLength) {
|
|
226
|
+
if (lineLength === 0) {
|
|
227
|
+
try {
|
|
228
|
+
if (this.data.length > 0 && this.event) {
|
|
229
|
+
const event = JSON.parse(this.data);
|
|
230
|
+
event.name = this.eventName || '';
|
|
231
|
+
this.emitSafe('event', event);
|
|
232
|
+
}
|
|
233
|
+
} catch (e) {
|
|
234
|
+
// do nothing if JSON.parse fails
|
|
235
|
+
} finally {
|
|
236
|
+
this.data = '';
|
|
237
|
+
this.eventName = undefined;
|
|
238
|
+
this.event = false;
|
|
239
|
+
}
|
|
240
|
+
} else if (fieldLength > 0) {
|
|
241
|
+
const field = this.buf.slice(pos, pos + fieldLength);
|
|
242
|
+
let step = 0;
|
|
243
|
+
|
|
244
|
+
if (this.buf[pos + fieldLength + 1] !== ' ') {
|
|
245
|
+
step = fieldLength + 1;
|
|
246
|
+
} else {
|
|
247
|
+
step = fieldLength + 2;
|
|
248
|
+
}
|
|
249
|
+
pos += step;
|
|
250
|
+
const valueLength = lineLength - step;
|
|
251
|
+
const value = this.buf.slice(pos, pos + valueLength);
|
|
252
|
+
|
|
253
|
+
if (field === 'data') {
|
|
254
|
+
this.data += value + '\n';
|
|
255
|
+
} else if (field === 'event') {
|
|
256
|
+
this.eventName = value;
|
|
257
|
+
this.event = true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = EventStream;
|
package/src/Library.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Library
|
|
2
|
+
* Represents a version of a library contributed in the cloud.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class Library {
|
|
6
|
+
constructor(client, data) {
|
|
7
|
+
// Make client non-enumerable so it doesn't show up in Object.keys, JSON.stringify, etc
|
|
8
|
+
Object.defineProperty(this, 'client', { value: client });
|
|
9
|
+
this._assignAttributes(data);
|
|
10
|
+
this.downloadUrl = data.links && data.links.download;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_assignAttributes(data) {
|
|
14
|
+
Object.assign(this, data.attributes);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Download the compressed file containing the source code for this library version.
|
|
20
|
+
* @return {Promise} Resolves to the .tar.gz compressed source code
|
|
21
|
+
*/
|
|
22
|
+
download() {
|
|
23
|
+
if (!this.downloadUrl) {
|
|
24
|
+
return Promise.reject(new Error('No download URL for this library'));
|
|
25
|
+
}
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
return this.client.downloadFile(this.downloadUrl);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* TODO: add a versions() method to fetch an array of library objects */
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = Library;
|