xchan.rb 0.16.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,347 @@
1
+ require 'delegate'
2
+ require 'tmpdir'
3
+
4
+ ##
5
+ # {Chan::Tempfile Chan::Tempfile} is a fork of Tempfile from
6
+ # Ruby's standard library. The primary difference between
7
+ # {Chan::Tempfile Chan::Tempfile}, and the standard library is
8
+ # that [ruby/tempfile#22](https://github.com/ruby/tempfile/pull/22)
9
+ # is applied in this fork.
10
+ class Chan::Tempfile < DelegateClass(File)
11
+ VERSION = "0.1.3"
12
+
13
+ # Creates a file in the underlying file system;
14
+ # returns a new \Tempfile object based on that file.
15
+ #
16
+ # If possible, consider instead using Tempfile.create, which:
17
+ #
18
+ # - Avoids the performance cost of delegation,
19
+ # incurred when Tempfile.new calls its superclass <tt>DelegateClass(File)</tt>.
20
+ # - Does not rely on a finalizer to close and unlink the file,
21
+ # which can be unreliable.
22
+ #
23
+ # Creates and returns file whose:
24
+ #
25
+ # - Class is \Tempfile (not \File, as in Tempfile.create).
26
+ # - Directory is the system temporary directory (system-dependent).
27
+ # - Generated filename is unique in that directory.
28
+ # - Permissions are <tt>0600</tt>;
29
+ # see {File Permissions}[https://docs.ruby-lang.org/en/master/File.html#label-File+Permissions].
30
+ # - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end).
31
+ #
32
+ # The underlying file is removed when the \Tempfile object dies
33
+ # and is reclaimed by the garbage collector.
34
+ #
35
+ # Example:
36
+ #
37
+ # f = Tempfile.new # => #<Tempfile:/tmp/20220505-17839-1s0kt30>
38
+ # f.class # => Tempfile
39
+ # f.path # => "/tmp/20220505-17839-1s0kt30"
40
+ # f.stat.mode.to_s(8) # => "100600"
41
+ # File.exist?(f.path) # => true
42
+ # File.unlink(f.path) #
43
+ # File.exist?(f.path) # => false
44
+ #
45
+ # Argument +basename+, if given, may be one of:
46
+ #
47
+ # - A string: the generated filename begins with +basename+:
48
+ #
49
+ # Tempfile.new('foo') # => #<Tempfile:/tmp/foo20220505-17839-1whk2f>
50
+ #
51
+ # - An array of two strings <tt>[prefix, suffix]</tt>:
52
+ # the generated filename begins with +prefix+ and ends with +suffix+:
53
+ #
54
+ # Tempfile.new(%w/foo .jpg/) # => #<Tempfile:/tmp/foo20220505-17839-58xtfi.jpg>
55
+ #
56
+ # With arguments +basename+ and +tmpdir+, the file is created in directory +tmpdir+:
57
+ #
58
+ # Tempfile.new('foo', '.') # => #<Tempfile:./foo20220505-17839-xfstr8>
59
+ #
60
+ # Keyword arguments +mode+ and +options+ are passed directly to method
61
+ # {File.open}[https://docs.ruby-lang.org/en/master/File.html#method-c-open]:
62
+ #
63
+ # - The value given with +mode+ must be an integer,
64
+ # and may be expressed as the logical OR of constants defined in
65
+ # {File::Constants}[https://docs.ruby-lang.org/en/master/File/Constants.html].
66
+ # - For +options+, see {Open Options}[https://docs.ruby-lang.org/en/master/IO.html#class-IO-label-Open+Options].
67
+ #
68
+ # Related: Tempfile.create.
69
+ #
70
+ def initialize(basename="", tmpdir=nil, mode: 0, perm: 0600, **options)
71
+ warn "Tempfile.new doesn't call the given block.", uplevel: 1 if block_given?
72
+
73
+ @unlinked = false
74
+ @mode = mode|File::RDWR|File::CREAT|File::EXCL
75
+ ::Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
76
+ @tmpfile = File.open(tmpname, @mode, perm, **opts)
77
+ @perm = perm
78
+ @opts = opts.freeze
79
+ end
80
+ ObjectSpace.define_finalizer(self, Remover.new(@tmpfile))
81
+
82
+ super(@tmpfile)
83
+ end
84
+
85
+ # Opens or reopens the file with mode "r+".
86
+ def open
87
+ _close
88
+ mode = @mode & ~(File::CREAT|File::EXCL)
89
+ @tmpfile = File.open(@tmpfile.path, mode, **@opts)
90
+ __setobj__(@tmpfile)
91
+ end
92
+
93
+ def _close # :nodoc:
94
+ @tmpfile.close
95
+ end
96
+ protected :_close
97
+
98
+ # Closes the file. If +unlink_now+ is true, then the file will be unlinked
99
+ # (deleted) after closing. Of course, you can choose to later call #unlink
100
+ # if you do not unlink it now.
101
+ #
102
+ # If you don't explicitly unlink the temporary file, the removal
103
+ # will be delayed until the object is finalized.
104
+ def close(unlink_now=false)
105
+ _close
106
+ unlink if unlink_now
107
+ end
108
+
109
+ # Closes and unlinks (deletes) the file. Has the same effect as called
110
+ # <tt>close(true)</tt>.
111
+ def close!
112
+ close(true)
113
+ end
114
+
115
+ # Unlinks (deletes) the file from the filesystem. One should always unlink
116
+ # the file after using it, as is explained in the "Explicit close" good
117
+ # practice section in the Tempfile overview:
118
+ #
119
+ # file = Tempfile.new('foo')
120
+ # begin
121
+ # # ...do something with file...
122
+ # ensure
123
+ # file.close
124
+ # file.unlink # deletes the temp file
125
+ # end
126
+ #
127
+ # === Unlink-before-close
128
+ #
129
+ # On POSIX systems it's possible to unlink a file before closing it. This
130
+ # practice is explained in detail in the Tempfile overview (section
131
+ # "Unlink after creation"); please refer there for more information.
132
+ #
133
+ # However, unlink-before-close may not be supported on non-POSIX operating
134
+ # systems. Microsoft Windows is the most notable case: unlinking a non-closed
135
+ # file will result in an error, which this method will silently ignore. If
136
+ # you want to practice unlink-before-close whenever possible, then you should
137
+ # write code like this:
138
+ #
139
+ # file = Tempfile.new('foo')
140
+ # file.unlink # On Windows this silently fails.
141
+ # begin
142
+ # # ... do something with file ...
143
+ # ensure
144
+ # file.close! # Closes the file handle. If the file wasn't unlinked
145
+ # # because #unlink failed, then this method will attempt
146
+ # # to do so again.
147
+ # end
148
+ def unlink
149
+ return if @unlinked
150
+ begin
151
+ File.unlink(@tmpfile.path)
152
+ rescue Errno::ENOENT
153
+ rescue Errno::EACCES
154
+ # may not be able to unlink on Windows; just ignore
155
+ return
156
+ end
157
+ ObjectSpace.undefine_finalizer(self)
158
+ @unlinked = true
159
+ end
160
+ alias delete unlink
161
+
162
+ # Returns the full path name of the temporary file.
163
+ # This will be nil if #unlink has been called.
164
+ def path
165
+ @unlinked ? nil : @tmpfile.path
166
+ end
167
+
168
+ # Returns the size of the temporary file. As a side effect, the IO
169
+ # buffer is flushed before determining the size.
170
+ def size
171
+ if !@tmpfile.closed?
172
+ @tmpfile.size # File#size calls rb_io_flush_raw()
173
+ else
174
+ File.size(@tmpfile.path)
175
+ end
176
+ end
177
+ alias length size
178
+
179
+ # :stopdoc:
180
+ def inspect
181
+ if @tmpfile.closed?
182
+ "#<#{self.class}:#{path} (closed)>"
183
+ else
184
+ "#<#{self.class}:#{path}>"
185
+ end
186
+ end
187
+
188
+ class Remover # :nodoc:
189
+ def initialize(tmpfile)
190
+ @pid = Process.pid
191
+ @tmpfile = tmpfile
192
+ end
193
+
194
+ def call(*args)
195
+ return if @pid != Process.pid
196
+
197
+ $stderr.puts "removing #{@tmpfile.path}..." if $DEBUG
198
+
199
+ @tmpfile.close
200
+ begin
201
+ File.unlink(@tmpfile.path)
202
+ rescue Errno::ENOENT
203
+ end
204
+
205
+ $stderr.puts "done" if $DEBUG
206
+ end
207
+ end
208
+
209
+ class << self
210
+ # :startdoc:
211
+ # Creates a new Tempfile.
212
+ #
213
+ # This method is not recommended and exists mostly for backward compatibility.
214
+ # Please use Tempfile.create instead, which avoids the cost of delegation,
215
+ # does not rely on a finalizer, and also unlinks the file when given a block.
216
+ #
217
+ # Tempfile.open is still appropriate if you need the Tempfile to be unlinked
218
+ # by a finalizer and you cannot explicitly know where in the program the
219
+ # Tempfile can be unlinked safely.
220
+ #
221
+ # If no block is given, this is a synonym for Tempfile.new.
222
+ #
223
+ # If a block is given, then a Tempfile object will be constructed,
224
+ # and the block is run with the Tempfile object as argument. The Tempfile
225
+ # object will be automatically closed after the block terminates.
226
+ # However, the file will *not* be unlinked and needs to be manually unlinked
227
+ # with Tempfile#close! or Tempfile#unlink. The finalizer will try to unlink
228
+ # but should not be relied upon as it can keep the file on the disk much
229
+ # longer than intended. For instance, on CRuby, finalizers can be delayed
230
+ # due to conservative stack scanning and references left in unused memory.
231
+ #
232
+ # The call returns the value of the block.
233
+ #
234
+ # In any case, all arguments (<code>*args</code>) will be passed to Tempfile.new.
235
+ #
236
+ # Tempfile.open('foo', '/home/temp') do |f|
237
+ # # ... do something with f ...
238
+ # end
239
+ #
240
+ # # Equivalent:
241
+ # f = Tempfile.open('foo', '/home/temp')
242
+ # begin
243
+ # # ... do something with f ...
244
+ # ensure
245
+ # f.close
246
+ # end
247
+ def self.open(*args, **kw)
248
+ tempfile = new(*args, **kw)
249
+
250
+ if block_given?
251
+ begin
252
+ yield(tempfile)
253
+ ensure
254
+ tempfile.close
255
+ end
256
+ else
257
+ tempfile
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ # Creates a file in the underlying file system;
264
+ # returns a new \File object based on that file.
265
+ #
266
+ # With no block given and no arguments, creates and returns file whose:
267
+ #
268
+ # - Class is {File}[https://docs.ruby-lang.org/en/master/File.html] (not \Tempfile).
269
+ # - Directory is the system temporary directory (system-dependent).
270
+ # - Generated filename is unique in that directory.
271
+ # - Permissions are <tt>0600</tt>;
272
+ # see {File Permissions}[https://docs.ruby-lang.org/en/master/File.html#label-File+Permissions].
273
+ # - Mode is <tt>'w+'</tt> (read/write mode, positioned at the end).
274
+ #
275
+ # With no block, the file is not removed automatically,
276
+ # and so should be explicitly removed.
277
+ #
278
+ # Example:
279
+ #
280
+ # f = Tempfile.create # => #<File:/tmp/20220505-9795-17ky6f6>
281
+ # f.class # => File
282
+ # f.path # => "/tmp/20220505-9795-17ky6f6"
283
+ # f.stat.mode.to_s(8) # => "100600"
284
+ # File.exist?(f.path) # => true
285
+ # File.unlink(f.path)
286
+ # File.exist?(f.path) # => false
287
+ #
288
+ # Argument +basename+, if given, may be one of:
289
+ #
290
+ # - A string: the generated filename begins with +basename+:
291
+ #
292
+ # Tempfile.create('foo') # => #<File:/tmp/foo20220505-9795-1gok8l9>
293
+ #
294
+ # - An array of two strings <tt>[prefix, suffix]</tt>:
295
+ # the generated filename begins with +prefix+ and ends with +suffix+:
296
+ #
297
+ # Tempfile.create(%w/foo .jpg/) # => #<File:/tmp/foo20220505-17839-tnjchh.jpg>
298
+ #
299
+ # With arguments +basename+ and +tmpdir+, the file is created in directory +tmpdir+:
300
+ #
301
+ # Tempfile.create('foo', '.') # => #<File:./foo20220505-9795-1emu6g8>
302
+ #
303
+ # Keyword arguments +mode+ and +options+ are passed directly to method
304
+ # {File.open}[https://docs.ruby-lang.org/en/master/File.html#method-c-open]:
305
+ #
306
+ # - The value given with +mode+ must be an integer,
307
+ # and may be expressed as the logical OR of constants defined in
308
+ # {File::Constants}[https://docs.ruby-lang.org/en/master/File/Constants.html].
309
+ # - For +options+, see {Open Options}[https://docs.ruby-lang.org/en/master/IO.html#class-IO-label-Open+Options].
310
+ #
311
+ # With a block given, creates the file as above, passes it to the block,
312
+ # and returns the block's value;
313
+ # before the return, the file object is closed and the underlying file is removed:
314
+ #
315
+ # Tempfile.create {|file| file.path } # => "/tmp/20220505-9795-rkists"
316
+ #
317
+ # Related: Tempfile.new.
318
+ #
319
+ module Chan
320
+ def Tempfile.create(basename="", tmpdir=nil, mode: 0, perm: 0600, **options)
321
+ tmpfile = nil
322
+ Dir::Tmpname.create(basename, tmpdir, **options) do |tmpname, n, opts|
323
+ mode |= File::RDWR|File::CREAT|File::EXCL
324
+ tmpfile = File.open(tmpname, mode, perm, **opts)
325
+ end
326
+ if block_given?
327
+ begin
328
+ yield tmpfile
329
+ ensure
330
+ unless tmpfile.closed?
331
+ if File.identical?(tmpfile, tmpfile.path)
332
+ unlinked = File.unlink tmpfile.path rescue nil
333
+ end
334
+ tmpfile.close
335
+ end
336
+ unless unlinked
337
+ begin
338
+ File.unlink tmpfile.path
339
+ rescue Errno::ENOENT
340
+ end
341
+ end
342
+ end
343
+ else
344
+ tmpfile
345
+ end
346
+ end
347
+ end
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # The {Chan::UNIXSocket Chan::UNIXSocket} class implements a channel
5
+ # for interprocess communication (IPC) using an unnamed UNIXSocket.
6
+ class Chan::UNIXSocket
7
+ require "socket"
8
+ require "lockf"
9
+ require_relative "bytes"
10
+
11
+ ##
12
+ # @example
13
+ # ch = Chan::UNIXSocket.new(:marshal)
14
+ # ch.send([1,2,3])
15
+ # ch.recv.pop # => 3
16
+ # ch.close
17
+ #
18
+ # @param [Symbol, <#dump, #load>] serializer
19
+ # A serializer.
20
+ #
21
+ # @param [Integer] socket
22
+ # A socket type (eg Socket::SOCK_STREAM).
23
+ #
24
+ # @param [String] tmpdir
25
+ # Path to a directory where temporary files will be stored.
26
+ #
27
+ # @return [Chan::UNIXSocket]
28
+ # Returns an instance of {Chan::UNIXSocket Chan::UNIXSocket}.
29
+ def initialize(serializer, tmpdir: Dir.tmpdir, socket: Socket::SOCK_DGRAM)
30
+ @serializer = Chan.serializers[serializer]&.call || serializer
31
+ @r, @w = ::UNIXSocket.pair(socket)
32
+ @bytes = Chan::Bytes.new(tmpdir)
33
+ @lock = LockFile.new Chan.temporary_file("xchan.lock", tmpdir:)
34
+ end
35
+
36
+ ##
37
+ # @return [<#dump, #load>]
38
+ # Returns the serializer used by the channel.
39
+ def serializer
40
+ @serializer
41
+ end
42
+
43
+ ##
44
+ # @return [Boolean]
45
+ # Returns true when the channel is closed.
46
+ def closed?
47
+ @r.closed? and @w.closed?
48
+ end
49
+
50
+ ##
51
+ # Closes the channel.
52
+ #
53
+ # @raise [IOError]
54
+ # When the channel is closed.
55
+ #
56
+ # @return [void]
57
+ def close
58
+ @lock.lock
59
+ raise IOError, "channel is closed" if closed?
60
+ [@r, @w, @bytes, @lock.file].each(&:close)
61
+ rescue IOError => ex
62
+ @lock.release
63
+ raise(ex)
64
+ end
65
+
66
+ ##
67
+ # @group Write methods
68
+
69
+ ##
70
+ # Performs a blocking write
71
+ #
72
+ # @param [Object] object
73
+ # An object to write to the channel.
74
+ #
75
+ # @raise [IOError]
76
+ # When the channel is closed.
77
+ #
78
+ # @return [Object]
79
+ # Returns the number of bytes written to the channel.
80
+ def send(object)
81
+ send_nonblock(object)
82
+ rescue Chan::WaitWritable, Chan::WaitLockable
83
+ retry
84
+ end
85
+ alias_method :write, :send
86
+
87
+ ##
88
+ # Performs a non-blocking write
89
+ #
90
+ # @param [Object] object
91
+ # An object to write to the channel.
92
+ #
93
+ # @raise [IOError]
94
+ # When the channel is closed.
95
+ #
96
+ # @raise [Chan::WaitWritable]
97
+ # When a write to the underlying IO blocks.
98
+ #
99
+ # @raise [Chan::WaitLockable]
100
+ # When a write blocks because of a lock held by another process.
101
+ #
102
+ # @return [Integer, nil]
103
+ # Returns the number of bytes written to the channel.
104
+ def send_nonblock(object)
105
+ @lock.lock_nonblock
106
+ raise IOError, "channel closed" if closed?
107
+ len = @w.write_nonblock(serialize(object))
108
+ @bytes.push(len)
109
+ len.tap { @lock.release }
110
+ rescue IOError, IO::WaitWritable, Errno::ENOBUFS => ex
111
+ @lock.release
112
+ raise Chan::WaitWritable, ex.message
113
+ rescue Errno::EWOULDBLOCK => ex
114
+ raise Chan::WaitLockable, ex.message
115
+ end
116
+ alias_method :write_nonblock, :send_nonblock
117
+
118
+ ##
119
+ # @endgroup
120
+
121
+ ##
122
+ # @group Read methods
123
+
124
+ ##
125
+ # Performs a blocking read
126
+ #
127
+ # @raise [IOError]
128
+ # When the channel is closed.
129
+ #
130
+ # @return [Object]
131
+ # Returns an object from the channel.
132
+ def recv
133
+ recv_nonblock
134
+ rescue Chan::WaitReadable
135
+ wait_readable
136
+ retry
137
+ rescue Chan::WaitLockable
138
+ retry
139
+ end
140
+ alias_method :read, :recv
141
+
142
+ ##
143
+ # Performs a non-blocking read
144
+ #
145
+ # @raise [IOError]
146
+ # When the channel is closed.
147
+ #
148
+ # @raise [Chan::WaitReadable]
149
+ # When a read from the underlying IO blocks.
150
+ #
151
+ # @raise [Chan::WaitLockable]
152
+ # When a read blocks because of a lock held by another process.
153
+ #
154
+ # @return [Object]
155
+ # Returns an object from the channel.
156
+ def recv_nonblock
157
+ @lock.lock_nonblock
158
+ raise IOError, "closed channel" if closed?
159
+ len = @bytes.shift
160
+ obj = deserialize(@r.read_nonblock(len.zero? ? 1 : len))
161
+ obj.tap { @lock.release }
162
+ rescue IOError => ex
163
+ @lock.release
164
+ raise(ex)
165
+ rescue IO::WaitReadable => ex
166
+ @bytes.unshift(len)
167
+ @lock.release
168
+ raise Chan::WaitReadable, ex.message
169
+ rescue Errno::EAGAIN => ex
170
+ raise Chan::WaitLockable, ex.message
171
+ end
172
+ alias_method :read_nonblock, :recv_nonblock
173
+
174
+ ##
175
+ # @endgroup
176
+
177
+ ##
178
+ # @example
179
+ # ch = xchan
180
+ # 1.upto(4) { ch.send(_1) }
181
+ # ch.to_a.last # => 4
182
+ #
183
+ # @return [Array<Object>]
184
+ # Returns the consumed contents of the channel.
185
+ def to_a
186
+ lock do
187
+ [].tap { _1.push(recv) until empty? }
188
+ end
189
+ end
190
+
191
+ ##
192
+ # @return [Boolean]
193
+ # Returns true when the channel is empty.
194
+ def empty?
195
+ return true if closed?
196
+ lock { size.zero? }
197
+ end
198
+
199
+ ##
200
+ # @group Size methods
201
+
202
+ ##
203
+ # @return [Integer]
204
+ # Returns the total number of bytes written to the channel.
205
+ def bytes_sent
206
+ lock { @bytes.stat.bytes_written }
207
+ end
208
+ alias_method :bytes_written, :bytes_sent
209
+
210
+ ##
211
+ # @return [Integer]
212
+ # Returns the total number of bytes read from the channel.
213
+ def bytes_received
214
+ lock { @bytes.stat.bytes_read }
215
+ end
216
+ alias_method :bytes_read, :bytes_received
217
+
218
+ ##
219
+ # @return [Integer]
220
+ # Returns the number of objects waiting to be read.
221
+ def size
222
+ lock { @bytes.size }
223
+ end
224
+
225
+ ##
226
+ # @endgroup
227
+
228
+ ##
229
+ # @group Wait methods
230
+
231
+ ##
232
+ # Waits for the channel to become readable.
233
+ #
234
+ # @param [Float, Integer, nil] s
235
+ # The number of seconds to wait. Waits indefinitely when "nil".
236
+ #
237
+ # @return [Chan::UNIXSocket, nil]
238
+ # Returns self when the channel is readable, otherwise returns nil.
239
+ def wait_readable(s = nil)
240
+ @r.wait_readable(s) and self
241
+ end
242
+
243
+ ##
244
+ # Waits for the channel to become writable.
245
+ #
246
+ # @param [Float, Integer, nil] s
247
+ # The number of seconds to wait. Waits indefinitely when "nil".
248
+ #
249
+ # @return [Chan::UNIXSocket, nil]
250
+ # Returns self when the channel is writable, otherwise returns nil.
251
+ def wait_writable(s = nil)
252
+ @w.wait_writable(s) and self
253
+ end
254
+
255
+ ##
256
+ # @endgroup
257
+
258
+ ##
259
+ # @group Socket options
260
+
261
+ ##
262
+ # @param [String, Symbol] target
263
+ # `:reader`, or `:writer`.
264
+ #
265
+ # @param [Integer] level
266
+ # The level (eg `Socket::SOL_SOCKET` for the socket level).
267
+ #
268
+ # @param [Integer] option_name
269
+ # The name of an option (eg `Socket::SO_RCVBUF`).
270
+ #
271
+ # @param [Boolean, Integer] option_value
272
+ # The option value (eg 12345)
273
+ #
274
+ # @return [Integer]
275
+ # Returns 0 on success.
276
+ def setsockopt(target, level, option_name, option_value)
277
+ @lock.lock
278
+ if !%w[reader writer].include?(target.to_s)
279
+ raise ArgumentError, "target can be ':reader', or ':writer'"
280
+ end
281
+ target = (target == :reader) ? @r : @w
282
+ target.setsockopt(level, option_name, option_value)
283
+ ensure
284
+ @lock.release
285
+ end
286
+
287
+ ##
288
+ # @param [String, Symbol] target
289
+ # `:reader`, or `:writer`.
290
+ #
291
+ # @param [Integer] level
292
+ # The level (eg `Socket::SOL_SOCKET` for the socket level).
293
+ #
294
+ # @param [Integer] option_name
295
+ # The name of an option (eg `Socket::SO_RCVBUF`).
296
+ #
297
+ # @return [Socket::Option]
298
+ # Returns an instance of `Socket::Option`.
299
+ def getsockopt(target, level, option_name)
300
+ @lock.lock
301
+ if !%w[reader writer].include?(target.to_s)
302
+ raise ArgumentError, "target can be ':reader', or ':writer'"
303
+ end
304
+ target = (target == :reader) ? @r : @w
305
+ target.getsockopt(level, option_name)
306
+ ensure
307
+ @lock.release
308
+ end
309
+
310
+ ##
311
+ # @endgroup
312
+
313
+ private
314
+
315
+ def lock
316
+ @lock.lock
317
+ yield
318
+ ensure
319
+ @lock.release
320
+ end
321
+
322
+ def serialize(obj)
323
+ @serializer.dump(obj)
324
+ end
325
+
326
+ def deserialize(str)
327
+ @serializer.load(str)
328
+ end
329
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Chan
4
+ VERSION = "0.16.4"
5
+ end