apns-ruby 0.0.0 → 0.0.2
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.
- 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:
|