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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bb1f035a12ec76366159c112eccd23dc2f319b8d
4
- data.tar.gz: 2d6a9188eba06cb48683f10ba748a71069a46157
3
+ metadata.gz: a82816830da1287822fead19cd690a9e6f9a8042
4
+ data.tar.gz: cf75ab95b88a81f4b3415182744c3ddf5888e760
5
5
  SHA512:
6
- metadata.gz: 07fe76e8c32cd0a91d0bc76eb283896aa53a65b4ffc4b5caf960fbb018c1212c29880e13b9acb7a65d948ab8618497b70fe6da9f946d8595e528f87ed03c1987
7
- data.tar.gz: 47499c4c0f549710d22a27e4d379d8642ebc0e2cd34fb18b9c3d9b2b8e7a65b6cfd4a76f599eec9c23853d00b078df8de61a43b3f6dbe9ecb3cebaa6385652fc
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.write([n1, ne, n2])
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.write([APNS::Notification.new(token, alert: 'hello world 0')])
36
+ conn.push([APNS::Notification.new(token, alert: 'hello world 0')])
32
37
  sleep(7)
33
- conn.write([APNS::Notification.new(token, alert: 'hello world 1')])
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 the 'world', 'hello world 0', and 'hello world 1' notifications
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.
@@ -1,2 +1,3 @@
1
+ require 'apns/infinite_array'
1
2
  require 'apns/connection'
2
3
  require 'apns/notification'
@@ -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
- notification_buffer_size: 1_000_000)
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 write ns
32
- if @notifications.size > @notification_buffer_size
33
- ns = detect_failed_notifications(timeout: 0.5) + ns
34
- @notifications = []
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
- failed_notifications = detect_failed_notifications timeout: 3
41
- @notifications = []
42
- @ssl.close
43
- @sock.close
44
- @sock, @ssl = open_connection
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
- ns = failed_notifications
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 detect_failed_notifications(timeout:)
68
- begin
69
- tuple = Timeout::timeout(timeout){ @ssl.read(6) }
70
- _, code, failed_id = tuple.unpack("ccN")
71
- rescue Timeout::Error
72
- end
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
- @notifications[failed_id+1..-1] || []
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 a hundred thousand
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} @notifications.size=#{@notifications.size} @error_handler=#{@error_handler}>"
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.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: