apns-ruby 0.0.0 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +13 -7
- data/lib/apns.rb +1 -0
- data/lib/apns/connection.rb +47 -32
- data/lib/apns/infinite_array.rb +64 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a82816830da1287822fead19cd690a9e6f9a8042
|
4
|
+
data.tar.gz: cf75ab95b88a81f4b3415182744c3ddf5888e760
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8426a8e9f97f4ee0ba56d8b619d3c6588ab7e801db21aacad8e4f1a5ba13a2c8fb208a67061189c2a2214d3f24e46641e3c7c19d8727eac80f8af7991878620b
|
7
|
+
data.tar.gz: 2e4ef8e08692bf8228aa6a280c9f72d28efbda116d84b6e44be8e80db2c1d2bc5d7394afdd638b137aac6867bd57ee80ffacc8452c62c9a79cefba3fae34236b
|
data/README.md
CHANGED
@@ -4,8 +4,6 @@ This gem sends APNS notifications with proper error handling. Most importantly,
|
|
4
4
|
* Storing notifications in a database, and have a separate process consume them. Worse, only a single consumer is ever allowed.
|
5
5
|
* Opening a new SSL connection for every notification, which typically takes several hundred milliseconds. Imagine how long it would take to send a million notifications.
|
6
6
|
|
7
|
-
Beware though, that this gem has it's own nonsense in that by default, it sets the notification buffer size to 1 million, which could potentially take up 256MB of memory (Apple caps the size of a single notification's payload at 256 bytes). If you cannot afford to spare that amount of memory, configure it to a lesser number `conn = APNS::Connection.new(notification_buffer_size: 1024) # 1024 bytes`.
|
8
|
-
|
9
7
|
Example usage:
|
10
8
|
```
|
11
9
|
pem = path_to_your_pem_file
|
@@ -15,7 +13,12 @@ conn = APNS::Connection.new(pem: pem, host: host)
|
|
15
13
|
conn.error_handler = ->(code, notification) {
|
16
14
|
case code
|
17
15
|
when 8
|
16
|
+
if notification.nil?
|
17
|
+
puts "Insufficient buffer size to collect failed notifications, please set a larger buffer size when creating an APNS connection."
|
18
|
+
return
|
19
|
+
end
|
18
20
|
puts "Invalid token: #{notification.device_token}"
|
21
|
+
# Handle the invalid token per Apple's docs
|
19
22
|
else
|
20
23
|
# Consult Apple's docs
|
21
24
|
end
|
@@ -23,17 +26,20 @@ conn.error_handler = ->(code, notification) {
|
|
23
26
|
n1 = APNS::Notification.new(token, alert: 'hello')
|
24
27
|
ne = APNS::Notification.new('bogustoken', alert: 'error')
|
25
28
|
n2 = APNS::Notification.new(token, alert: 'world')
|
26
|
-
conn.
|
29
|
+
conn.push([n1, ne, n2])
|
27
30
|
# Should receive only a 'hello' notification on your device
|
28
31
|
|
29
|
-
# Wait for Apple to report an error and close the connection
|
32
|
+
# Wait for Apple to report an error and close the connection on their side, due to
|
33
|
+
# the bogus token in the second notification.
|
34
|
+
# We'll detect this and automatically retry and invoke your error handler.
|
30
35
|
sleep(7)
|
31
|
-
conn.
|
36
|
+
conn.push([APNS::Notification.new(token, alert: 'hello world 0')])
|
32
37
|
sleep(7)
|
33
|
-
conn.
|
38
|
+
conn.push([APNS::Notification.new(token, alert: 'hello world 1')])
|
34
39
|
|
35
40
|
# 'Invalid token: bogustoken' is printed out
|
36
|
-
# We should be receiving
|
41
|
+
# We should be receiving each and every successful notification with texts:
|
42
|
+
# 'world', 'hello world 0', and 'hello world 1'.
|
37
43
|
```
|
38
44
|
|
39
45
|
A great amount of code in this gem is copied from https://github.com/jpoz/APNS , many thanks to his pioneering work. This work itself is licensed under the MIT license.
|
data/lib/apns.rb
CHANGED
data/lib/apns/connection.rb
CHANGED
@@ -3,6 +3,9 @@ require 'socket'
|
|
3
3
|
require 'timeout'
|
4
4
|
|
5
5
|
module APNS
|
6
|
+
|
7
|
+
MAX_32_BIT = 2_147_483_647
|
8
|
+
|
6
9
|
class Connection
|
7
10
|
attr_accessor :error_handler
|
8
11
|
|
@@ -10,13 +13,12 @@ class Connection
|
|
10
13
|
pass: nil,
|
11
14
|
host: 'gateway.sandbox.push.apple.com',
|
12
15
|
port: 2195,
|
13
|
-
|
14
|
-
@notifications =
|
16
|
+
buffer_size: 4 * 1024)
|
17
|
+
@notifications = InfiniteArray.new(buffer_size: buffer_size)
|
15
18
|
@pem = pem
|
16
19
|
@pass = pass
|
17
20
|
@host = host
|
18
21
|
@port = port
|
19
|
-
@notification_buffer_size = notification_buffer_size
|
20
22
|
|
21
23
|
@sock, @ssl = open_connection
|
22
24
|
ObjectSpace.define_finalizer(self, self.class.finalize(@sock, @ssl))
|
@@ -28,22 +30,42 @@ class Connection
|
|
28
30
|
}
|
29
31
|
end
|
30
32
|
|
31
|
-
def
|
32
|
-
|
33
|
-
|
34
|
-
|
33
|
+
def push ns
|
34
|
+
# The notification identifier is set to 4 bytes in the APNS protocol. Thus,
|
35
|
+
# upon hitting this limit, read for failures and restart the counting again.
|
36
|
+
if @notifications.size + ns.size > MAX_32_BIT - 10
|
37
|
+
code, failed_id = read_failure_info(timeout: 3)
|
38
|
+
if failed_id
|
39
|
+
ns = @notifications.items_from(failed_id+1) + ns
|
40
|
+
reopen_connection
|
41
|
+
@error_handler.call(code, @notifications.item_at(failed_id))
|
42
|
+
end
|
43
|
+
|
44
|
+
@notifications.clear
|
35
45
|
end
|
36
46
|
|
47
|
+
ns.each{ |n|
|
48
|
+
n.message_identifier = [@notifications.size].pack('N')
|
49
|
+
@notifications.push(n)
|
50
|
+
}
|
51
|
+
write ns
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def write ns
|
37
58
|
packed = pack_notifications(ns)
|
38
59
|
@ssl.write(packed)
|
39
60
|
rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@
|
61
|
+
code, failed_id = read_failure_info(timeout: 30)
|
62
|
+
reopen_connection
|
63
|
+
return unless failed_id # there's nothing we can do
|
64
|
+
|
65
|
+
@error_handler.call(code, @notifications.item_at(failed_id))
|
45
66
|
|
46
|
-
|
67
|
+
@notifications.delete_where_index_less_than(failed_id+1)
|
68
|
+
ns = @notifications.items_from(failed_id+1)
|
47
69
|
retry
|
48
70
|
end
|
49
71
|
|
@@ -51,9 +73,6 @@ class Connection
|
|
51
73
|
bytes = ''
|
52
74
|
|
53
75
|
notifications.each do |n|
|
54
|
-
n.message_identifier = [@notifications.size].pack('N')
|
55
|
-
@notifications << n
|
56
|
-
|
57
76
|
# Each notification frame consists of
|
58
77
|
# 1. (e.g. protocol version) 2 (unsigned char [1 byte])
|
59
78
|
# 2. size of the full frame (unsigend int [4 byte], big endian)
|
@@ -64,21 +83,17 @@ class Connection
|
|
64
83
|
bytes
|
65
84
|
end
|
66
85
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
failed_id ||= @notifications.size
|
74
|
-
|
75
|
-
# Report error to user
|
76
|
-
failed_notification = @notifications[failed_id]
|
77
|
-
if @error_handler && failed_notification
|
78
|
-
@error_handler.call(code, failed_notification)
|
79
|
-
end
|
86
|
+
def read_failure_info(timeout:)
|
87
|
+
tuple = Timeout::timeout(timeout){ @ssl.read(6) }
|
88
|
+
_, code, failed_id = tuple.unpack("ccN")
|
89
|
+
[code, failed_id]
|
90
|
+
rescue Timeout::Error
|
91
|
+
end
|
80
92
|
|
81
|
-
|
93
|
+
def reopen_connection
|
94
|
+
@ssl.close
|
95
|
+
@sock.close
|
96
|
+
@sock, @ssl = open_connection
|
82
97
|
end
|
83
98
|
|
84
99
|
def open_connection
|
@@ -94,9 +109,9 @@ class Connection
|
|
94
109
|
end
|
95
110
|
|
96
111
|
# Override inspect since we do not want to print out the entire @notifications,
|
97
|
-
# whose size might be over
|
112
|
+
# whose size might be over tens of thousands
|
98
113
|
def inspect
|
99
|
-
puts "#<#{self.class}:#{'0x%014x' % object_id} @pem=#{@pem} @pass=#{@pass} @host=#{@host} @port=#{@port} @
|
114
|
+
puts "#<#{self.class}:#{'0x%014x' % object_id} @pem=#{@pem} @pass=#{@pass} @host=#{@host} @port=#{@port} @error_handler=#{@error_handler}>"
|
100
115
|
end
|
101
116
|
|
102
117
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module APNS
|
2
|
+
|
3
|
+
class Element
|
4
|
+
attr_accessor :index, :item
|
5
|
+
def initialize(index:, item:)
|
6
|
+
self.index = index
|
7
|
+
self.item = item
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class InfiniteArray
|
12
|
+
|
13
|
+
def initialize(buffer_size:)
|
14
|
+
@buffer_size = buffer_size
|
15
|
+
@buf = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def push item
|
19
|
+
@buf << Element.new(index: size, item: item)
|
20
|
+
pop_front if @buf.size > @buffer_size
|
21
|
+
end
|
22
|
+
|
23
|
+
def size
|
24
|
+
return 0 if @buf[0].nil?
|
25
|
+
@buf.last.index + 1
|
26
|
+
end
|
27
|
+
|
28
|
+
def item_at index
|
29
|
+
buf_index = buffer_index_from(index: index)
|
30
|
+
return unless buf_index
|
31
|
+
@buf[buf_index].item
|
32
|
+
end
|
33
|
+
|
34
|
+
def items_from index
|
35
|
+
buf_index = buffer_index_from(index: index)
|
36
|
+
return [] unless buf_index
|
37
|
+
@buf[buf_index..-1].map(&:item)
|
38
|
+
end
|
39
|
+
|
40
|
+
def pop_front
|
41
|
+
@buf = @buf[1..-1]
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete_where_index_less_than index
|
45
|
+
while @buf[0].index < index
|
46
|
+
pop_front
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def clear
|
51
|
+
@buf = []
|
52
|
+
@hash = {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def buffer_index_from(index:)
|
56
|
+
return if index > @buf.last.index
|
57
|
+
buf_index = index - @buf[0].index
|
58
|
+
return if buf_index < 0
|
59
|
+
buf_index
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: apns-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- awaw
|
@@ -20,6 +20,7 @@ files:
|
|
20
20
|
- README.md
|
21
21
|
- lib/apns.rb
|
22
22
|
- lib/apns/connection.rb
|
23
|
+
- lib/apns/infinite_array.rb
|
23
24
|
- lib/apns/notification.rb
|
24
25
|
homepage: https://github.com/fumin/apns-ruby
|
25
26
|
licenses:
|