Class: Mayu::Session

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/mayu/session.rb

Defined Under Namespace

Classes: AlreadyRunningError, InvalidIdError, InvalidTokenError, SerializedSession

Constant Summary collapse

ID_FORMAT =
/\A[A-Za-z0-9_-]{21}\z/
TOKEN_LENGTH =
64
TOKEN_FORMAT =
/\A\w{#{TOKEN_LENGTH}}\z/
Marshaled =
T.type_alias { [String, String, String, String, String] }
Writable =
T.type_alias { T.any(Async::HTTP::Body::Writable, Brotli::Writer) }

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(environment:, path:, headers: {}, vtree: nil, store: nil) ⇒ Session

Returns a new instance of Session.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/mayu/session.rb', line 120

def initialize(environment:, path:, headers: {}, vtree: nil, store: nil)
  @environment = environment
  @id = T.let(Nanoid.generate, String)
  @token = T.let(self.class.generate_token, String)
  @path = path
  @headers = headers
  @vtree = T.let(vtree || VDOM::VTree.new(session: self), VDOM::VTree)
  @log = T.let(EventStream::Log.new, EventStream::Log)
  @store =
    T.let(
      store || environment.create_store(initial_state: {}),
      State::Store
    )
  @app = T.let(environment.load_root(path, headers:), VDOM::Descriptor)
  @last_ping_at = T.let(Time.now.to_f, Float)
  @barrier = T.let(Async::Barrier.new, Async::Barrier)
end

Instance Attribute Details

#environmentObject (readonly)

Returns the value of attribute environment.



95
96
97
# File 'lib/mayu/session.rb', line 95

def environment
  @environment
end

#headersObject (readonly)

Returns the value of attribute headers.



93
94
95
# File 'lib/mayu/session.rb', line 93

def headers
  @headers
end

#idObject (readonly)

Returns the value of attribute id.



87
88
89
# File 'lib/mayu/session.rb', line 87

def id
  @id
end

#last_ping_atObject (readonly)

Returns the value of attribute last_ping_at.



97
98
99
# File 'lib/mayu/session.rb', line 97

def last_ping_at
  @last_ping_at
end

#logObject (readonly)

Returns the value of attribute log.



99
100
101
# File 'lib/mayu/session.rb', line 99

def log
  @log
end

#pathObject (readonly)

Returns the value of attribute path.



91
92
93
# File 'lib/mayu/session.rb', line 91

def path
  @path
end

#tokenObject (readonly)

Returns the value of attribute token.



89
90
91
# File 'lib/mayu/session.rb', line 89

def token
  @token
end

Class Method Details

.generate_tokenObject



55
56
57
# File 'lib/mayu/session.rb', line 55

def self.generate_token
  SecureRandom.alphanumeric(TOKEN_LENGTH)
end

.init(environment:, path:) ⇒ Object



25
26
27
# File 'lib/mayu/session.rb', line 25

def self.init(environment:, path:)
  new(environment:, path:)
end

.restore(environment:, dumped:) ⇒ Object



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/mayu/session.rb', line 32

def self.restore(environment:, dumped:)
  Marshal.restore(
    dumped,
    ->(obj) do
      case obj
      when self
        obj.instance_variable_set(:@environment, environment)
        obj
      when SerializedSession
        obj.to_session(environment)
      else
        obj
      end
    end
  )
end

.valid_id?(id) ⇒ Boolean

Returns:

  • (Boolean)


70
71
72
# File 'lib/mayu/session.rb', line 70

def self.valid_id?(id)
  id.match?(ID_FORMAT)
end

.valid_token?(token) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/mayu/session.rb', line 60

def self.valid_token?(token)
  token.match?(TOKEN_FORMAT)
end

.validate_id!(id) ⇒ Object

Raises:



75
76
77
# File 'lib/mayu/session.rb', line 75

def self.validate_id!(id)
  raise InvalidIdError unless valid_id?(id)
end

.validate_token!(token) ⇒ Object

Raises:



65
66
67
# File 'lib/mayu/session.rb', line 65

def self.validate_token!(token)
  raise InvalidTokenError unless valid_token?(token)
end

Instance Method Details

#activity!Object



260
261
262
# File 'lib/mayu/session.rb', line 260

def activity!
  @last_ping_at = Time.now.to_f
end

#authorized?(token) ⇒ Boolean

Returns:

  • (Boolean)


80
81
82
# File 'lib/mayu/session.rb', line 80

def authorized?(token)
  RbNaCl::Util.verify64(self.token, token)
end

#expired?(timeout_seconds = 30) ⇒ Boolean

Returns:

  • (Boolean)


102
103
104
# File 'lib/mayu/session.rb', line 102

def expired?(timeout_seconds = 30)
  seconds_since_last_ping > timeout_seconds
end

#fetch(url, method: :GET, headers: {}, body: nil) ⇒ Object



255
256
257
# File 'lib/mayu/session.rb', line 255

def fetch(url, method: :GET, headers: {}, body: nil)
  @environment.fetch.fetch(url, method:, headers:, body:)
end

#handle_callback(callback_id, payload = {}) ⇒ Object



267
268
269
270
# File 'lib/mayu/session.rb', line 267

def handle_callback(callback_id, payload = {})
  activity!
  @vtree.handle_callback(callback_id, payload)
end

#initial_render(body, task: Async::Task.current) ⇒ Object



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/mayu/session.rb', line 151

def initial_render(body, task: Async::Task.current)
  @vtree.render(@app, lifecycles: false)

  root = @vtree.root or raise "There is no root"

  html = root.to_html
  stylesheets =
    @vtree
      .assets
      .select { _1.end_with?(".css") }
      .map { "/__mayu/static/#{_1}" }

  # freeze

  # encrypted_session =
  #   @environment.encrypted_marshal.dump(SerializedSession.dump_session(self))

  links = [
    %{<script async type="module" src="/__mayu/runtime/#{environment.init_js}##{id}" crossorigin="same-origin" fetchpriority="high"></script>},
    *stylesheets.map do |stylesheet|
      %{<link rel="stylesheet" href="#{stylesheet}">}
    end
  ].join

  # scripts = %{<template id="mayu-init">#{encrypted_session}</template>}
  scripts = ""
  body.write("<!DOCTYPE html>\n")

  task.async do
    @vtree.root&.write_html(body, links:, scripts:)
    body.close
  rescue => e
    p e
  end

  { stylesheets: }
end

#marshal_dumpObject



225
226
227
228
229
230
231
232
233
234
# File 'lib/mayu/session.rb', line 225

def marshal_dump
  [
    @id,
    @token,
    @path,
    @headers,
    VDOM::Marshalling.dump(@vtree),
    Marshal.dump(@store.state)
  ]
end

#marshal_load(a) ⇒ Object



237
238
239
240
241
242
243
244
245
# File 'lib/mayu/session.rb', line 237

def marshal_load(a)
  @id, @token, @path, @headers, dumped_vtree, state = a
  @last_ping_at = Time.now.to_f
  @vtree = VDOM::Marshalling.restore(dumped_vtree, session: self)
  @store = @environment.create_store(initial_state: Marshal.restore(state))
  @app = @environment.load_root(@path, headers:)
  @barrier = Async::Barrier.new
  @log = EventStream::Log.new
end


279
280
281
282
283
284
# File 'lib/mayu/session.rb', line 279

def navigate(path)
  Console.logger.info(self, "navigate: #{path.inspect}")
  @app = @environment.load_root(path, headers:)
  @path = path
  @vtree.replace_root(@app)
end

#rerenderObject



273
274
275
276
# File 'lib/mayu/session.rb', line 273

def rerender
  @app = @environment.load_root(path, headers:)
  @vtree.replace_root(@app)
end

#run(task: Async::Task.current, &block) ⇒ Object



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/mayu/session.rb', line 292

def run(task: Async::Task.current, &block)
  root = @vtree.root

  raise "No root!" unless root

  barrier = Async::Barrier.new(parent: @barrier)

  barrier.async do |subtask|
    yield [:init, { ids: root.id_tree }]

    root.traverse do |vnode|
      if c = vnode.component
        # TODO: Make sure the component isn't already mounted..
        # maybe can check that in the component wrapper?
        # Also, shouldn't this be done once when resuming rather
        # than when starting the event stream?
        c.mount
      end
    end

    @vtree.render(@app, lifecycles: true)

    updater = VDOM::VTree::Updater.new(@vtree)

    updater
      .run(metrics: environment.metrics, task: subtask) do |msg|
        case msg
        in [:patch, patches]
          yield [:patch, patches]
        in [:exception, error]
          yield [:exception, error]
        in [:pong, timestamp]
          yield [:pong, timestamp]
        in [:navigate, href]
          navigate(href)
          yield [:navigate, path: href.force_encoding("utf-8")]
        in [:action, payload]
          yield [:action, payload]
        in [:update_finished, *]
          # noop
        else
          Console.logger.error(self, "Unknown event: #{msg.inspect}")
        end
      end
      .wait

    barrier.stop
  end

  barrier.async do
    loop do
      # puts "keep alive task"
      sleep 1
      yield [:keep_alive, nil]
    end
  ensure
    # puts "Stopped this task"
    barrier.stop
  end

  barrier
end

#seconds_since_last_pingObject



107
108
109
# File 'lib/mayu/session.rb', line 107

def seconds_since_last_ping
  Time.now.to_f - last_ping_at
end

#stop!Object



139
140
141
# File 'lib/mayu/session.rb', line 139

def stop!
  @barrier.stop
end