pushi-js 0.5.1
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/package.json +52 -0
- package/pushi.js +1183 -0
- package/pushi.mjs +12 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pushi-js",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "JavaScript client for Hive Pushi push notification system",
|
|
5
|
+
"main": "pushi.js",
|
|
6
|
+
"browser": "pushi.js",
|
|
7
|
+
"module": "pushi.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./pushi.mjs",
|
|
11
|
+
"require": "./pushi.js",
|
|
12
|
+
"browser": "./pushi.js",
|
|
13
|
+
"default": "./pushi.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"pushi.js",
|
|
18
|
+
"pushi.mjs"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"lint": "eslint pushi.js",
|
|
22
|
+
"lint-fix": "eslint pushi.js --fix",
|
|
23
|
+
"test": "mocha --recursive test/"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"pushi",
|
|
27
|
+
"websocket",
|
|
28
|
+
"push",
|
|
29
|
+
"notifications",
|
|
30
|
+
"realtime",
|
|
31
|
+
"pubsub",
|
|
32
|
+
"pusher"
|
|
33
|
+
],
|
|
34
|
+
"author": "Hive Solutions <development@hive.pt>",
|
|
35
|
+
"license": "Apache-2.0",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/hivesolutions/pushi.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/hivesolutions/pushi#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/hivesolutions/pushi/issues"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"eslint": "^9.17.0",
|
|
46
|
+
"eslint-config-hive": "^0.7.0",
|
|
47
|
+
"mocha": "^10.8.2"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/pushi.js
ADDED
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
// Hive Pushi System
|
|
2
|
+
// Copyright (c) 2008-2024 Hive Solutions Lda.
|
|
3
|
+
//
|
|
4
|
+
// This file is part of Hive Pushi System.
|
|
5
|
+
//
|
|
6
|
+
// Hive Pushi System is free software: you can redistribute it and/or modify
|
|
7
|
+
// it under the terms of the Apache License as published by the Apache
|
|
8
|
+
// Foundation, either version 2.0 of the License, or (at your option) any
|
|
9
|
+
// later version.
|
|
10
|
+
//
|
|
11
|
+
// Hive Pushi System is distributed in the hope that it will be useful,
|
|
12
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
// Apache License for more details.
|
|
15
|
+
//
|
|
16
|
+
// You should have received a copy of the Apache License along with
|
|
17
|
+
// Hive Pushi System. If not, see <http://www.apache.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
// __author__ = João Magalhães <joamag@hive.pt>
|
|
20
|
+
// __copyright__ = Copyright (c) 2008-2024 Hive Solutions Lda.
|
|
21
|
+
// __license__ = Apache License, Version 2.0
|
|
22
|
+
|
|
23
|
+
var PUSHI_CONNECTIONS = {};
|
|
24
|
+
|
|
25
|
+
// ===========================================
|
|
26
|
+
// Observable Implementation
|
|
27
|
+
// ===========================================
|
|
28
|
+
|
|
29
|
+
var Observable = function() {
|
|
30
|
+
this.events = {};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
Observable.prototype.trigger = function(event) {
|
|
34
|
+
var index = 0;
|
|
35
|
+
var oneshots = null;
|
|
36
|
+
var methods = this.events[event] || [];
|
|
37
|
+
for (index = 0; index < methods.length; index++) {
|
|
38
|
+
var method = methods[index];
|
|
39
|
+
method.apply(this, arguments);
|
|
40
|
+
if (method.oneshot === false) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
oneshots = oneshots === null ? [] : oneshots;
|
|
44
|
+
oneshots.push(method);
|
|
45
|
+
}
|
|
46
|
+
if (oneshots === null) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
for (index = 0; index < oneshots.length; index++) {
|
|
50
|
+
var oneshot = oneshots[index];
|
|
51
|
+
this.unbind(event, oneshot);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
Observable.prototype.bind = function(event, method, oneshot) {
|
|
56
|
+
method.oneshot = Boolean(oneshot);
|
|
57
|
+
var methods = this.events[event] || [];
|
|
58
|
+
methods.push(method);
|
|
59
|
+
this.events[event] = methods;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
Observable.prototype.unbind = function(event, method) {
|
|
63
|
+
var methods = this.events[event] || [];
|
|
64
|
+
var index = methods.indexOf(method);
|
|
65
|
+
index !== -1 && methods.splice(index, 1);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ===========================================
|
|
69
|
+
// Pushi Channel Implementation
|
|
70
|
+
// ===========================================
|
|
71
|
+
|
|
72
|
+
var Channel = function(pushi, name) {
|
|
73
|
+
this.pushi = pushi;
|
|
74
|
+
this.name = name;
|
|
75
|
+
this.data = null;
|
|
76
|
+
this.subscribed = false;
|
|
77
|
+
this.events = {};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
Channel.prototype.setsubscribe = function(data) {
|
|
81
|
+
var alias = (data && data.alias) || [];
|
|
82
|
+
for (var index = 0; index < alias.length; index++) {
|
|
83
|
+
var name = alias[index];
|
|
84
|
+
|
|
85
|
+
var channel = new Channel(this.pushi, name);
|
|
86
|
+
this.pushi.channels[name] = channel;
|
|
87
|
+
|
|
88
|
+
this.pushi.onsubscribe(name, {});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.data = data;
|
|
92
|
+
this.subscribed = true;
|
|
93
|
+
this.trigger("subscribe", data);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
Channel.prototype.setunsubscribe = function(data) {
|
|
97
|
+
var alias = (data && data.alias) || [];
|
|
98
|
+
for (var index = 0; index < alias.length; index++) {
|
|
99
|
+
var name = alias[index];
|
|
100
|
+
this.pushi.onunsubscribe(name, {});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
this.subscribed = false;
|
|
104
|
+
this.trigger("unsubscribe", data);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
Channel.prototype.setlatest = function(data) {
|
|
108
|
+
this.trigger("latest", data);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
Channel.prototype.setmessage = function(event, data, mid, timestamp) {
|
|
112
|
+
this.trigger(event, data, mid, timestamp);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
Channel.prototype.send = function(event, data, echo, persist) {
|
|
116
|
+
this.pushi.sendChannel(event, data, this.name, echo, persist);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
Channel.prototype.unsubscribe = function(callback) {
|
|
120
|
+
this.pushi.unsubscribe(this.name, callback);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
Channel.prototype.latest = function(skip, count, callback) {
|
|
124
|
+
this.pushi.latest(this.name, skip, count, callback);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
Channel.prototype.trigger = Observable.prototype.trigger;
|
|
128
|
+
Channel.prototype.bind = Observable.prototype.bind;
|
|
129
|
+
Channel.prototype.unbind = Observable.prototype.unbind;
|
|
130
|
+
|
|
131
|
+
// ===========================================
|
|
132
|
+
// Pushi Base Implementation
|
|
133
|
+
// ===========================================
|
|
134
|
+
|
|
135
|
+
var Pushi = function(appKey, options) {
|
|
136
|
+
this.init(appKey, options);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
Pushi.prototype.init = function(appKey, options, callback) {
|
|
140
|
+
// tries to retrieve any previously existing instance
|
|
141
|
+
// of pushi for the provided key and in case it exists
|
|
142
|
+
// clones it and returns it as the properly initialized
|
|
143
|
+
// pushi instance (provides re-usage of resources)
|
|
144
|
+
var previous = PUSHI_CONNECTIONS[appKey];
|
|
145
|
+
if (previous) {
|
|
146
|
+
return this.clone(previous);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// runs the configuration operation for the current instance
|
|
150
|
+
// so that the state based configuration variables are set
|
|
151
|
+
// according to the provided (configuration) values
|
|
152
|
+
this.config(appKey, options);
|
|
153
|
+
|
|
154
|
+
// starts the various state related variables for
|
|
155
|
+
// the newly initialized pushi instance
|
|
156
|
+
this.socket = null;
|
|
157
|
+
this.socketId = null;
|
|
158
|
+
this.state = "disconnected";
|
|
159
|
+
this.channels = {};
|
|
160
|
+
this.events = {};
|
|
161
|
+
this.auths = {};
|
|
162
|
+
this._base = null;
|
|
163
|
+
this._cloned = false;
|
|
164
|
+
|
|
165
|
+
// updates the proper auth endpoint for the current
|
|
166
|
+
// instance so that the proper call is made if required
|
|
167
|
+
this.authEndpoint = this.options.authEndpoint;
|
|
168
|
+
|
|
169
|
+
// triggers the starts of the connection loading by calling
|
|
170
|
+
// the open (connection) method in the instance
|
|
171
|
+
this.open(callback);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
Pushi.prototype.config = function(appKey, options) {
|
|
175
|
+
// runs the definition of a series of constant values that
|
|
176
|
+
// will be used as defaults for some options
|
|
177
|
+
var TIMEOUT = 5000;
|
|
178
|
+
var BASE_URL = "wss://puxiapp.com/";
|
|
179
|
+
|
|
180
|
+
// retrieves the proper values for the options, defaulting
|
|
181
|
+
// to the pre-defined (constant) values if that's required
|
|
182
|
+
var timeout = options.timeout || TIMEOUT;
|
|
183
|
+
var baseUrl = options.baseUrl || BASE_URL;
|
|
184
|
+
var baseWebUrl = options.baseWebUrl || null;
|
|
185
|
+
var appId = options.appId || null;
|
|
186
|
+
var appSecret = options.appSecret || null;
|
|
187
|
+
|
|
188
|
+
// removes any previously registered configuration for the
|
|
189
|
+
// the current instance app key (for cases of re-configuration)
|
|
190
|
+
delete PUSHI_CONNECTIONS[this.appKey];
|
|
191
|
+
|
|
192
|
+
// updates the various configuration related variables
|
|
193
|
+
// that will condition the way the pushi instance behaves
|
|
194
|
+
this.timeout = timeout;
|
|
195
|
+
this.url = baseUrl + appKey;
|
|
196
|
+
this.baseUrl = baseUrl;
|
|
197
|
+
this.baseWebUrl = baseWebUrl;
|
|
198
|
+
this.appKey = appKey;
|
|
199
|
+
this.appId = appId;
|
|
200
|
+
this.appSecret = appSecret;
|
|
201
|
+
this.authenticated = false;
|
|
202
|
+
this.options = options || {};
|
|
203
|
+
|
|
204
|
+
// sets the current connection for the app key value
|
|
205
|
+
// so that it gets re-used if that's requested
|
|
206
|
+
PUSHI_CONNECTIONS[appKey] = this;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
Pushi.prototype.reconfig = function(appKey, options, callback) {
|
|
210
|
+
this.config(appKey, options);
|
|
211
|
+
this.reopen(callback);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
Pushi.prototype.clone = function(base) {
|
|
215
|
+
// copies the complete set of attributes from the base
|
|
216
|
+
// object to the new one (cloned) so that they may be
|
|
217
|
+
// re-used for any future operation/action
|
|
218
|
+
this.timeout = base.timeout;
|
|
219
|
+
this.url = base.url;
|
|
220
|
+
this.baseUrl = base.baseUrl;
|
|
221
|
+
this.baseWebUrl = base.baseWebUrl;
|
|
222
|
+
this.appKey = base.appKey;
|
|
223
|
+
this.options = base.options;
|
|
224
|
+
this.socket = base.socket;
|
|
225
|
+
this.socketId = base.socketId;
|
|
226
|
+
this.state = base.state;
|
|
227
|
+
this.channels = [];
|
|
228
|
+
this.events = [];
|
|
229
|
+
this.auths = base.auths;
|
|
230
|
+
this.authEndpoint = base.authEndpoint;
|
|
231
|
+
this._base = base;
|
|
232
|
+
this._cloned = true;
|
|
233
|
+
|
|
234
|
+
// adds the current reference to the list of subscriptions
|
|
235
|
+
// for the socket that is going to be used (as expected)
|
|
236
|
+
this.socket.subscriptions.push(this);
|
|
237
|
+
|
|
238
|
+
// in case the current state of the connection is
|
|
239
|
+
// connected must simulate the connection by calling
|
|
240
|
+
// the appropriate handler with the correct data
|
|
241
|
+
if (this.state === "connected") {
|
|
242
|
+
var data = {
|
|
243
|
+
socket_id: this.socketId
|
|
244
|
+
};
|
|
245
|
+
this.onoconnect(data);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
Pushi.prototype.open = function(callback) {
|
|
250
|
+
// in case the current state is not disconnected returns immediately
|
|
251
|
+
// as this is considered to be the only valid state for the operation
|
|
252
|
+
if (this.state !== "disconnected") {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// retrieves the current context as a local variable and then tries
|
|
257
|
+
// to gather the subscriptions from the current socket defaulting to
|
|
258
|
+
// a simple list with the current instance otherwise, this will make
|
|
259
|
+
// possible the re-usage of previously existing subscriptions for the
|
|
260
|
+
// instance for cloning situations (as defined in specifications)
|
|
261
|
+
var self = this;
|
|
262
|
+
var subscriptions = this.socket ? this.socket.subscriptions : [this];
|
|
263
|
+
|
|
264
|
+
// creates the new websocket reference with the currently defined
|
|
265
|
+
// url, then updates the reference to the underlying subscriptions
|
|
266
|
+
// and sets the proper callback value for the socket operation
|
|
267
|
+
var socket = new WebSocket(this.url);
|
|
268
|
+
socket.subscriptions = subscriptions;
|
|
269
|
+
socket._callback = callback;
|
|
270
|
+
|
|
271
|
+
// creates the function that will initialize the instance's socket to
|
|
272
|
+
// the one that has now been created and then calls it to all the
|
|
273
|
+
// subscriptions of the current socket (sets socket for subscription)
|
|
274
|
+
var _init = function() {
|
|
275
|
+
this.socket = socket;
|
|
276
|
+
};
|
|
277
|
+
this.callobj(_init, subscriptions);
|
|
278
|
+
|
|
279
|
+
this.socket.onopen = function() {
|
|
280
|
+
this._callback && this._callback();
|
|
281
|
+
this._callback = null;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
this.socket.onmessage = function(event) {
|
|
285
|
+
var data = null;
|
|
286
|
+
var message = event.data;
|
|
287
|
+
var json = JSON.parse(message);
|
|
288
|
+
|
|
289
|
+
var isConnected = self.state === "disconnected" && json.event === "pusher:connection_established";
|
|
290
|
+
|
|
291
|
+
if (isConnected) {
|
|
292
|
+
data = JSON.parse(json.data);
|
|
293
|
+
self.callobj(Pushi.prototype.onoconnect, this.subscriptions, data);
|
|
294
|
+
} else if (self.state === "connected") {
|
|
295
|
+
data = json;
|
|
296
|
+
self.callobj(Pushi.prototype.onmessage, this.subscriptions, data);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
this._callback && this._callback();
|
|
300
|
+
this._callback = null;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
this.socket.onclose = function() {
|
|
304
|
+
self.callobj(Pushi.prototype.onodisconnect, this.subscriptions);
|
|
305
|
+
this._callback && this._callback();
|
|
306
|
+
this._callback = null;
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
Pushi.prototype.close = function(callback) {
|
|
311
|
+
// in case the current state is not connected returns immediately
|
|
312
|
+
// as this is considered to be the only valid state for the operation
|
|
313
|
+
if (this.state !== "connected") {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// updates the next operation callback reference in the socket to the
|
|
318
|
+
// provided callback so that it gets notified on closing
|
|
319
|
+
this.socket._callback = callback;
|
|
320
|
+
|
|
321
|
+
// closes the currently assigned socket, triggering a series of events
|
|
322
|
+
// that will update the current pushi status as disconnected
|
|
323
|
+
this.socket.close();
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
Pushi.prototype.reopen = function(callback) {
|
|
327
|
+
var self = this;
|
|
328
|
+
this.close(function() {
|
|
329
|
+
self.open(callback);
|
|
330
|
+
});
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
Pushi.prototype.callobj = function(callable, objects) {
|
|
334
|
+
var index = 0;
|
|
335
|
+
var args = [];
|
|
336
|
+
|
|
337
|
+
for (index = 2; index < arguments.length; index++) {
|
|
338
|
+
args.push(arguments[index]);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
for (index = 0; index < objects.length; index++) {
|
|
342
|
+
var _object = objects[index];
|
|
343
|
+
callable.apply(_object, args);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
Pushi.prototype.retry = function() {
|
|
348
|
+
// sets the current object context in the self variable
|
|
349
|
+
// to be used by the clojures that are going to be created
|
|
350
|
+
var self = this;
|
|
351
|
+
|
|
352
|
+
// in case this is a cloned object the retry operation
|
|
353
|
+
// is not possible because this object does not owns
|
|
354
|
+
// the underlying websocket object
|
|
355
|
+
if (this._cloned) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// sets the timeout for the new initialization of the
|
|
360
|
+
// object this value should not be to low that congests
|
|
361
|
+
// the server side nor to large that takes to long for
|
|
362
|
+
// the reconnection to take effect (bad user experience)
|
|
363
|
+
setTimeout(function() {
|
|
364
|
+
self.open();
|
|
365
|
+
}, this.timeout);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
Pushi.prototype.onoconnect = function(data) {
|
|
369
|
+
this.socketId = data.socket_id;
|
|
370
|
+
this.state = "connected";
|
|
371
|
+
this.trigger("connect");
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
Pushi.prototype.onodisconnect = function(_data) {
|
|
375
|
+
this.socketId = null;
|
|
376
|
+
this.channels = {};
|
|
377
|
+
this.state = "disconnected";
|
|
378
|
+
this.trigger("disconnect");
|
|
379
|
+
this.retry();
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
Pushi.prototype.onsubscribe = function(channel, data) {
|
|
383
|
+
if (!this.channels[channel]) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
var _channel = this.channels[channel];
|
|
387
|
+
_channel.setsubscribe(data);
|
|
388
|
+
this.trigger("subscribe", channel, data);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
Pushi.prototype.onunsubscribe = function(channel, data) {
|
|
392
|
+
if (!this.channels[channel]) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
var _channel = this.channels[channel];
|
|
396
|
+
delete this.channels[channel];
|
|
397
|
+
_channel.setunsubscribe(data);
|
|
398
|
+
this.trigger("unsubscribe", channel, data);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
Pushi.prototype.onlatest = function(channel, data) {
|
|
402
|
+
if (!this.channels[channel]) {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
var _channel = this.channels[channel];
|
|
406
|
+
_channel.setlatest(data);
|
|
407
|
+
this.trigger("latest", channel, data);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
Pushi.prototype.onmemberadded = function(channel, member) {
|
|
411
|
+
this.trigger("member_added", channel, member);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
Pushi.prototype.onmemberremoved = function(channel, member) {
|
|
415
|
+
this.trigger("member_removed", channel, member);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
Pushi.prototype.onmessage = function(json) {
|
|
419
|
+
var data = null;
|
|
420
|
+
var member = null;
|
|
421
|
+
var channel = json.channel;
|
|
422
|
+
var _channel = this.channels[channel];
|
|
423
|
+
var isPeer = channel.startsWith("peer-");
|
|
424
|
+
if (channel && !_channel && !isPeer) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
switch (json.event) {
|
|
429
|
+
case "pusher_internal:subscription_succeeded":
|
|
430
|
+
data = JSON.parse(json.data);
|
|
431
|
+
this.onsubscribe(channel, data);
|
|
432
|
+
break;
|
|
433
|
+
|
|
434
|
+
case "pusher_internal:unsubscription_succeeded":
|
|
435
|
+
data = JSON.parse(json.data);
|
|
436
|
+
this.onunsubscribe(channel, data);
|
|
437
|
+
break;
|
|
438
|
+
|
|
439
|
+
case "pusher_internal:latest":
|
|
440
|
+
data = JSON.parse(json.data);
|
|
441
|
+
this.onlatest(channel, data);
|
|
442
|
+
break;
|
|
443
|
+
|
|
444
|
+
case "pusher:member_added":
|
|
445
|
+
member = JSON.parse(json.member);
|
|
446
|
+
this.onmemberadded(channel, member);
|
|
447
|
+
break;
|
|
448
|
+
|
|
449
|
+
case "pusher:member_removed":
|
|
450
|
+
member = JSON.parse(json.member);
|
|
451
|
+
this.onmemberremoved(channel, member);
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this.trigger(json.event, json.data, json.channel, json.mid, json.timestamp);
|
|
456
|
+
_channel && _channel.setmessage(json.event, json.data, json.mid, json.timestamp);
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
Pushi.prototype.send = function(json) {
|
|
460
|
+
var data = JSON.stringify(json);
|
|
461
|
+
this.socket.send(data);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
Pushi.prototype.sendEvent = function(event, data, echo, persist) {
|
|
465
|
+
echo = echo === undefined ? false : echo;
|
|
466
|
+
persist = persist === undefined ? true : persist;
|
|
467
|
+
var json = {
|
|
468
|
+
event: event,
|
|
469
|
+
data: data,
|
|
470
|
+
echo: echo,
|
|
471
|
+
persist: persist
|
|
472
|
+
};
|
|
473
|
+
this.send(json);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
Pushi.prototype.sendChannel = function(event, data, channel, echo, persist) {
|
|
477
|
+
echo = echo === undefined ? false : echo;
|
|
478
|
+
persist = persist === undefined ? true : persist;
|
|
479
|
+
var json = {
|
|
480
|
+
event: event,
|
|
481
|
+
data: data,
|
|
482
|
+
channel: channel,
|
|
483
|
+
echo: echo,
|
|
484
|
+
persist: persist
|
|
485
|
+
};
|
|
486
|
+
this.send(json);
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
Pushi.prototype.invalidate = function(channel) {
|
|
490
|
+
// in case the channel (name) value is provided
|
|
491
|
+
// removes its reference from the map associating
|
|
492
|
+
// the channel name with the channel info
|
|
493
|
+
if (channel) {
|
|
494
|
+
delete this.channels[channel];
|
|
495
|
+
}
|
|
496
|
+
// otherwise removes all the channel information from
|
|
497
|
+
// the channels map invalidating all of the elements
|
|
498
|
+
else {
|
|
499
|
+
this.channels = {};
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
Pushi.prototype.subscribe = function(channel, force, callback) {
|
|
504
|
+
// sets the current context in the self variable to
|
|
505
|
+
// be used latter for the clojure functions
|
|
506
|
+
var self = this;
|
|
507
|
+
|
|
508
|
+
// tries to retrieve the channel information for the
|
|
509
|
+
// provided channel name in case it's found returns
|
|
510
|
+
// the channel object immediately (avoids double
|
|
511
|
+
// registration of the channel)
|
|
512
|
+
var _channel = this.channels[channel];
|
|
513
|
+
if (_channel && !force) {
|
|
514
|
+
return _channel;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// in case this is a cloned proxy object we must also
|
|
518
|
+
// check if the base object is already subscribed for
|
|
519
|
+
// the channel for such cases the callback should be
|
|
520
|
+
// called immediately as there's no remote call to be
|
|
521
|
+
// performed for such situations
|
|
522
|
+
if (this._cloned) {
|
|
523
|
+
_channel = this._base.channels[channel];
|
|
524
|
+
if (_channel && !force) {
|
|
525
|
+
setTimeout(function() {
|
|
526
|
+
self.onsubscribe(channel, _channel.data);
|
|
527
|
+
});
|
|
528
|
+
this.channels[channel] = _channel;
|
|
529
|
+
return _channel;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// verifies if the current channel to be subscribed
|
|
534
|
+
// is of type private and in case it is uses the proper
|
|
535
|
+
// private way of subscription otherwise uses the public
|
|
536
|
+
// way for subscription (no authentication process)
|
|
537
|
+
var isPrivate = channel.startsWith("private-") || channel.startsWith("presence-") || channel.startsWith(
|
|
538
|
+
"personal-");
|
|
539
|
+
if (isPrivate) {
|
|
540
|
+
this.subscribePrivate(channel);
|
|
541
|
+
} else {
|
|
542
|
+
this.subscribePublic(channel);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// retrieves the channel as the name value and then creates
|
|
546
|
+
// a channel object with the current context and the name and
|
|
547
|
+
// then sets the channel in the channels map structure
|
|
548
|
+
var name = channel;
|
|
549
|
+
channel = new Channel(this, name);
|
|
550
|
+
this.channels[name] = channel;
|
|
551
|
+
|
|
552
|
+
// in case the callback function is defined registers for the
|
|
553
|
+
// subscribe event on the channel object
|
|
554
|
+
callback && channel.bind("subscribe", callback, true);
|
|
555
|
+
|
|
556
|
+
// returns the channel structure as a result of this function
|
|
557
|
+
// to be used by the caller method or function
|
|
558
|
+
return channel;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
Pushi.prototype.unsubscribe = function(channel, callback) {
|
|
562
|
+
// verifies if the channel is currently defined in the
|
|
563
|
+
// list of channels for the connection if not returns immediately
|
|
564
|
+
if (!this.channels[channel]) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// sends the event for the unsubscription of the channel through
|
|
569
|
+
// the current pushi socket so that no more messages are received
|
|
570
|
+
// regarding the provided channel
|
|
571
|
+
this.sendEvent("pusher:unsubscribe", {
|
|
572
|
+
channel: channel
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
// sets the channel as the name value and then tries to retrieve
|
|
576
|
+
// the channel structure for the provided name
|
|
577
|
+
var name = channel;
|
|
578
|
+
channel = this.channels[name];
|
|
579
|
+
|
|
580
|
+
// in case the callback function is defined registers for the
|
|
581
|
+
// unsubscribe event on the channel object
|
|
582
|
+
callback && channel.bind("unsubscribe", callback, true);
|
|
583
|
+
|
|
584
|
+
// returns the channel structure to the caller function so that
|
|
585
|
+
// may be used for any other operations pending
|
|
586
|
+
return channel;
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
Pushi.prototype.latest = function(channel, skip, count, callback) {
|
|
590
|
+
// sets the default values for the latest retrieval, so that if
|
|
591
|
+
// they are not provided values are ensured
|
|
592
|
+
skip = skip || 0;
|
|
593
|
+
count = count || 10;
|
|
594
|
+
|
|
595
|
+
// verifies if the channel is currently defined in the
|
|
596
|
+
// list of channels for the connection if not returns immediately
|
|
597
|
+
if (!this.channels[channel] && !channel.startsWith("peer-")) {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// sends the event for the latest (retrieval) of the channel through
|
|
602
|
+
// the current pushi socket so that the latest messages are retrieved
|
|
603
|
+
this.sendEvent("pusher:latest", {
|
|
604
|
+
channel: channel,
|
|
605
|
+
skip: skip,
|
|
606
|
+
count: count
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// sets the channel as the name value and then tries to retrieve
|
|
610
|
+
// the channel structure for the provided name, note that the ensure
|
|
611
|
+
// call will make sure that at least one channel object exists
|
|
612
|
+
var name = channel;
|
|
613
|
+
channel = this.ensureChannel(name);
|
|
614
|
+
|
|
615
|
+
// in case the callback function is defined registers for the
|
|
616
|
+
// latest event on the channel object
|
|
617
|
+
callback && channel.bind("latest", callback, true);
|
|
618
|
+
|
|
619
|
+
// returns the channel structure to the caller function so that
|
|
620
|
+
// may be used for any other operations pending
|
|
621
|
+
return channel;
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
Pushi.prototype.ensureChannel = function(name) {
|
|
625
|
+
if (this.channels[name]) {
|
|
626
|
+
return this.channels[name];
|
|
627
|
+
}
|
|
628
|
+
var channel = new Channel(this, name);
|
|
629
|
+
this.channels[name] = channel;
|
|
630
|
+
return channel;
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
Pushi.prototype.subscribePublic = function(channel) {
|
|
634
|
+
this.sendEvent("pusher:subscribe", {
|
|
635
|
+
channel: channel
|
|
636
|
+
});
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
Pushi.prototype.subscribePrivate = function(channel) {
|
|
640
|
+
// in case no authentication endpoint exists returns immediately
|
|
641
|
+
// because there's not enough information to proceed with the
|
|
642
|
+
// authentication process for the private channel
|
|
643
|
+
if (!this.authEndpoint) {
|
|
644
|
+
throw new Error("No auth endpoint defined");
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// sets the current context in the self variable to be
|
|
648
|
+
// used by the clojures in the current function
|
|
649
|
+
var self = this;
|
|
650
|
+
|
|
651
|
+
// constructs the get query part of the url with both the socket
|
|
652
|
+
// id of the current connection and the channel value for it
|
|
653
|
+
// then constructs the complete url value for the connection
|
|
654
|
+
var query = "?socket_id=" + this.socketId + "&channel=" + channel;
|
|
655
|
+
var url = this.authEndpoint + query;
|
|
656
|
+
|
|
657
|
+
// creates the remote async request that it's going
|
|
658
|
+
// to be used to retrieve the authentication information
|
|
659
|
+
// this is going to use the provided auth endpoint together
|
|
660
|
+
// with some of the current context
|
|
661
|
+
var request = new XMLHttpRequest();
|
|
662
|
+
request.open("get", url, true);
|
|
663
|
+
request.onreadystatechange = function() {
|
|
664
|
+
// in case the current state is not ready returns
|
|
665
|
+
// immediately as it's not a (to) success change
|
|
666
|
+
if (request.readyState !== 4) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// retrieves the response data and parses it as a json
|
|
671
|
+
// message and returns immediately in case no auth
|
|
672
|
+
// information is provided as part of the response
|
|
673
|
+
var result = JSON.parse(request.responseText);
|
|
674
|
+
if (!result.auth) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// sends a pusher subscribe event containing all of the
|
|
679
|
+
// channel information together with the auth token and
|
|
680
|
+
// the channel data to be used (in case it exists)
|
|
681
|
+
self.sendEvent("pusher:subscribe", {
|
|
682
|
+
channel: channel,
|
|
683
|
+
auth: result.auth,
|
|
684
|
+
channel_data: result.channel_data
|
|
685
|
+
});
|
|
686
|
+
};
|
|
687
|
+
request.send();
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
Pushi.prototype.isValid = function(appKey, baseUrl) {
|
|
691
|
+
return appKey === this.appKey && baseUrl === this.baseUrl;
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// ===========================================
|
|
695
|
+
// Observable Support
|
|
696
|
+
// ===========================================
|
|
697
|
+
|
|
698
|
+
Pushi.prototype.trigger = Observable.prototype.trigger;
|
|
699
|
+
Pushi.prototype.bind = Observable.prototype.bind;
|
|
700
|
+
Pushi.prototype.unbind = Observable.prototype.unbind;
|
|
701
|
+
|
|
702
|
+
// ===========================================
|
|
703
|
+
// Authentication Support
|
|
704
|
+
// ===========================================
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Logs in to the Pushi server using app credentials.
|
|
708
|
+
* This establishes a session that is required for Web Push API
|
|
709
|
+
* operations. Must be called before using any Web Push methods.
|
|
710
|
+
*
|
|
711
|
+
* @param {Object} options Configuration options:
|
|
712
|
+
* - appId: The app identifier (required if not provided in constructor)
|
|
713
|
+
* - appSecret: The app secret (required if not provided in constructor)
|
|
714
|
+
* @returns {Promise} Promise that resolves when logged in.
|
|
715
|
+
*/
|
|
716
|
+
Pushi.prototype.login = function(options) {
|
|
717
|
+
var self = this;
|
|
718
|
+
options = options || {};
|
|
719
|
+
|
|
720
|
+
// uses the provided values or falls back to the configured ones
|
|
721
|
+
var appId = options.appId || this.appId;
|
|
722
|
+
var appSecret = options.appSecret || this.appSecret;
|
|
723
|
+
|
|
724
|
+
if (!appId) {
|
|
725
|
+
return Promise.reject(new Error("App ID is required for authentication"));
|
|
726
|
+
}
|
|
727
|
+
if (!appSecret) {
|
|
728
|
+
return Promise.reject(new Error("App secret is required for authentication"));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// builds the login URL with authentication parameters
|
|
732
|
+
var url = this._buildApiUrl("/login");
|
|
733
|
+
url += "?app_id=" + encodeURIComponent(appId);
|
|
734
|
+
url += "&app_key=" + encodeURIComponent(this.appKey);
|
|
735
|
+
url += "&app_secret=" + encodeURIComponent(appSecret);
|
|
736
|
+
|
|
737
|
+
return new Promise(function(resolve, reject) {
|
|
738
|
+
var request = new XMLHttpRequest();
|
|
739
|
+
request.open("GET", url, true);
|
|
740
|
+
request.withCredentials = true;
|
|
741
|
+
request.onreadystatechange = function() {
|
|
742
|
+
if (request.readyState !== 4) {
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (request.status === 200) {
|
|
747
|
+
self.authenticated = true;
|
|
748
|
+
self.trigger("login");
|
|
749
|
+
resolve();
|
|
750
|
+
} else {
|
|
751
|
+
var error = new Error("Login failed: " + request.status);
|
|
752
|
+
reject(error);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
request.send();
|
|
756
|
+
});
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Logs out from the Pushi server, ending the current session.
|
|
761
|
+
*
|
|
762
|
+
* @returns {Promise} Promise that resolves when logged out.
|
|
763
|
+
*/
|
|
764
|
+
Pushi.prototype.logout = function() {
|
|
765
|
+
var self = this;
|
|
766
|
+
var url = this._buildApiUrl("/logout");
|
|
767
|
+
|
|
768
|
+
return new Promise(function(resolve, reject) {
|
|
769
|
+
var request = new XMLHttpRequest();
|
|
770
|
+
request.open("GET", url, true);
|
|
771
|
+
request.withCredentials = true;
|
|
772
|
+
request.onreadystatechange = function() {
|
|
773
|
+
if (request.readyState !== 4) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (request.status === 200) {
|
|
778
|
+
self.authenticated = false;
|
|
779
|
+
self.trigger("logout");
|
|
780
|
+
resolve();
|
|
781
|
+
} else {
|
|
782
|
+
var error = new Error("Logout failed: " + request.status);
|
|
783
|
+
reject(error);
|
|
784
|
+
}
|
|
785
|
+
};
|
|
786
|
+
request.send();
|
|
787
|
+
});
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// ===========================================
|
|
791
|
+
// Web Push API Support
|
|
792
|
+
// ===========================================
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Fetches the VAPID public key from the server. This key is
|
|
796
|
+
* needed to subscribe to push notifications using the browser's
|
|
797
|
+
* Push API (applicationServerKey parameter).
|
|
798
|
+
*
|
|
799
|
+
* @param {Function} callback Optional callback with (error, vapidPublicKey).
|
|
800
|
+
* @returns {Promise} Promise that resolves with the VAPID public key.
|
|
801
|
+
*/
|
|
802
|
+
Pushi.prototype.getVapidPublicKey = function(callback) {
|
|
803
|
+
var url = this._buildApiUrl("/vapid_key");
|
|
804
|
+
|
|
805
|
+
return new Promise(function(resolve, reject) {
|
|
806
|
+
var request = new XMLHttpRequest();
|
|
807
|
+
request.open("GET", url, true);
|
|
808
|
+
request.withCredentials = true;
|
|
809
|
+
request.onreadystatechange = function() {
|
|
810
|
+
if (request.readyState !== 4) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (request.status === 200) {
|
|
815
|
+
var result = JSON.parse(request.responseText);
|
|
816
|
+
var publicKey = result.vapid_public_key;
|
|
817
|
+
callback && callback(null, publicKey);
|
|
818
|
+
resolve(publicKey);
|
|
819
|
+
} else {
|
|
820
|
+
var error = new Error("Failed to get VAPID public key: " + request.status);
|
|
821
|
+
callback && callback(error, null);
|
|
822
|
+
reject(error);
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
request.send();
|
|
826
|
+
});
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Requests notification permission from the user. This must be
|
|
831
|
+
* called before attempting to subscribe to push notifications.
|
|
832
|
+
*
|
|
833
|
+
* @returns {Promise} Promise that resolves with permission state
|
|
834
|
+
* ('granted', 'denied', or 'default').
|
|
835
|
+
*/
|
|
836
|
+
Pushi.prototype.requestNotificationPermission = function() {
|
|
837
|
+
return new Promise(function(resolve, reject) {
|
|
838
|
+
if (!("Notification" in window)) {
|
|
839
|
+
reject(new Error("Notifications not supported in this browser"));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
Notification.requestPermission().then(function(permission) {
|
|
844
|
+
resolve(permission);
|
|
845
|
+
}).catch(function(error) {
|
|
846
|
+
reject(error);
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Registers a service worker for handling push notifications.
|
|
853
|
+
* The service worker is required to receive and display push
|
|
854
|
+
* notifications in the browser.
|
|
855
|
+
*
|
|
856
|
+
* @param {String} swPath Path to the service worker file (default: '/sw.js').
|
|
857
|
+
* @returns {Promise} Promise that resolves with the ServiceWorkerRegistration.
|
|
858
|
+
*/
|
|
859
|
+
Pushi.prototype.registerServiceWorker = function(swPath) {
|
|
860
|
+
swPath = swPath || "/sw.js";
|
|
861
|
+
|
|
862
|
+
return new Promise(function(resolve, reject) {
|
|
863
|
+
if (!("serviceWorker" in navigator)) {
|
|
864
|
+
reject(new Error("Service workers not supported in this browser"));
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
navigator.serviceWorker.register(swPath).then(function(registration) {
|
|
869
|
+
resolve(registration);
|
|
870
|
+
}).catch(function(error) {
|
|
871
|
+
reject(error);
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Gets the current push subscription from the browser's
|
|
878
|
+
* push manager, if one exists.
|
|
879
|
+
*
|
|
880
|
+
* @param {ServiceWorkerRegistration} registration The service worker registration.
|
|
881
|
+
* @returns {Promise} Promise that resolves with PushSubscription or null.
|
|
882
|
+
*/
|
|
883
|
+
Pushi.prototype.getPushSubscription = function(registration) {
|
|
884
|
+
return registration.pushManager.getSubscription();
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Subscribes the browser to push notifications using the
|
|
889
|
+
* Web Push API. Requires a VAPID public key from the server.
|
|
890
|
+
*
|
|
891
|
+
* @param {ServiceWorkerRegistration} registration The service worker registration.
|
|
892
|
+
* @param {String} vapidPublicKey The VAPID public key in base64url format.
|
|
893
|
+
* @returns {Promise} Promise that resolves with PushSubscription.
|
|
894
|
+
*/
|
|
895
|
+
Pushi.prototype.subscribeToPush = function(registration, vapidPublicKey) {
|
|
896
|
+
var self = this;
|
|
897
|
+
|
|
898
|
+
return new Promise(function(resolve, reject) {
|
|
899
|
+
// converts the base64url public key to Uint8Array
|
|
900
|
+
// as required by the Push API
|
|
901
|
+
var applicationServerKey = self._urlBase64ToUint8Array(vapidPublicKey);
|
|
902
|
+
|
|
903
|
+
registration.pushManager.subscribe({
|
|
904
|
+
userVisibleOnly: true,
|
|
905
|
+
applicationServerKey: applicationServerKey
|
|
906
|
+
}).then(function(subscription) {
|
|
907
|
+
resolve(subscription);
|
|
908
|
+
}).catch(function(error) {
|
|
909
|
+
reject(error);
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Extracts the subscription information from a PushSubscription
|
|
916
|
+
* object into a plain object suitable for sending to the server.
|
|
917
|
+
*
|
|
918
|
+
* @param {PushSubscription} subscription The browser's push subscription.
|
|
919
|
+
* @returns {Object} Object containing endpoint, p256dh, and auth keys.
|
|
920
|
+
*/
|
|
921
|
+
Pushi.prototype.extractSubscriptionInfo = function(subscription) {
|
|
922
|
+
var json = subscription.toJSON();
|
|
923
|
+
return {
|
|
924
|
+
endpoint: json.endpoint,
|
|
925
|
+
p256dh: json.keys.p256dh,
|
|
926
|
+
auth: json.keys.auth
|
|
927
|
+
};
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Sends the push subscription to the Pushi server to register
|
|
932
|
+
* for notifications on a specific event/channel.
|
|
933
|
+
*
|
|
934
|
+
* @param {Object} subscriptionInfo Object with endpoint, p256dh, auth.
|
|
935
|
+
* @param {String} event The event/channel name to subscribe to.
|
|
936
|
+
* @param {Object} options Optional settings (auth, unsubscribe).
|
|
937
|
+
* @returns {Promise} Promise that resolves with server response.
|
|
938
|
+
*/
|
|
939
|
+
Pushi.prototype.sendSubscriptionToServer = function(subscriptionInfo, event, options) {
|
|
940
|
+
options = options || {};
|
|
941
|
+
|
|
942
|
+
var url = this._buildApiUrl("/web_pushes");
|
|
943
|
+
|
|
944
|
+
// adds optional query parameters
|
|
945
|
+
var params = [];
|
|
946
|
+
if (options.auth) {
|
|
947
|
+
params.push("auth=" + encodeURIComponent(options.auth));
|
|
948
|
+
}
|
|
949
|
+
if (options.unsubscribe !== undefined) {
|
|
950
|
+
params.push("unsubscribe=" + options.unsubscribe);
|
|
951
|
+
}
|
|
952
|
+
if (params.length > 0) {
|
|
953
|
+
url += "?" + params.join("&");
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
var data = {
|
|
957
|
+
endpoint: subscriptionInfo.endpoint,
|
|
958
|
+
p256dh: subscriptionInfo.p256dh,
|
|
959
|
+
auth: subscriptionInfo.auth,
|
|
960
|
+
event: event
|
|
961
|
+
};
|
|
962
|
+
|
|
963
|
+
return new Promise(function(resolve, reject) {
|
|
964
|
+
var request = new XMLHttpRequest();
|
|
965
|
+
request.open("POST", url, true);
|
|
966
|
+
request.withCredentials = true;
|
|
967
|
+
request.setRequestHeader("Content-Type", "application/json");
|
|
968
|
+
request.onreadystatechange = function() {
|
|
969
|
+
if (request.readyState !== 4) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (request.status === 200 || request.status === 201) {
|
|
974
|
+
var result = JSON.parse(request.responseText);
|
|
975
|
+
resolve(result);
|
|
976
|
+
} else {
|
|
977
|
+
var error = new Error("Failed to send subscription to server: " + request.status);
|
|
978
|
+
reject(error);
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
request.send(JSON.stringify(data));
|
|
982
|
+
});
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Removes the push subscription from the Pushi server for a
|
|
987
|
+
* specific event/channel or all events if event is not provided.
|
|
988
|
+
*
|
|
989
|
+
* @param {String} endpoint The push endpoint URL.
|
|
990
|
+
* @param {String} event The event/channel (optional, removes all if not provided).
|
|
991
|
+
* @returns {Promise} Promise that resolves with server response.
|
|
992
|
+
*/
|
|
993
|
+
Pushi.prototype.removeSubscriptionFromServer = function(endpoint, event) {
|
|
994
|
+
// builds the URL with event as a query parameter
|
|
995
|
+
var path = "/web_pushes/" + encodeURIComponent(endpoint);
|
|
996
|
+
var url = this._buildApiUrl(path);
|
|
997
|
+
if (event) {
|
|
998
|
+
url += "?event=" + encodeURIComponent(event);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return new Promise(function(resolve, reject) {
|
|
1002
|
+
var request = new XMLHttpRequest();
|
|
1003
|
+
request.open("DELETE", url, true);
|
|
1004
|
+
request.withCredentials = true;
|
|
1005
|
+
request.onreadystatechange = function() {
|
|
1006
|
+
if (request.readyState !== 4) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (request.status === 200) {
|
|
1011
|
+
var result = JSON.parse(request.responseText);
|
|
1012
|
+
resolve(result);
|
|
1013
|
+
} else {
|
|
1014
|
+
var error = new Error("Failed to remove subscription from server: " + request.status);
|
|
1015
|
+
reject(error);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
request.send();
|
|
1019
|
+
});
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* High-level method to set up Web Push notifications. This handles
|
|
1024
|
+
* the complete flow: requesting permission, registering service worker,
|
|
1025
|
+
* subscribing to push, and registering with the server.
|
|
1026
|
+
*
|
|
1027
|
+
* IMPORTANT: You must call login() before using this method.
|
|
1028
|
+
*
|
|
1029
|
+
* @param {String} event The event/channel to subscribe to.
|
|
1030
|
+
* @param {Object} options Configuration options including:
|
|
1031
|
+
* - swPath: Service worker path (default: '/sw.js')
|
|
1032
|
+
* - auth: Authentication token for private channels
|
|
1033
|
+
* - unsubscribe: Remove existing subscriptions (default: true)
|
|
1034
|
+
* @returns {Promise} Promise that resolves with the subscription info.
|
|
1035
|
+
*/
|
|
1036
|
+
Pushi.prototype.setupWebPush = function(event, options) {
|
|
1037
|
+
var self = this;
|
|
1038
|
+
options = options || {};
|
|
1039
|
+
|
|
1040
|
+
// checks if the user is logged in
|
|
1041
|
+
if (!this.authenticated) {
|
|
1042
|
+
return Promise.reject(new Error(
|
|
1043
|
+
"Login required. Call login() before setupWebPush()"
|
|
1044
|
+
));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
var registration = null;
|
|
1048
|
+
var vapidPublicKey = null;
|
|
1049
|
+
|
|
1050
|
+
return this.requestNotificationPermission()
|
|
1051
|
+
.then(function(permission) {
|
|
1052
|
+
if (permission !== "granted") {
|
|
1053
|
+
throw new Error("Notification permission denied");
|
|
1054
|
+
}
|
|
1055
|
+
return self.registerServiceWorker(options.swPath);
|
|
1056
|
+
})
|
|
1057
|
+
.then(function(reg) {
|
|
1058
|
+
registration = reg;
|
|
1059
|
+
return self.getVapidPublicKey();
|
|
1060
|
+
})
|
|
1061
|
+
.then(function(key) {
|
|
1062
|
+
vapidPublicKey = key;
|
|
1063
|
+
return self.subscribeToPush(registration, vapidPublicKey);
|
|
1064
|
+
})
|
|
1065
|
+
.then(function(subscription) {
|
|
1066
|
+
var info = self.extractSubscriptionInfo(subscription);
|
|
1067
|
+
return self.sendSubscriptionToServer(info, event, options)
|
|
1068
|
+
.then(function(result) {
|
|
1069
|
+
return {
|
|
1070
|
+
subscription: subscription,
|
|
1071
|
+
subscriptionInfo: info,
|
|
1072
|
+
serverResponse: result
|
|
1073
|
+
};
|
|
1074
|
+
});
|
|
1075
|
+
});
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
/**
|
|
1079
|
+
* High-level method to unsubscribe from Web Push notifications.
|
|
1080
|
+
* This removes the subscription from both the browser and the server.
|
|
1081
|
+
*
|
|
1082
|
+
* IMPORTANT: You must call login() before using this method.
|
|
1083
|
+
*
|
|
1084
|
+
* @param {String} event The event/channel to unsubscribe from (optional).
|
|
1085
|
+
* @returns {Promise} Promise that resolves when unsubscribed.
|
|
1086
|
+
*/
|
|
1087
|
+
Pushi.prototype.teardownWebPush = function(event) {
|
|
1088
|
+
var self = this;
|
|
1089
|
+
|
|
1090
|
+
// checks if the user is logged in
|
|
1091
|
+
if (!this.authenticated) {
|
|
1092
|
+
return Promise.reject(new Error(
|
|
1093
|
+
"Login required. Call login() before teardownWebPush()"
|
|
1094
|
+
));
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
return navigator.serviceWorker.ready
|
|
1098
|
+
.then(function(registration) {
|
|
1099
|
+
return registration.pushManager.getSubscription();
|
|
1100
|
+
})
|
|
1101
|
+
.then(function(subscription) {
|
|
1102
|
+
if (!subscription) {
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
var endpoint = subscription.endpoint;
|
|
1107
|
+
|
|
1108
|
+
// unsubscribes from the browser first, then removes
|
|
1109
|
+
// the subscription from the server
|
|
1110
|
+
return subscription.unsubscribe()
|
|
1111
|
+
.then(function() {
|
|
1112
|
+
return self.removeSubscriptionFromServer(endpoint, event);
|
|
1113
|
+
});
|
|
1114
|
+
});
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// ===========================================
|
|
1118
|
+
// Helper Methods
|
|
1119
|
+
// ===========================================
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Builds an API URL for the Web Push methods. Uses session-based
|
|
1123
|
+
* authentication, so no app key is included in the URL.
|
|
1124
|
+
*
|
|
1125
|
+
* @param {String} path The API path (e.g., '/vapid_key').
|
|
1126
|
+
* @returns {String} The complete API URL.
|
|
1127
|
+
* @private
|
|
1128
|
+
*/
|
|
1129
|
+
Pushi.prototype._buildApiUrl = function(path) {
|
|
1130
|
+
return this._getBaseWebUrl() + path;
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Gets the base HTTP URL for API calls. Derives it from the
|
|
1135
|
+
* WebSocket URL if not explicitly configured.
|
|
1136
|
+
*
|
|
1137
|
+
* @returns {String} The base HTTP URL without trailing slash.
|
|
1138
|
+
* @private
|
|
1139
|
+
*/
|
|
1140
|
+
Pushi.prototype._getBaseWebUrl = function() {
|
|
1141
|
+
var baseWebUrl = this.baseWebUrl;
|
|
1142
|
+
if (!baseWebUrl) {
|
|
1143
|
+
baseWebUrl = this.baseUrl.replace("wss://", "https://").replace("ws://", "http://");
|
|
1144
|
+
}
|
|
1145
|
+
return baseWebUrl.replace(/\/$/, "");
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
/**
|
|
1149
|
+
* Converts a base64url string to Uint8Array. This is used
|
|
1150
|
+
* internally to convert the VAPID public key to the format
|
|
1151
|
+
* required by the Push API (applicationServerKey).
|
|
1152
|
+
*
|
|
1153
|
+
* @param {String} base64String Base64url encoded string.
|
|
1154
|
+
* @returns {Uint8Array} The decoded bytes.
|
|
1155
|
+
* @private
|
|
1156
|
+
*/
|
|
1157
|
+
Pushi.prototype._urlBase64ToUint8Array = function(base64String) {
|
|
1158
|
+
// adds padding if needed (base64url doesn't require padding
|
|
1159
|
+
// but atob requires it)
|
|
1160
|
+
var padding = "=".repeat((4 - base64String.length % 4) % 4);
|
|
1161
|
+
var base64 = (base64String + padding)
|
|
1162
|
+
.replace(/-/g, "+")
|
|
1163
|
+
.replace(/_/g, "/");
|
|
1164
|
+
|
|
1165
|
+
var rawData = window.atob(base64);
|
|
1166
|
+
var outputArray = new Uint8Array(rawData.length);
|
|
1167
|
+
|
|
1168
|
+
for (var i = 0; i < rawData.length; ++i) {
|
|
1169
|
+
outputArray[i] = rawData.charCodeAt(i);
|
|
1170
|
+
}
|
|
1171
|
+
return outputArray;
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
if (typeof String.prototype.startsWith !== "function") {
|
|
1175
|
+
String.prototype.startsWith = function(string) {
|
|
1176
|
+
return this.slice(0, string.length) === string;
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Module exports for Node.js/CommonJS environments
|
|
1181
|
+
if (typeof module !== "undefined" && module.exports) {
|
|
1182
|
+
module.exports = { Pushi: Pushi, Channel: Channel, Observable: Observable };
|
|
1183
|
+
}
|
package/pushi.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESM wrapper for pushi.js
|
|
3
|
+
* This allows importing pushi in ESM environments while keeping
|
|
4
|
+
* the main pushi.js file compatible with legacy browsers.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const pushi = require("./pushi.js");
|
|
10
|
+
|
|
11
|
+
export const { Pushi, Channel, Observable } = pushi;
|
|
12
|
+
export default pushi;
|