File: streaming.rb

package info (click to toggle)
ruby-sinatra 4.2.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,932 kB
  • sloc: ruby: 17,700; sh: 25; makefile: 8
file content (246 lines) | stat: -rw-r--r-- 5,800 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# frozen_string_literal: true

require 'sinatra/base'

module Sinatra
  # = Sinatra::Streaming
  #
  # Sinatra 1.3 introduced the +stream+ helper. This addon improves the
  # streaming API by making the stream object imitate an IO object, turning
  # it into a real Deferrable and making the body play nicer with middleware
  # unaware of streaming.
  #
  # == IO-like behavior
  #
  # This is useful when passing the stream object to a library expecting an
  # IO or StringIO object.
  #
  #   get '/' do
  #     stream do |out|
  #       out.puts "Hello World!", "How are you?"
  #       out.write "Written #{out.pos} bytes so far!\n"
  #       out.putc(65) unless out.closed?
  #       out.flush
  #     end
  #   end
  #
  # == Better Middleware Handling
  #
  # Blocks passed to #map! or #map will actually be applied when streaming
  # takes place (as you might have suspected, #map! applies modifications
  # to the current body, while #map creates a new one):
  #
  #   class StupidMiddleware
  #     def initialize(app) @app = app end
  #
  #     def call(env)
  #       status, headers, body = @app.call(env)
  #       body.map! { |e| e.upcase }
  #       [status, headers, body]
  #     end
  #   end
  #
  #   use StupidMiddleware
  #
  #   get '/' do
  #     stream do |out|
  #       out.puts "still"
  #       sleep 1
  #       out.puts "streaming"
  #     end
  #   end
  #
  # Even works if #each is used to generate an Enumerator:
  #
  #   def call(env)
  #     status, headers, body = @app.call(env)
  #     body = body.each.map { |s| s.upcase }
  #     [status, headers, body]
  #   end
  #
  # Note that both examples violate the Rack specification.
  #
  # == Setup
  #
  # In a classic application:
  #
  #   require "sinatra"
  #   require "sinatra/streaming"
  #
  # In a modular application:
  #
  #   require "sinatra/base"
  #   require "sinatra/streaming"
  #
  #   class MyApp < Sinatra::Base
  #     helpers Sinatra::Streaming
  #   end
  module Streaming
    def stream(*)
      stream = super
      stream.extend Stream
      stream.app = self
      env['async.close'].callback { stream.close } if env.key? 'async.close'
      stream
    end

    module Stream
      attr_accessor :app, :lineno, :pos, :transformer, :closed
      alias tell pos
      alias closed? closed

      def self.extended(obj)
        obj.closed = false
        obj.lineno = 0
        obj.pos = 0
        obj.callback { obj.closed = true }
        obj.errback  { obj.closed = true }
      end

      def <<(data)
        raise IOError, 'not opened for writing' if closed?

        @transformer ||= nil
        data = data.to_s
        data = @transformer[data] if @transformer
        @pos += data.bytesize
        super(data)
      end

      def each
        # that way body.each.map { ... } works
        return self unless block_given?

        super
      end

      def map(&block)
        # dup would not copy the mixin
        clone.map!(&block)
      end

      def map!(&block)
        @transformer ||= nil

        if @transformer
          inner = @transformer
          outer = block
          block = proc { |value| outer[inner[value]] }
        end
        @transformer = block
        self
      end

      def write(data)
        self << data
        data.to_s.bytesize
      end

      alias syswrite write
      alias write_nonblock write

      def print(*args)
        args.each { |arg| self << arg }
        nil
      end

      def printf(format, *args)
        print(format.to_s % args)
      end

      def putc(c)
        print c.chr
      end

      def puts(*args)
        args.each { |arg| self << "#{arg}\n" }
        nil
      end

      def close_read
        raise IOError, 'closing non-duplex IO for reading'
      end

      def closed_read?
        true
      end

      def closed_write?
        closed?
      end

      def external_encoding
        Encoding.find settings.default_encoding
      rescue NameError
        settings.default_encoding
      end

      def settings
        app.settings
      end

      def rewind
        @pos = @lineno = 0
      end

      def not_open_for_reading(*)
        raise IOError, 'not opened for reading'
      end

      alias bytes         not_open_for_reading
      alias eof?          not_open_for_reading
      alias eof           not_open_for_reading
      alias getbyte       not_open_for_reading
      alias getc          not_open_for_reading
      alias gets          not_open_for_reading
      alias read          not_open_for_reading
      alias read_nonblock not_open_for_reading
      alias readbyte      not_open_for_reading
      alias readchar      not_open_for_reading
      alias readline      not_open_for_reading
      alias readlines     not_open_for_reading
      alias readpartial   not_open_for_reading
      alias sysread       not_open_for_reading
      alias ungetbyte     not_open_for_reading
      alias ungetc        not_open_for_reading
      private :not_open_for_reading

      def enum_not_open_for_reading(*)
        not_open_for_reading if block_given?
        enum_for(:not_open_for_reading)
      end

      alias chars     enum_not_open_for_reading
      alias each_line enum_not_open_for_reading
      alias each_byte enum_not_open_for_reading
      alias each_char enum_not_open_for_reading
      alias lines     enum_not_open_for_reading
      undef enum_not_open_for_reading

      def dummy(*) end
      alias flush             dummy
      alias fsync             dummy
      alias internal_encoding dummy
      alias pid               dummy
      undef dummy

      def seek(*)
        0
      end

      alias sysseek seek

      def sync
        true
      end

      def tty?
        false
      end

      alias isatty tty?
    end
  end

  helpers Streaming
end