restforce 3.0.1 → 3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 96f8f9560cf8a66a733f7755fd374d741191b7f8
4
- data.tar.gz: bbcfa722aefa2285b4f632b98e4109fdb585041c
3
+ metadata.gz: 3352202fbc4460b9f83a50b93f32dce58142b7ad
4
+ data.tar.gz: a2680033dec036183f9fcb767cb4935f17ecad27
5
5
  SHA512:
6
- metadata.gz: 207ba956da9189b584ae3e6362f0f46fe7cb877105d28b4550eb6c071f38aca8f8f326b04e4c7082ce129a719e2b56cb0870ae715e95eb3aee2b7ee0d027eaf0
7
- data.tar.gz: fe5cf08c7d393d8720324a8aeceb80456b0ceda886df6cb1a185ab4b35516fe2cf89bacb0697af78fbe6da45c979320e4569d41f842f8d7c392f9fbe38f1d2e3
6
+ metadata.gz: 8ef0d7d4a741961ce17a76fb5b93e3906f216bbe7bea5dbe2598eead312571e859392e4a54f32414fda1cffb0ffca9a4a3e8283f9eb333baea58529e51d9f86a
7
+ data.tar.gz: b6f445ee78e3e217b9279b7958744281e9d16dcb5cb7e1606c1a1cdc7d6f462afc8c7278a1de4ce751fd929a8372d5fbdd22c19148bc504b64f6790f1e52cfdb
@@ -1,3 +1,7 @@
1
+ ## 3.1.0 (Aug 16, 2018)
2
+
3
+ * Add support for replaying missed messages when using the Salesforce Streaming API (@andreimaxim, @drteeth, @panozzaj)
4
+
1
5
  ## 3.0.1 (Aug 4, 2018)
2
6
 
3
7
  * Fix `NoMethodError` when upserting an existing record (@opti)
data/README.md CHANGED
@@ -25,7 +25,7 @@ Features include:
25
25
 
26
26
  Add this line to your application's Gemfile:
27
27
 
28
- gem 'restforce', '~> 3.0.1'
28
+ gem 'restforce', '~> 3.1.0'
29
29
 
30
30
  And then execute:
31
31
 
@@ -547,7 +547,102 @@ end
547
547
  Boom, you're now receiving push notifications when Accounts are
548
548
  created/updated.
549
549
 
550
- _See also: [Force.com Streaming API docs](http://www.salesforce.com/us/developer/docs/api_streaming/index.htm)_
550
+ #### Replaying Events
551
+
552
+ Since API version 37.0, Salesforce stores events for 24 hours and they can be
553
+ replayed if your application experienced some downtime.
554
+
555
+ In order to replay past events, all you need to do is specify the last known
556
+ event ID when subscribing and you will receive all events that happened since
557
+ that event ID:
558
+
559
+ ```ruby
560
+ EM.run {
561
+ # Subscribe to the PushTopic.
562
+ client.subscribe 'AllAccounts', replay: 10 do |message|
563
+ puts message.inspect
564
+ end
565
+ }
566
+ ```
567
+
568
+ In this specific case you will see events with replay ID 11, 12 and so on.
569
+
570
+ There are two magic values for the replay ID accepted by Salesforce:
571
+
572
+ * `-2`, for getting all the events that appeared in the last 24 hours
573
+ * `-1`, for getting only newer events
574
+
575
+ **Warning**: Only use a replay ID of a event from the last 24 hours otherwise
576
+ Salesforce will not send anything, including newer events. If in doubt, use one
577
+ of the two magic replay IDs mentioned above.
578
+
579
+ You might want to store the replay ID in some sort of datastore so you can
580
+ access it, for example between application restarts. In that case, there is the
581
+ option of passing a custom replay handler which responds to `[]` and `[]=`.
582
+
583
+ Below is a sample replay handler that stores the replay ID for each channel in
584
+ memory using a Hash, stores a timestamp and has some rudimentary logic that
585
+ will use one of the magic IDs depending on the value of the timestamp:
586
+
587
+ ```ruby
588
+ class SimpleReplayHander
589
+
590
+ MAX_AGE = 86_400 # 24 hours
591
+
592
+ INIT_REPLAY_ID = -1
593
+ DEFAULT_REPLAY_ID = -2
594
+
595
+ def initialize
596
+ @channels = {}
597
+ @last_modified = nil
598
+ end
599
+
600
+ # This method is called during the initial subscribe phase
601
+ # in order to send the correct replay ID.
602
+ def [](channel)
603
+ if @last_modified.nil?
604
+ puts "[#{channel}] No timestamp defined, sending magic replay ID #{INIT_REPLAY_ID}"
605
+
606
+ INIT_REPLAY_ID
607
+ elsif old_replay_id?
608
+ puts "[#{channel}] Old timestamp, sending magic replay ID #{DEFAULT_REPLAY_ID}"
609
+
610
+ DEFAULT_REPLAY_ID
611
+ else
612
+ @channels[channel]
613
+ end
614
+ end
615
+
616
+ def []=(channel, replay_id)
617
+ puts "[#{channel}] Writing replay ID: #{replay_id}"
618
+
619
+ @last_modified = Time.now
620
+ @channels[channel] = replay_id
621
+ end
622
+
623
+ def old_replay_id?
624
+ @last_modified.is_a?(Time) && Time.now - @last_modified > MAX_AGE
625
+ end
626
+ end
627
+ ```
628
+
629
+ In order to use it, simply pass the object as the value of the `replay` option
630
+ of the subscription:
631
+
632
+ ```ruby
633
+ EM.run {
634
+ # Subscribe to the PushTopic and use the custom replay handler to store any
635
+ # received replay ID.
636
+ client.subscribe 'AllAccounts', replay: SimpleReplayHandler.new do |message|
637
+ puts message.inspect
638
+ end
639
+ }
640
+ ```
641
+
642
+ _See also_:
643
+
644
+ * [Force.com Streaming API docs](http://www.salesforce.com/us/developer/docs/api_streaming/index.htm)
645
+ * [Message Durability docs](https://developer.salesforce.com/docs/atlas.en-us.api_streaming.meta/api_streaming/using_streaming_api_durability.htm)
551
646
 
552
647
  *Note:* Restforce's streaming implementation is known to be compatible with version `0.8.9` of the faye gem.
553
648
 
@@ -9,7 +9,8 @@ module Restforce
9
9
  # block - A block to run when a new message is received.
10
10
  #
11
11
  # Returns a Faye::Subscription
12
- def subscribe(channels, &block)
12
+ def subscribe(channels, options = {}, &block)
13
+ Array(channels).each { |channel| replay_handlers[channel] = options[:replay] }
13
14
  faye.subscribe Array(channels).map { |channel| "/topic/#{channel}" }, &block
14
15
  end
15
16
 
@@ -32,6 +33,64 @@ module Restforce
32
33
  client.bind 'transport:up' do
33
34
  Restforce.log "[COMETD UP]"
34
35
  end
36
+
37
+ client.add_extension ReplayExtension.new(replay_handlers)
38
+ end
39
+ end
40
+
41
+ def replay_handlers
42
+ @_replay_handlers ||= {}
43
+ end
44
+
45
+ class ReplayExtension
46
+ def initialize(replay_handlers)
47
+ @replay_handlers = replay_handlers
48
+ end
49
+
50
+ def incoming(message, callback)
51
+ callback.call(message).tap do
52
+ channel = message.fetch('channel').gsub('/topic/', '')
53
+ replay_id = message.fetch('data', {}).fetch('event', {})['replayId']
54
+
55
+ handler = @replay_handlers[channel]
56
+ if !replay_id.nil? && !handler.nil? && handler.respond_to?(:[]=)
57
+ # remember the last replay_id for this channel
58
+ handler[channel] = replay_id
59
+ end
60
+ end
61
+ end
62
+
63
+ def outgoing(message, callback)
64
+ # Leave non-subscribe messages alone
65
+ unless message['channel'] == '/meta/subscribe'
66
+ return callback.call(message)
67
+ end
68
+
69
+ channel = message['subscription'].gsub('/topic/', '')
70
+
71
+ # Set the replay value for the channel
72
+ message['ext'] ||= {}
73
+ message['ext']['replay'] = {
74
+ "/topic/#{channel}" => replay_id(channel)
75
+ }
76
+
77
+ # Carry on and send the message to the server
78
+ callback.call message
79
+ end
80
+
81
+ private
82
+
83
+ def replay_id(channel)
84
+ handler = @replay_handlers[channel]
85
+ if handler.is_a?(Integer)
86
+ handler # treat it as a scalar
87
+ elsif handler.respond_to?(:[])
88
+ # Ask for the latest replayId for this channel
89
+ handler[channel]
90
+ else
91
+ # Just pass it along
92
+ handler
93
+ end
35
94
  end
36
95
  end
37
96
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Restforce
4
- VERSION = '3.0.1'
4
+ VERSION = '3.1.0'
5
5
  end
@@ -17,6 +17,33 @@ describe Restforce::Concerns::Streaming, event_machine: true do
17
17
 
18
18
  client.subscribe(channels, &subscribe_block)
19
19
  end
20
+
21
+ context "replay_handlers" do
22
+ before {
23
+ faye_double.should_receive(:subscribe).at_least(1)
24
+ client.stub faye: faye_double
25
+ }
26
+
27
+ it 'registers nil handlers when no replay option is given' do
28
+ client.subscribe(channels, &subscribe_block)
29
+ client.replay_handlers.should eq('channel1' => nil, 'channel2' => nil)
30
+ end
31
+
32
+ it 'registers a replay_handler for each channel given' do
33
+ client.subscribe(channels, replay: -2, &subscribe_block)
34
+ client.replay_handlers.should eq('channel1' => -2, 'channel2' => -2)
35
+ end
36
+
37
+ it 'replaces earlier handlers in subsequent calls' do
38
+ client.subscribe(%w[channel1 channel2], replay: 2, &subscribe_block)
39
+ client.subscribe(%w[channel2 channel3], replay: 3, &subscribe_block)
40
+ client.replay_handlers.should eq(
41
+ 'channel1' => 2,
42
+ 'channel2' => 3,
43
+ 'channel3' => 3
44
+ )
45
+ end
46
+ end
20
47
  end
21
48
 
22
49
  describe '.faye' do
@@ -42,6 +69,8 @@ describe Restforce::Concerns::Streaming, event_machine: true do
42
69
  faye_double.should_receive(:set_header).with('Authorization', 'OAuth secret2')
43
70
  faye_double.should_receive(:bind).with('transport:down').and_yield
44
71
  faye_double.should_receive(:bind).with('transport:up').and_yield
72
+ faye_double.should_receive(:add_extension).with \
73
+ kind_of(Restforce::Concerns::Streaming::ReplayExtension)
45
74
  subject
46
75
  end
47
76
  end
@@ -52,4 +81,87 @@ describe Restforce::Concerns::Streaming, event_machine: true do
52
81
  end
53
82
  end
54
83
  end
84
+
85
+ describe Restforce::Concerns::Streaming::ReplayExtension do
86
+ let(:handlers) { {} }
87
+ let(:extension) { Restforce::Concerns::Streaming::ReplayExtension.new(handlers) }
88
+
89
+ it 'sends nil without a specified handler' do
90
+ output = subscribe(extension, to: "channel1")
91
+ read_replay(output).should eq('/topic/channel1' => nil)
92
+ end
93
+
94
+ it 'with a scalar replay id' do
95
+ handlers['channel1'] = -2
96
+ output = subscribe(extension, to: "channel1")
97
+ read_replay(output).should eq('/topic/channel1' => -2)
98
+ end
99
+
100
+ it 'with a hash' do
101
+ hash_handler = { 'channel1' => -1, 'channel2' => -2 }
102
+
103
+ handlers['channel1'] = hash_handler
104
+ handlers['channel2'] = hash_handler
105
+
106
+ output = subscribe(extension, to: "channel1")
107
+ read_replay(output).should eq('/topic/channel1' => -1)
108
+
109
+ output = subscribe(extension, to: "channel2")
110
+ read_replay(output).should eq('/topic/channel2' => -2)
111
+ end
112
+
113
+ it 'with an object' do
114
+ custom_handler = double('custom_handler')
115
+ custom_handler.should_receive(:[]).and_return(123)
116
+ handlers['channel1'] = custom_handler
117
+
118
+ output = subscribe(extension, to: "channel1")
119
+ read_replay(output).should eq('/topic/channel1' => 123)
120
+ end
121
+
122
+ it 'remembers the last replayId' do
123
+ handler = { 'channel1' => 41 }
124
+ handlers['channel1'] = handler
125
+ message = {
126
+ 'channel' => '/topic/channel1',
127
+ 'data' => {
128
+ 'event' => { 'replayId' => 42 }
129
+ }
130
+ }
131
+
132
+ extension.incoming(message, ->(m) {})
133
+ handler.should eq('channel1' => 42)
134
+ end
135
+
136
+ it 'when an incoming message has no replayId' do
137
+ handler = { 'channel1' => 41 }
138
+ handlers['channel1'] = handler
139
+
140
+ message = {
141
+ 'channel' => '/topic/channel1',
142
+ 'data' => {}
143
+ }
144
+
145
+ extension.incoming(message, ->(m) {})
146
+ handler.should eq('channel1' => 41)
147
+ end
148
+
149
+ private
150
+
151
+ def subscribe(extension, options = {})
152
+ output = nil
153
+ message = {
154
+ 'channel' => '/meta/subscribe',
155
+ 'subscription' => "/topic/#{options[:to]}"
156
+ }
157
+ extension.outgoing(message, ->(m) {
158
+ output = m
159
+ })
160
+ output
161
+ end
162
+
163
+ def read_replay(message)
164
+ message.fetch('ext', {})['replay']
165
+ end
166
+ end
55
167
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restforce
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eric J. Holmes
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-08-04 00:00:00.000000000 Z
12
+ date: 2018-08-16 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: faraday