json-emitter 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +64 -71
- data/lib/json-emitter.rb +5 -4
- data/lib/json-emitter/context.rb +45 -0
- data/lib/json-emitter/emitter.rb +6 -32
- data/lib/json-emitter/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5ffd5b387bbbf0ce7f40a9b725dbaaea83a479ac53ada2ede1f01231c5be28e1
|
4
|
+
data.tar.gz: c0af5bb60b80c67da129f2ba14fea69a99c97ba69a47e508de165ebb12bf11e7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ffb66b5cae058582b8d0904b6ae55272b7065ea6f412a2ddc7ab6db89a030f1e13c7801894e672f182c2da0b72d9e7211b9751f500f05fa91c8356679416403
|
7
|
+
data.tar.gz: e7a855f40029fbd7343cd4bfebf0e59594d81257fd2d0a232b8bf9cee4d0ac82210c8fa8907f6c5ae0eeaf4ee3acb68ca5b58adaa9a7525c1c094ac8f67e6d35
|
data/README.md
CHANGED
@@ -1,106 +1,99 @@
|
|
1
|
-
|
1
|
+
# JsonEmitter
|
2
2
|
|
3
3
|
JsonEmitter is a library for efficiently generating very large bits of JSON in Ruby. Need to generate a JSON array of 10,000 database records without eating up all your RAM? No problem! Objects? Nested structures? JsonEmitter has you covered.
|
4
4
|
|
5
5
|
Use JsonEmitter in your Rack/Rails/Sinatra/Grape API to stream large JSON responses without worrying about RAM or HTTP timeouts. Use it to write large JSON objects to your filesystem, S3, or ~~a 3D printer~~ anywhere else!
|
6
6
|
|
7
|
-
|
7
|
+
# HTTP Chunked Responses
|
8
|
+
|
9
|
+
These examples will use the Order enumerator to generate chunks of JSON and send them to the client as more chunks are generated. No more than 500 orders will be in memory at a time, regardless of how many orders there are. And only small portions of the JSON will be in memory at once, no matter how much we're generating.
|
8
10
|
|
9
11
|
```ruby
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
number: order.id,
|
14
|
-
desc: order.description,
|
15
|
-
...
|
16
|
-
}
|
17
|
-
}
|
12
|
+
enumerator = Order.
|
13
|
+
where("created_at >= ?", 1.year.ago).
|
14
|
+
find_each(batch_size: 500)
|
18
15
|
```
|
19
16
|
|
20
|
-
**
|
17
|
+
**Rails**
|
21
18
|
|
22
19
|
```ruby
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
load_tons_of_text
|
33
|
-
},
|
34
|
-
|
35
|
-
big_text_2: ->() {
|
36
|
-
load_tons_of_text
|
37
|
-
},
|
38
|
-
})
|
20
|
+
class OrdersController < ApplicationController
|
21
|
+
def index
|
22
|
+
headers["Content-Type"] = "application/json"
|
23
|
+
headers["Last-Modified"] = Time.now.ctime.to_s
|
24
|
+
self.response_body = JsonEmitter.array(enumerator) { |order|
|
25
|
+
order.to_h
|
26
|
+
}
|
27
|
+
end
|
28
|
+
end
|
39
29
|
```
|
40
30
|
|
41
|
-
**
|
31
|
+
**Sinatra**
|
42
32
|
|
43
33
|
```ruby
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
stream.each { |json_chunk|
|
51
|
-
...
|
52
|
-
}
|
34
|
+
get "/orders" do
|
35
|
+
content_type :json
|
36
|
+
JsonEmitter.array(enumerator) { |order|
|
37
|
+
order.to_h
|
38
|
+
}
|
39
|
+
end
|
53
40
|
```
|
54
41
|
|
55
|
-
|
56
|
-
|
57
|
-
In HTTP 1.0 the entire response is normally sent all at once. Usually this is fine, but it can cause problems when very large responses must be generated and sent. These problems usually manifest as spikes in memory usage and/or responses that take so long to send that the client (or an in-between proxy) times out the request.
|
58
|
-
|
59
|
-
The solution to this in HTTP 1.1 is chunked transfer encoding. The response body can be split up and sent in a series of separate "chunks" for the client to receive and automatically put back together. Ruby's Rack specification supports chunking, as do most frameworks based on it (e.g. Rails, Sinatra, Grape, etc).
|
60
|
-
|
61
|
-
The following examples all show the same streaming API in various Rack-based frameworks. Without streaming, the examples could eat up tons of memory, take too long, and time out on the client. With streaming, the following improvements are possible without your client-side code needing any changes:
|
62
|
-
|
63
|
-
1. Only 500 orders will ever be in memory at once.
|
64
|
-
2. Only one `ApiV1::Entities::Order` will ever be in memory at once.
|
65
|
-
3. Only 16kb (roughly) of JSON will ever be in memory at once.
|
66
|
-
5. That 16kb of JSON will be sent to the client while the next 16kb of JSON is generating.
|
67
|
-
|
68
|
-
**IMPORTANT** Not every Ruby application server supports HTTP chunking. Puma definitely supports it and WEBrick definitely does not. Phusion Passenger claims to but I have not tried it.
|
69
|
-
|
70
|
-
## Rails
|
71
|
-
|
72
|
-
TODO
|
73
|
-
|
74
|
-
## Sinatra
|
75
|
-
|
76
|
-
TODO
|
77
|
-
|
78
|
-
## Grape
|
42
|
+
**Grape**
|
79
43
|
|
80
44
|
```ruby
|
81
45
|
get :orders do
|
82
|
-
enumerator = Order.
|
83
|
-
where("created_at >= ?", 1.year.ago).
|
84
|
-
find_each(batch_size: 500)
|
85
|
-
|
86
46
|
stream JsonEmitter.array(enumerator) { |order|
|
87
47
|
ApiV1::Entities::Order.new(order)
|
88
48
|
}
|
89
49
|
end
|
90
50
|
```
|
91
51
|
|
92
|
-
|
52
|
+
**Rack**
|
93
53
|
|
94
54
|
```ruby
|
95
55
|
app = ->(env) {
|
96
|
-
enumerator = Order.
|
97
|
-
where("created_at >= ?", 1.year.ago).
|
98
|
-
find_each(batch_size: 500)
|
99
|
-
|
100
56
|
stream = JsonEmitter.array(enumerator) { |order|
|
101
57
|
order.to_h
|
102
58
|
}
|
103
|
-
|
104
59
|
[200, {"Content-Type" => "application/json"}, stream]
|
105
60
|
}
|
106
61
|
```
|
62
|
+
|
63
|
+
# Other uses
|
64
|
+
|
65
|
+
`JsonEmitter.array` takes an `Enumerable` and returns a stream that generates chunks of JSON.
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
JsonEmitter.array(enumerator).each { |json_chunk|
|
69
|
+
# write json_chunk somewhere
|
70
|
+
}
|
71
|
+
```
|
72
|
+
|
73
|
+
`JsonEmitter.object` takes a `Hash` and returns a stream that generates chunks of JSON.
|
74
|
+
|
75
|
+
```ruby
|
76
|
+
JsonEmitter.object({
|
77
|
+
orders: Order.find_each.lazy.map { |order|
|
78
|
+
{id: order.id, desc: order.description}
|
79
|
+
},
|
80
|
+
|
81
|
+
big_text_1: ->() {
|
82
|
+
load_tons_of_text
|
83
|
+
},
|
84
|
+
|
85
|
+
big_text_2: ->() {
|
86
|
+
load_tons_of_text
|
87
|
+
},
|
88
|
+
}).each { |json_chunk|
|
89
|
+
# write json_chunk somewhere
|
90
|
+
}
|
91
|
+
```
|
92
|
+
|
93
|
+
Streams have a `#write` method for writing directly to a `File` or `IO` object.
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
File.open("~/out.json", "w+") { |f|
|
97
|
+
JsonEmitter.array(enumerator).write f
|
98
|
+
}
|
99
|
+
```
|
data/lib/json-emitter.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'multi_json'
|
2
2
|
|
3
3
|
require 'json-emitter/version'
|
4
|
+
require 'json-emitter/context'
|
4
5
|
require 'json-emitter/emitter'
|
5
6
|
require 'json-emitter/stream'
|
6
7
|
require 'json-emitter/buffered_stream'
|
@@ -24,7 +25,7 @@ module JsonEmitter
|
|
24
25
|
@error_handlers = []
|
25
26
|
|
26
27
|
#
|
27
|
-
# Generates
|
28
|
+
# Generates a stream that will output a JSON array. The input can be any Enumerable, such as an Array or an Enumerator.
|
28
29
|
#
|
29
30
|
# The following example uses minumum memory to genrate a very large JSON array string from an ActiveRecord query.
|
30
31
|
# Only 500 Order records will ever by in memory at once. The JSON will be generated in small chunks so that
|
@@ -49,7 +50,7 @@ module JsonEmitter
|
|
49
50
|
#
|
50
51
|
# @param enum [Enumerable] Something that can be enumerated over, like an Array or Enumerator. Each element should be something that can be rendered as JSON (e.g. a number, string, boolean, Array, or Hash).
|
51
52
|
# @param buffer_size [Integer] The buffer size in kb. This is a size *hint*, not a hard limit.
|
52
|
-
# @param
|
53
|
+
# @param buffer_unit [Symbol] :bytes | :kb (default) | :mb
|
53
54
|
# @yield If a block is given, it will be yielded each value in the array. The return value from the block will be converted to JSON instead of the original value.
|
54
55
|
# @return [JsonEmitter::BufferedStream]
|
55
56
|
#
|
@@ -59,7 +60,7 @@ module JsonEmitter
|
|
59
60
|
end
|
60
61
|
|
61
62
|
#
|
62
|
-
# Generates
|
63
|
+
# Generates a stream that will output a JSON object.
|
63
64
|
#
|
64
65
|
# If some of the values will be large arrays, use Enumerators or lazy Enumerators to build each element on demand
|
65
66
|
# (to potentially save lots of RAM).
|
@@ -93,7 +94,7 @@ module JsonEmitter
|
|
93
94
|
#
|
94
95
|
# @param hash [Hash] Keys should be Strings or Symbols and values should be any JSON-compatible value like a number, string, boolean, Array, or Hash.
|
95
96
|
# @param buffer_size [Integer] The buffer size in kb. This is a size *hint*, not a hard limit.
|
96
|
-
# @param
|
97
|
+
# @param buffer_unit [Symbol] :bytes | :kb (default) | :mb
|
97
98
|
# @return [JsonEmitter::BufferedStream]
|
98
99
|
#
|
99
100
|
def self.object(hash, buffer_size: 16, buffer_unit: :kb)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module JsonEmitter
|
2
|
+
#
|
3
|
+
# By using JsonEmitter.wrap and JsonEmitter.error you can wrap your streams within a special "context".
|
4
|
+
#
|
5
|
+
# This is probably most useful when your stream is being consumed by Rack/Rails/Sinatra/Grape/etc. Your
|
6
|
+
# app is probably depending on certain Rack middlewars to provide before/after behavior and error handling.
|
7
|
+
# All those middlewares will over by the time Rack consumes your stream, but you can use JsonEmitter.wrap
|
8
|
+
# and JsonEmitter.error to add critical behavior back in.
|
9
|
+
#
|
10
|
+
class Context
|
11
|
+
def initialize
|
12
|
+
@wrappers = JsonEmitter.wrappers.map(&:call).compact
|
13
|
+
@error_handlers = JsonEmitter.error_handlers
|
14
|
+
@pass_through_errors = []
|
15
|
+
@pass_through_errors << Puma::ConnectionError if defined? Puma::ConnectionError
|
16
|
+
end
|
17
|
+
|
18
|
+
# Wrap the enumeration in a block. It will be passed a callback which it must call to continue.
|
19
|
+
# TODO better docs and examples.
|
20
|
+
def wrap(&block)
|
21
|
+
if (wrapper = block.call)
|
22
|
+
@wrappers.unshift wrapper
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Add an error handler.
|
27
|
+
# TODO better docs and examples.
|
28
|
+
def error(&handler)
|
29
|
+
@error_handlers += [handler]
|
30
|
+
end
|
31
|
+
|
32
|
+
# Execute a block within this context.
|
33
|
+
def execute(&inner)
|
34
|
+
@wrappers.reduce(inner) { |f, outer_wrapper|
|
35
|
+
->() { outer_wrapper.call(f) }
|
36
|
+
}.call
|
37
|
+
|
38
|
+
rescue *@pass_through_errors => e
|
39
|
+
raise e
|
40
|
+
rescue => e
|
41
|
+
@error_handlers.each { |h| h.call(e) }
|
42
|
+
raise e
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/json-emitter/emitter.rb
CHANGED
@@ -3,11 +3,11 @@ module JsonEmitter
|
|
3
3
|
# Builds Enumerators that yield JSON from Ruby Arrays or Hashes.
|
4
4
|
#
|
5
5
|
class Emitter
|
6
|
+
# @return [JsonEmitter::Context]
|
7
|
+
attr_reader :context
|
8
|
+
|
6
9
|
def initialize
|
7
|
-
@
|
8
|
-
@error_handlers = JsonEmitter.error_handlers
|
9
|
-
@pass_through_errors = []
|
10
|
-
@pass_through_errors << Puma::ConnectionError if defined? Puma::ConnectionError
|
10
|
+
@context = Context.new
|
11
11
|
end
|
12
12
|
|
13
13
|
#
|
@@ -19,7 +19,7 @@ module JsonEmitter
|
|
19
19
|
#
|
20
20
|
def array(enum, &mapper)
|
21
21
|
Enumerator.new { |y|
|
22
|
-
|
22
|
+
context.execute {
|
23
23
|
array_generator(enum, &mapper).each { |json_val|
|
24
24
|
y << json_val
|
25
25
|
}
|
@@ -35,7 +35,7 @@ module JsonEmitter
|
|
35
35
|
#
|
36
36
|
def object(hash)
|
37
37
|
Enumerator.new { |y|
|
38
|
-
|
38
|
+
context.execute {
|
39
39
|
object_generator(hash).each { |json_val|
|
40
40
|
y << json_val
|
41
41
|
}
|
@@ -43,20 +43,6 @@ module JsonEmitter
|
|
43
43
|
}
|
44
44
|
end
|
45
45
|
|
46
|
-
# Wrap the enumeration in a block. It will be passed a callback which it must call to continue.
|
47
|
-
# TODO better docs and examples.
|
48
|
-
def wrap(&block)
|
49
|
-
if (wrapper = block.call)
|
50
|
-
@wrappers.unshift wrapper
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
# Add an error handler.
|
55
|
-
# TODO better docs and examples.
|
56
|
-
def error(&handler)
|
57
|
-
@error_handlers += [handler]
|
58
|
-
end
|
59
|
-
|
60
46
|
private
|
61
47
|
|
62
48
|
def array_generator(enum, &mapper)
|
@@ -112,17 +98,5 @@ module JsonEmitter
|
|
112
98
|
[MultiJson.dump(x)]
|
113
99
|
end
|
114
100
|
end
|
115
|
-
|
116
|
-
def wrapped(&final)
|
117
|
-
@wrappers.reduce(final) { |f, outer_wrapper|
|
118
|
-
->() { outer_wrapper.call(f) }
|
119
|
-
}.call
|
120
|
-
|
121
|
-
rescue *@pass_through_errors => e
|
122
|
-
raise e
|
123
|
-
rescue => e
|
124
|
-
@error_handlers.each { |h| h.call(e) }
|
125
|
-
raise e
|
126
|
-
end
|
127
101
|
end
|
128
102
|
end
|
data/lib/json-emitter/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json-emitter
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jordan Hollinger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: multi_json
|
@@ -33,6 +33,7 @@ files:
|
|
33
33
|
- README.md
|
34
34
|
- lib/json-emitter.rb
|
35
35
|
- lib/json-emitter/buffered_stream.rb
|
36
|
+
- lib/json-emitter/context.rb
|
36
37
|
- lib/json-emitter/emitter.rb
|
37
38
|
- lib/json-emitter/stream.rb
|
38
39
|
- lib/json-emitter/version.rb
|