fragmentary 0.2.2 → 0.4.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.
@@ -24,8 +24,6 @@ module Fragmentary
24
24
  # redundant duplicate request.
25
25
  after_commit :touch_parent, :on => [:update, :destroy]
26
26
 
27
- attr_accessible :parent_id, :root_id, :record_id, :user_id, :user_type, :key
28
-
29
27
  attr_accessor :indexed_children
30
28
 
31
29
  # Set cache timestamp format to :usec instead of :nsec because the latter is greater precision than Postgres supports,
@@ -73,8 +71,14 @@ module Fragmentary
73
71
  # Collect the attributes to be used when searching for an existing fragment. Fragments are unique by these values.
74
72
  search_attributes = {}
75
73
 
76
- parent_id = options.delete(:parent_id)
77
- search_attributes.merge!(:parent_id => parent_id) if parent_id
74
+ if (parent_id = options.delete(:parent_id))
75
+ search_attributes.merge!(:parent_id => parent_id)
76
+ else
77
+ application_root_url_column = Fragmentary.config.application_root_url_column
78
+ if (application_root_url = options.delete(application_root_url_column)) && column_names.include?(application_root_url_column.to_s)
79
+ search_attributes.merge!(application_root_url_column => application_root_url)
80
+ end
81
+ end
78
82
 
79
83
  [:record_id, :user_id, :user_type, :key].each do |attribute_name|
80
84
  if klass.needs?(attribute_name)
@@ -117,20 +121,53 @@ module Fragmentary
117
121
  self
118
122
  end
119
123
 
124
+ # There is one queue per user_type per application instance (the current app and any external instances). The queues
125
+ # for all fragments are held in common by the Fragment base class here in @@request_queues but are also indexed on a
126
+ # subclass basis by an individual subclass's user_types (see the inherited hook below). As well as being accessible
127
+ # here as Fragment.request_queues, the queues are also available without indexation as RequestQueue.all.
120
128
  def request_queues
121
- @@request_queues ||= Hash.new do |hash, user_type|
122
- hash[user_type] = RequestQueue.new(user_type)
129
+ @@request_queues ||= Hash.new do |hsh, host_url|
130
+ # As well as acting as a hash key to index the set of request queues for a given target application instance
131
+ # (for which its uniqueness is the only requirement), host_url is also passed to the RequestQueue constructor,
132
+ # from which it is used:
133
+ # (i) by the RequestQueue::Sender to derive the name of the delayed_job queue that will be used to process the
134
+ # queued requests if the sender is invoked in asynchronous mode - see RequestQueue::Sender#schedulerequests.
135
+ # (ii) by the Fragmentary::InternalUserSession instantiated by the Sender to configure the session_host.
136
+ hsh[host_url] = Hash.new do |hsh2, user_type|
137
+ hsh2[user_type] = RequestQueue.new(user_type, host_url)
138
+ end
123
139
  end
124
- if self == base_class
125
- @@request_queues
126
- else
127
- return nil unless (requestable? or new.requestable?)
128
- user_types.each_with_object({}){|user_type, queues| queues[user_type] = @@request_queues[user_type]}
140
+ end
141
+
142
+ # Subclass-specific request_queues
143
+ def inherited(subclass)
144
+ subclass.instance_eval do
145
+
146
+ def request_queues
147
+ super # ensure that @@request_queues has been defined
148
+ @request_queues ||= begin
149
+ app_root_url = Fragmentary.application_root_url
150
+ remote_urls = Fragmentary.config.remote_urls
151
+ user_types.each_with_object( Hash.new {|hsh0, url| hsh0[url] = {}} ) do |user_type, hsh|
152
+ # Internal request queues
153
+ hsh[app_root_url][user_type] = @@request_queues[app_root_url][user_type]
154
+ # External request queues
155
+ if remote_urls.any?
156
+ unless Rails.application.routes.default_url_options[:host]
157
+ raise "Can't create external request queues without setting Rails.application.routes.default_url_options[:host]"
158
+ end
159
+ remote_urls.each {|remote_url| hsh[remote_url][user_type] = @@request_queues[remote_url][user_type]}
160
+ end
161
+ end
162
+ end
163
+ end
164
+
129
165
  end
166
+ super
130
167
  end
131
168
 
132
169
  def remove_queued_request(user:, request_path:)
133
- request_queues[user_type(user)].remove_path(request_path)
170
+ request_queues.each{|key, hsh| hsh[user_type(user)].remove_path(request_path)}
134
171
  end
135
172
 
136
173
  def subscriber
@@ -158,6 +195,11 @@ module Fragmentary
158
195
  # signed in. When the fragment is instantiated using FragmentsHelper methods 'cache_fragment' or 'CacheBuilder.cache_child',
159
196
  # a :user option is added to the options hash automatically from the value of 'current_user'. The user_type is extracted
160
197
  # from this option in Fragment.attributes.
198
+ #
199
+ # For each class that declares 'needs_user_type', a set of user_types is defined that determines the set of request_queues
200
+ # that will be used to send requests to the application when a fragment is touched. By default these user_types are defined
201
+ # globally using 'Fragmentary.setup' but they can alternatively be set on a class-specific basis by passing a :session_users
202
+ # option to 'needs_user_type'. See 'Fragmentary.parse_session_users' for details.
161
203
  def needs_user_type(options = {})
162
204
  self.extend NeedsUserType
163
205
  instance_eval do
@@ -218,28 +260,25 @@ module Fragmentary
218
260
  # by the fragment.
219
261
  class << record_type_subscription
220
262
  set_callback :after_destroy, :after, ->{subscriber.client.remove_fragments_for_record(record.id)}
263
+ set_callback :after_create, :after, ->{subscriber.client.try_request_for_record(record.id)}
221
264
  end
222
265
  end
223
266
 
224
- if requestable?
225
- record_class = record_type.constantize
226
- instance_eval <<-HEREDOC
227
- subscribe_to #{record_class} do
228
- def create_#{record_class.model_name.param_key}_successful(record)
229
- request = Fragmentary::Request.new(request_method, request_path(record.id),
230
- request_parameters(record.id), request_options)
231
- queue_request(request)
232
- end
233
- end
234
- HEREDOC
235
- end
236
-
267
+ self.extend RecordClassMethods
237
268
  define_method(:record){record_type.constantize.find(record_id)}
238
269
  end
239
270
  end
240
271
 
241
- def remove_fragments_for_record(record_id)
242
- where(:record_id => record_id).each(&:destroy)
272
+ module RecordClassMethods
273
+ def remove_fragments_for_record(record_id)
274
+ where(:record_id => record_id).each(&:destroy)
275
+ end
276
+
277
+ def try_request_for_record(record_id)
278
+ if requestable?
279
+ queue_request(request(record_id))
280
+ end
281
+ end
243
282
  end
244
283
 
245
284
  def needs_record_id?
@@ -288,9 +327,7 @@ module Fragmentary
288
327
  end
289
328
 
290
329
  def queue_request(request=nil)
291
- if request
292
- request_queues.each{|key, queue| queue << request}
293
- end
330
+ request_queues.each{|key, hsh| hsh.each{|key2, queue| queue << request}} if request
294
331
  end
295
332
 
296
333
  def requestable?
@@ -308,7 +345,11 @@ module Fragmentary
308
345
 
309
346
  # The instance method 'request_options' is defined in terms of this.
310
347
  def request_options
311
- nil
348
+ {}
349
+ end
350
+
351
+ def request
352
+ raise NotImplementedError
312
353
  end
313
354
 
314
355
  # This method defines the handler for the creation of new list items. The method takes:
@@ -453,12 +494,15 @@ module Fragmentary
453
494
  end
454
495
 
455
496
  def touch(*args, no_request: false)
456
- request_queues.each{|key, queue| queue << request} if request && !no_request
497
+ @no_request = no_request # stored for use in #touch_parent via the after_commit callback
498
+ request_queues.each{|key, hsh| hsh.each{|key2, queue| queue << request}} if request && !no_request
457
499
  super(*args)
458
500
  end
459
501
 
502
+ # delete the associated cache content before destroying the fragment
460
503
  def destroy(options = {})
461
504
  options.delete(:delete_matches) ? delete_matched_cache : delete_cache
505
+ @no_request = options.delete(:no_request) # stored for use in #touch_parent via the after_commit callback
462
506
  super()
463
507
  end
464
508
 
@@ -467,9 +511,17 @@ module Fragmentary
467
511
  end
468
512
 
469
513
  def delete_cache
470
- cache_store.delete(ActiveSupport::Cache.expand_cache_key(self, 'views'))
514
+ cache_store.delete(fragment_key)
515
+ end
516
+
517
+ # Recursively delete the cache entry for this fragment and all of its children
518
+ # Does NOT destroy the fragment or its children
519
+ def delete_cache_tree
520
+ children.each(&:delete_cache_tree)
521
+ delete_cache if cache_exist?
471
522
  end
472
523
 
524
+ # Recursively touch the fragment and all of its children
473
525
  def touch_tree(no_request: false)
474
526
  children.each{|child| child.touch_tree(:no_request => no_request)}
475
527
  # If there are children, we'll have already touched this fragment in the process of touching them.
@@ -479,19 +531,34 @@ module Fragmentary
479
531
  # Touch this fragment and all descendants that have entries in the cache. Destroy any that
480
532
  # don't have cache entries.
481
533
  def touch_or_destroy
482
- puts " touch_or_destroy #{self.class.name} #{id}"
483
534
  if cache_exist?
484
535
  children.each(&:touch_or_destroy)
485
536
  # if there are children, this will be touched automatically once they are.
486
537
  touch(:no_request => true) unless children.any?
487
538
  else
488
- destroy # will also destroy all children because of :dependent => :destroy
539
+ destroy(:no_request => true) # will also destroy all children because of :dependent => :destroy
489
540
  end
490
541
  end
491
542
 
492
543
  def cache_exist?
493
544
  # expand_cache_key calls cache_key and prepends "views/"
494
- cache_store.exist?(ActiveSupport::Cache.expand_cache_key(self, 'views'))
545
+ cache_store.exist?(fragment_key)
546
+ end
547
+
548
+
549
+ # Typically used along with #cache_exist? when testing from the console.
550
+ # Note that both methods will only return correct results for fragments associated with the application_root_url
551
+ # (either root or children) corresponding to the particular console session in use. i.e. you can't see into the
552
+ # production cache from a prerelease console session and vice versa.
553
+ def content
554
+ cache_store.read(fragment_key)
555
+ end
556
+
557
+ # This emulates the result of passing the fragment object to AbstractController::Caching::Fragments#combined_fragment_cache_key
558
+ # when the cache helper method invokes controller.read_fragment from the view. The result can be passed to ActiveSupport::Cache methods
559
+ # #read, #write, #fetch, #delete, and #exist?
560
+ def fragment_key
561
+ ['views', self]
495
562
  end
496
563
 
497
564
  # Request-related methods...
@@ -529,7 +596,8 @@ module Fragmentary
529
596
 
530
597
  private
531
598
  def touch_parent
532
- parent.try :touch unless previous_changes["memo"]
599
+ parent.try(:touch, {:no_request => @no_request}) unless previous_changes["memo"]
600
+ @no_request = false
533
601
  end
534
602
 
535
603
  module NeedsRecordId
@@ -3,6 +3,7 @@ module Fragmentary
3
3
  module FragmentsHelper
4
4
 
5
5
  def cache_fragment(options, &block)
6
+ options.reverse_merge!(Fragmentary.config.application_root_url_column => Fragmentary.application_root_url.gsub(%r{https?://}, ''))
6
7
  CacheBuilder.new(self).cache_fragment(options, &block)
7
8
  end
8
9
 
@@ -10,6 +11,7 @@ module Fragmentary
10
11
  # the template option is deprecated but avoids breaking prior usage
11
12
  template = options.delete(:template) || self
12
13
  options.reverse_merge!(:user => Template.new(template).current_user)
14
+ options.reverse_merge!(Fragmentary.config.application_root_url_column => Fragmentary.application_root_url.gsub(%r{https?://}, ''))
13
15
  CacheBuilder.new(template, Fragmentary::Fragment.base_class.existing(options))
14
16
  end
15
17
 
@@ -34,7 +36,16 @@ module Fragmentary
34
36
  builder = CacheBuilder.new(@template, next_fragment)
35
37
  unless no_cache
36
38
  @template.cache next_fragment, :skip_digest => true do
37
- yield(builder)
39
+ if Fragmentary.config.insert_timestamps
40
+ @template.safe_concat("<!-- #{next_fragment.type} #{next_fragment.id} cached by Fragmentary version #{VERSION} at #{Time.now.utc} -->")
41
+ if deployed_at && release_name
42
+ @template.safe_concat("<!-- Cached using application release #{release_name} deployed at #{deployed_at} -->")
43
+ end
44
+ yield(builder)
45
+ @template.safe_concat("<!-- #{next_fragment.type} #{next_fragment.id} ends -->")
46
+ else
47
+ yield(builder)
48
+ end
38
49
  end
39
50
  else
40
51
  yield(builder)
@@ -50,6 +61,13 @@ module Fragmentary
50
61
  @fragment.send(method, *args)
51
62
  end
52
63
 
64
+ def deployed_at
65
+ @deployed_at ||= Fragmentary.config.deployed_at
66
+ end
67
+
68
+ def release_name
69
+ @release_name ||= Fragmentary.config.release_name
70
+ end
53
71
  end
54
72
 
55
73
  end
@@ -1,4 +1,26 @@
1
1
  module Fragmentary
2
+
3
+ class HandlerSerializer < ActiveJob::Serializers::ObjectSerializer
4
+
5
+ def serialize?(arg)
6
+ arg.is_a? Fragmentary::Handler
7
+ end
8
+
9
+ def serialize(handler)
10
+ super(
11
+ {
12
+ :class_name => handler.class.name,
13
+ :args => handler.args
14
+ }
15
+ )
16
+ end
17
+
18
+ def deserialize(hsh)
19
+ hsh[:class_name].constantize.new(hsh[:args])
20
+ end
21
+
22
+ end
23
+
2
24
  class Handler
3
25
  def self.all
4
26
  @@all
@@ -14,6 +36,8 @@ module Fragmentary
14
36
  handler
15
37
  end
16
38
 
39
+ attr_reader :args
40
+
17
41
  def initialize(**args)
18
42
  @args = args
19
43
  end
@@ -1,12 +1,11 @@
1
+ require 'fragmentary/serializers/handler_serializer'
2
+
1
3
  module Fragmentary
2
4
 
3
- class Dispatcher
4
- def initialize(tasks)
5
- @tasks = tasks
6
- end
5
+ class DispatchHandlersJob < ActiveJob::Base
7
6
 
8
- def perform
9
- @tasks.each do |task|
7
+ def perform(tasks)
8
+ tasks.each do |task|
10
9
  Rails.logger.info "***** Dispatching task for handler class #{task.class.name}"
11
10
  task.call
12
11
  end
@@ -14,6 +13,7 @@ module Fragmentary
14
13
  queue.start
15
14
  end
16
15
  end
16
+
17
17
  end
18
18
 
19
19
  end
@@ -0,0 +1,26 @@
1
+ require 'fragmentary/serializers/request_queue_serializer'
2
+
3
+ module Fragmentary
4
+
5
+ class SendRequestsJob < ActiveJob::Base
6
+
7
+ after_perform :schedule_next
8
+
9
+ def perform(queue, delay: nil, between: nil, queue_suffix: '', priority: 0)
10
+ @queue = queue
11
+ @delay = delay
12
+ @between = between
13
+ @queue_suffix = queue_suffix
14
+ @priority = priority
15
+ @between ? @queue.send_next_request : @queue.send_all_requests
16
+ end
17
+
18
+ def schedule_next
19
+ if @queue.size > 0
20
+ self.enqueue(:wait => @between, :queue => @queue.target.queue_name + @queue_suffix, :priority => @priority)
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -33,7 +33,9 @@ module Fragmentary
33
33
  end
34
34
 
35
35
  def after_update_broadcast
36
+ Rails.logger.info "\n***** #{start = Time.now} broadcasting :after_update from #{self.class.name} #{self.id}\n"
36
37
  broadcast(:after_update, self) if self.previous_changes.any?
38
+ Rails.logger.info "\n***** #{Time.now} broadcast :after_update from #{self.class.name} #{self.id} took #{(Time.now - start) * 1000} ms\n"
37
39
  end
38
40
 
39
41
  def after_destroy_broadcast
@@ -3,28 +3,13 @@ module Fragmentary
3
3
  class Request
4
4
  attr_reader :method, :path, :options, :parameters
5
5
 
6
- def initialize(method, path, parameters=nil, options=nil)
6
+ def initialize(method, path, parameters=nil, options={})
7
7
  @method, @path, @parameters, @options = method, path, parameters, options
8
8
  end
9
9
 
10
10
  def ==(other)
11
11
  method == other.method and path == other.path and parameters == other.parameters and options == other.options
12
12
  end
13
-
14
- def to_proc
15
- method = @method; path = @path; parameters = @parameters; options = @options.try :dup
16
- if @options.try(:[], :xhr)
17
- Proc.new do
18
- puts " * Sending xhr request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
19
- send(:xhr, method, path, parameters, options)
20
- end
21
- else
22
- Proc.new do
23
- puts " * Sending request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
24
- send(method, path, parameters, options)
25
- end
26
- end
27
- end
28
13
  end
29
14
 
30
15
  end
@@ -1,5 +1,3 @@
1
- require 'fragmentary/user_session'
2
-
3
1
  module Fragmentary
4
2
 
5
3
  class RequestQueue
@@ -8,12 +6,27 @@ module Fragmentary
8
6
  @@all ||= []
9
7
  end
10
8
 
11
- attr_reader :requests, :user_type, :sender
9
+ def self.send_all(between: nil)
10
+ unless between
11
+ all.each{|q| q.start}
12
+ else
13
+ unless between.is_a? ActiveSupport::Duration
14
+ raise TypeError, "Fragmentary::RequestQueue.send_all requires the keyword argument :between to be of class ActiveSupport::Duration. The value provided is of class #{between.class.name}."
15
+ end
16
+ delay = 0.seconds
17
+ all.each{|q| q.start(:delay => delay += between)}
18
+ end
19
+ end
20
+
21
+ attr_reader :requests, :user_type, :host_root_url
12
22
 
13
- def initialize(user_type)
23
+ def initialize(user_type, host_root_url)
14
24
  @user_type = user_type
25
+ # host_root_url represents where the queued *requests* are to be processed. For internal sessions it also represents where
26
+ # the *queue* will be processed by delayed_job. For external requests, the queue will be processed by the host creating the
27
+ # queue and the requests will be explicitly sent to the host_root_url.
28
+ @host_root_url = host_root_url
15
29
  @requests = []
16
- @sender = Sender.new(self)
17
30
  self.class.all << self
18
31
  end
19
32
 
@@ -40,6 +53,10 @@ module Fragmentary
40
53
  requests.delete_if{|r| r.path == path}
41
54
  end
42
55
 
56
+ def sender
57
+ @sender ||= Sender.new(self)
58
+ end
59
+
43
60
  def send(**args)
44
61
  sender.start(args)
45
62
  end
@@ -49,22 +66,46 @@ module Fragmentary
49
66
  end
50
67
 
51
68
  class Sender
69
+
52
70
  class << self
53
71
  def jobs
54
72
  ::Delayed::Job.where("(handler LIKE ?) OR (handler LIKE ?)", "--- !ruby/object:#{name} %", "--- !ruby/object:#{name}\n%")
55
73
  end
56
74
  end
57
75
 
58
- attr_reader :queue
76
+ class Target
77
+
78
+ attr_reader :url
79
+
80
+ def initialize(url)
81
+ @url = url
82
+ end
83
+
84
+ def queue_name
85
+ @url.gsub(%r{https?://}, '')
86
+ end
87
+
88
+ end
89
+
90
+ attr_reader :queue, :target, :delay, :between, :queue_suffix, :priority
59
91
 
60
92
  def initialize(queue)
61
93
  @queue = queue
94
+ @target = Target.new(queue.host_root_url)
95
+ end
96
+
97
+ def session_user
98
+ @session_user ||= Fragmentary::SessionUser.fetch(queue.user_type)
99
+ end
100
+
101
+ def session
102
+ @session ||= InternalUserSession.new(@target.url, session_user)
62
103
  end
63
104
 
64
105
  # Send all requests, either directly or by schedule
65
- def start(delay: nil, between: nil)
106
+ def start(delay: nil, between: nil, queue_suffix: '', priority: 0)
66
107
  Rails.logger.info "\n***** Processing request queue for user_type '#{queue.user_type}'\n"
67
- @delay = delay; @between = between
108
+ @delay = delay; @between = between; @queue_suffix = queue_suffix; @priority = priority
68
109
  if @delay or @between
69
110
  schedule_requests(@delay)
70
111
  # sending requests by schedule makes a copy of the sender and queue objects for
@@ -80,22 +121,15 @@ module Fragmentary
80
121
  @between ? send_next_request : send_all_requests
81
122
  end
82
123
 
83
- def success
84
- schedule_requests(@between) if queue.size > 0
85
- end
86
-
87
- private
88
-
89
- def next_request
90
- queue.next_request.to_proc
91
- end
92
-
93
124
  def send_next_request
94
125
  if queue.size > 0
95
- session.instance_exec(&(next_request))
126
+ request = queue.next_request
127
+ session.send_request(:method => request.method, :path => request.path, :parameters => request.parameters, :options => request.options)
96
128
  end
97
129
  end
98
130
 
131
+ private
132
+
99
133
  def send_all_requests
100
134
  while queue.size > 0
101
135
  send_next_request
@@ -105,22 +139,11 @@ module Fragmentary
105
139
  def schedule_requests(delay=0.seconds)
106
140
  if queue.size > 0
107
141
  clear_session
108
- Delayed::Job.transaction do
109
- self.class.jobs.destroy_all
110
- Delayed::Job.enqueue self, :run_at => delay.from_now
111
- end
142
+ job = SendRequestsJob.new(queue, delay: delay, between: between, queue_suffix: queue_suffix, priority: priority)
143
+ job.enqueue(:wait => delay, :queue => target.queue_name + queue_suffix, :priority => priority)
112
144
  end
113
145
  end
114
146
 
115
- def session
116
- @session ||= new_session
117
- end
118
-
119
- def new_session
120
- session_user = Fragmentary::SessionUser.fetch(queue.user_type)
121
- UserSession.new(session_user)
122
- end
123
-
124
147
  def clear_session
125
148
  @session = nil
126
149
  end
@@ -128,4 +151,5 @@ module Fragmentary
128
151
  end
129
152
 
130
153
  end
154
+
131
155
  end
@@ -0,0 +1,24 @@
1
+ module Fragmentary
2
+
3
+ class HandlerSerializer < ActiveJob::Serializers::ObjectSerializer
4
+
5
+ def serialize?(arg)
6
+ arg.is_a? Fragmentary::Handler
7
+ end
8
+
9
+ def serialize(handler)
10
+ super(
11
+ {
12
+ :class_name => handler.class.name,
13
+ :args => handler.args
14
+ }
15
+ )
16
+ end
17
+
18
+ def deserialize(hsh)
19
+ hsh[:class_name].constantize.new(hsh[:args])
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,36 @@
1
+ module Fragmentary
2
+
3
+ class RequestQueueSerializer < ActiveJob::Serializers::ObjectSerializer
4
+
5
+ def serialize?(arg)
6
+ arg.is_a? Fragmentary::RequestQueue
7
+ end
8
+
9
+ def serialize(queue)
10
+ super(
11
+ {
12
+ :user_type => queue.user_type,
13
+ :host_root_url => queue.host_root_url,
14
+ :requests => queue.requests.map do |r|
15
+ {
16
+ :method => r.method,
17
+ :path => r.path,
18
+ :parameters => r.parameters,
19
+ :opinions => r.options
20
+ }
21
+ end
22
+ }
23
+ )
24
+ end
25
+
26
+ def deserialize(hsh)
27
+ queue = RequestQueue.new(hsh[:user_type], hsh[:host_root_url])
28
+ hsh[:requests].each do |r|
29
+ queue << Request.new(r[:method], r[:path], r[:parameters], r[:options] || {})
30
+ end
31
+ queue
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,38 @@
1
+ module Fragmentary
2
+
3
+ class SessionUser
4
+
5
+ def self.all
6
+ @@all ||= Hash.new
7
+ end
8
+
9
+ def self.fetch(key)
10
+ all[key]
11
+ end
12
+
13
+ def initialize(user_type, options={})
14
+ if user = self.class.fetch(user_type)
15
+ if user.options != options
16
+ raise RangeError, "You can't redefine an existing SessionUser object: #{user_type.inspect}"
17
+ else
18
+ user
19
+ end
20
+ else
21
+ @user_type = user_type
22
+ @options = options
23
+ self.class.all.merge!({user_type => self})
24
+ end
25
+ end
26
+
27
+ def credentials
28
+ options[:credentials]
29
+ end
30
+
31
+ protected
32
+ def options
33
+ @options
34
+ end
35
+
36
+ end
37
+
38
+ end