cosmos 5.0.4 → 5.0.5

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.
@@ -32,12 +32,19 @@ module Cosmos
32
32
  # @return [String] Table configuration filename
33
33
  attr_reader :filename
34
34
 
35
+ def self.process_file(filename)
36
+ instance = self.new()
37
+ instance.process_file(filename)
38
+ instance
39
+ end
40
+
35
41
  # Create the table configuration
36
42
  def initialize
37
43
  super
38
-
39
44
  # Override commands with the Table::TARGET name to store tables
40
45
  @commands[Table::TARGET] = {}
46
+ @definitions = {}
47
+ @last_config = [] # Stores array of [filename, contents]
41
48
  end
42
49
 
43
50
  # @return [Array<Table>] All tables defined in the configuration file
@@ -45,6 +52,11 @@ module Cosmos
45
52
  @commands[Table::TARGET]
46
53
  end
47
54
 
55
+ # @return [String] Table definition for the specific table
56
+ def definition(table_name)
57
+ @definitions[table_name.upcase]
58
+ end
59
+
48
60
  # @return [Array<String>] All the table names
49
61
  def table_names
50
62
  tables.keys
@@ -64,6 +76,7 @@ module Cosmos
64
76
  # Partial files are included into another file and thus aren't directly processed
65
77
  return if File.basename(filename)[0] == '_' # Partials start with underscore
66
78
  @filename = filename
79
+ @last_config = [File.basename(filename), File.read(filename)]
67
80
  @converted_type = nil
68
81
  @converted_bit_size = nil
69
82
  @proc_text = ''
@@ -111,10 +124,12 @@ module Cosmos
111
124
  )
112
125
  end
113
126
  process_file(table_filename)
127
+
114
128
  when 'TABLE'
115
129
  finish_packet
116
130
  @current_packet =
117
131
  TableParser.parse_table(parser, @commands, @warnings)
132
+ @definitions[@current_packet.packet_name] = @last_config
118
133
  @current_cmd_or_tlm = COMMAND
119
134
  @default_index = 0
120
135
 
@@ -31,50 +31,111 @@ module Cosmos
31
31
  class MismatchError < CoreError
32
32
  end
33
33
 
34
- # Raised when there is no current table configuration
35
- class NoConfigError < CoreError
36
- # @return [String] Error message
37
- def message
38
- 'No current table configuration'
39
- end
34
+ def self.binary(binary, definition_filename, table_name)
35
+ config = TableConfig.process_file(definition_filename)
36
+ load_binary(config, binary)
37
+ return config.table(table_name).buffer
40
38
  end
41
39
 
42
- # Raised when there is no table in the current configuration
43
- class NoTableError < CoreError
44
- # @return [String] Error message
45
- def message
46
- 'Table does not exist in current configuration'
47
- end
40
+ def self.definition(definition_filename, table_name)
41
+ config = TableConfig.process_file(definition_filename)
42
+ return config.definition(table_name) # This returns an array: [filename, contents]
48
43
  end
49
44
 
50
- # @return [TableConfig] Configuration instance
51
- attr_reader :config
45
+ def self.report(binary, definition_filename, requested_table_name = nil)
46
+ report = StringIO.new
47
+ config = TableConfig.process_file(definition_filename)
48
+ begin
49
+ load_binary(config, binary)
50
+ rescue CoreError => err
51
+ report.puts "Error: #{err.message}\n"
52
+ end
53
+
54
+ config.tables.each do |table_name, table|
55
+ next if requested_table_name && table_name != requested_table_name
56
+ items = table.sorted_items
57
+ report.puts(table.table_name)
52
58
 
53
- # Create the instance
54
- def initialize
55
- reset
59
+ # Write the column headers
60
+ if table.type == :ROW_COLUMN
61
+ columns = ['Item']
62
+
63
+ # Remove the '0' from the 'itemname0'
64
+ table.num_columns.times.each do |x|
65
+ columns << items[x].name[0...-1]
66
+ end
67
+ report.puts columns.join(', ')
68
+ else
69
+ report.puts 'Label, Value'
70
+ end
71
+
72
+ # Write the table item values
73
+ (0...table.num_rows).each do |r|
74
+ if table.type == :ROW_COLUMN
75
+ rowtext = "#{r + 1}"
76
+ else
77
+ rowtext = items[r].name
78
+ end
79
+
80
+ report.write "#{rowtext}, "
81
+ (0...table.num_columns).each do |c|
82
+ if table.type == :ROW_COLUMN
83
+ table_item = items[c + r * table.num_columns]
84
+ else
85
+ table_item = items[r]
86
+ end
87
+ value = table.read(table_item.name, :FORMATTED)
88
+ if value.is_printable?
89
+ report.write "#{value}, "
90
+ else
91
+ report.write "#{value.simple_formatted}, "
92
+ end
93
+ end
94
+ report.write("\n") # newline after each row
95
+ end
96
+ report.write("\n") # newline after each table
97
+ end
98
+ report.string
56
99
  end
57
100
 
58
- # Clears the configuration
59
- def reset
60
- @config = nil
101
+ def self.generate(definition_filename)
102
+ config = TableConfig.process_file(definition_filename)
103
+ binary = ''
104
+ config.tables.each do |table_name, table|
105
+ table.restore_defaults
106
+ binary += table.buffer
107
+ end
108
+ binary
61
109
  end
62
110
 
63
- # @param filename [String] Create a new TableConfig instance and process the filename
64
- def process_definition(filename)
65
- @config = TableConfig.new
66
- @config.process_file(filename)
111
+ def self.save(definition_filename, tables)
112
+ config = TableConfig.process_file(definition_filename)
113
+ tables.each do |table|
114
+ table_def = config.tables[table['name']]
115
+ table['rows'].each do |row|
116
+ row.each do |item|
117
+ # TODO: I don't know how the frontend could edit an item like this:
118
+ # item:{"name"=>"BINARY", "value"=>{"json_class"=>"String", "raw"=>[222, 173, 190, 239]} }
119
+ next if item['value'].is_a? Hash
120
+ table_def.write(item['name'], item['value'])
121
+ end
122
+ end
123
+ end
124
+ binary = ''
125
+ config.tables.each { |table_name, table| binary += table.buffer }
126
+ binary
67
127
  end
68
128
 
69
- def generate_json(bin_path, def_path)
129
+ def self.build_json(binary, definition_filename)
130
+ config = TableConfig.process_file(definition_filename)
70
131
  tables = []
71
132
  json = { tables: tables }
72
133
  begin
73
- file_open(bin_path, def_path)
134
+ load_binary(config, binary)
74
135
  rescue CoreError => err
75
136
  json['errors'] = err.message
76
137
  end
77
- @config.tables.each do |table_name, table|
138
+ config.tables.each do |table_name, table|
78
139
  tables << {
79
140
  name: table_name,
80
141
  numRows: table.num_rows,
@@ -129,301 +190,144 @@ module Cosmos
129
190
  json.to_json
130
191
  end
131
192
 
132
- def save_tables(bin_path, def_path, tables)
133
- file_open(bin_path, def_path)
134
- tables.each do |table|
135
- table_def = @config.tables[table['name']]
136
- table['rows'].each do |row|
137
- row.each do |item|
138
- # TODO: I don't know how the frontend could edit an item like this:
139
- # item:{"name"=>"BINARY", "value"=>{"json_class"=>"String", "raw"=>[222, 173, 190, 239]} }
140
- next if item['value'].is_a? Hash
141
- table_def.write(item['name'], item['value'])
142
- end
143
- end
144
- end
145
- file_save(bin_path)
146
- bin_path
147
- end
148
-
149
- # @param def_path [String] Definition file to process
150
- # @param output_dir [String] Output directory to create the new file
151
- # @return [String] Binary file path
152
- def file_new(def_path, output_dir)
153
- process_definition(def_path)
154
- @config.table_names.each do |table_name|
155
- set_binary_data_to_default(table_name)
156
- end
157
- bin_path = File.join(output_dir, def_to_bin_filename(def_path))
158
- file_save(bin_path)
159
- bin_path
160
- end
161
-
162
- # @param bin_path [String] Binary file to open
163
- # @param def_path [String] Definition file to use when opening
164
- def file_open(bin_path, def_path)
165
- process_definition(def_path)
166
- open_and_load_binary_file(bin_path)
167
- end
168
-
169
- # Saves the current tables in the config instance to the given filename.
170
- #
171
- # @param filename [String] Filename to write, overwritten if it exists.
172
- def file_save(filename)
173
- raise NoConfigError unless @config
174
- file_check
175
- File.open(filename, 'wb') do |file|
176
- @config.tables.each { |table_name, table| file.write(table.buffer) }
177
- end
178
- # file_report(filename, @config.filename)
179
- end
180
-
181
- # @return [String] Success string if parameters all check. Raises
182
- # a CoreError if errors are found.
183
- def file_check
184
- raise NoConfigError unless @config
185
- result = ''
186
- @config.table_names.each do |name|
187
- table_result = table_check(name)
188
- unless table_result.empty?
189
- result << "Errors in #{name}:\n" + table_result
190
- end
191
- end
192
- raise CoreError, result unless result.empty?
193
- 'All parameters are within their constraints.'
194
- end
195
-
196
- # Create a CSV report file based on the file contents.
197
- #
198
- # @param bin_path [String] Binary filename currently open. Used to generate the
199
- # report name such that it matches the binary filename.
200
- # @param def_path [String] Definition filename currently open
201
- # @return [String] Report filename path
202
- def file_report(bin_path, def_path)
203
- raise NoConfigError unless @config
204
- file_check
205
-
206
- basename = File.basename(bin_path, '.bin')
207
- report_path = File.join(File.dirname(bin_path), "#{basename}.csv")
208
- File.open(report_path, 'w+') do |file|
209
- file.write("File Definition, #{def_path}\n")
210
- file.write("File Binary, #{bin_path}\n\n")
211
- @config.tables.values.each do |table|
212
- items = table.sorted_items
213
- file.puts(table.table_name)
214
-
215
- # Write the column headers
216
- if table.type == :ROW_COLUMN
217
- columns = ['Item']
218
-
219
- # Remove the '0' from the 'itemname0'
220
- table.num_columns.times.each do |x|
221
- columns << items[x].name[0...-1]
222
- end
223
- file.puts columns.join(', ')
224
- else
225
- file.puts 'Label, Value'
226
- end
227
-
228
- # Write the table item values
229
- (0...table.num_rows).each do |r|
230
- if table.type == :ROW_COLUMN
231
- rowtext = "#{r + 1}"
232
- else
233
- rowtext = items[r].name
234
- end
235
-
236
- file.write "#{rowtext}, "
237
- (0...table.num_columns).each do |c|
238
- if table.type == :ROW_COLUMN
239
- table_item = items[c + r * table.num_columns]
240
- else
241
- table_item = items[r]
242
- end
243
-
244
- file.write "#{table.read(table_item.name, :FORMATTED).to_s}, "
245
- end
246
- file.write("\n") # newline after each row
247
- end
248
- file.write("\n") # newline after each table
249
- end
250
- end
251
- report_path
252
- end
253
-
254
- # Create a hex formatted string of all the file data
255
- def file_hex
256
- raise NoConfigError unless @config
257
- data = ''
258
- @config.tables.values.each { |table| data << table.buffer }
259
- "#{data.formatted}\n\nTotal Bytes Read: #{data.length}"
260
- end
261
-
262
- # @param table_name [String] Name of the table to check for out of range values
263
- def table_check(table_name)
264
- raise NoConfigError unless @config
265
- table = @config.table(table_name)
266
- raise NoTableError unless table
267
-
268
- result = ''
269
- table_items = table.sorted_items
270
-
271
- # Check the ranges and constraints for each item in the table
272
- # We go through it this way (by row and columns) so we can grab the actual
273
- # user input when we display any errors found
274
- (0...table.num_rows).each do |r|
275
- (0...table.num_columns).each do |c|
276
- # get the table item definition so we know how to save it
277
- table_item = table_items[r * table.num_columns + c]
278
-
279
- value = table.read(table_item.name)
280
- unless table_item.range.nil?
281
- # If the item has states which include the value, then convert
282
- # the state back to the numeric value for range checking
283
- if table_item.states && table_item.states.include?(value)
284
- value = table_item.states[value]
285
- end
286
-
287
- # check to see if the value lies within its valid range
288
- unless table_item.range.include?(value)
289
- if table_item.format_string
290
- value = table.read(table_item.name, :FORMATTED)
291
- range_first =
292
- sprintf(table_item.format_string, table_item.range.first)
293
- range_last =
294
- sprintf(table_item.format_string, table_item.range.last)
295
- else
296
- range_first = table_item.range.first
297
- range_last = table_item.range.last
298
- end
299
- result <<
300
- " #{table_item.name}: #{value} outside valid range of #{range_first}..#{range_last}\n"
301
- end
302
- end
303
- end # end each column
304
- end # end each row
305
- result
306
- end
307
-
308
- # @param table_name [String] Name of the table to revert all values to default
309
- def table_default(table_name)
310
- raise NoConfigError unless @config
311
- set_binary_data_to_default(table_name)
312
- end
313
-
314
- # @param table_name [String] Create a hex formatted string of the given table data
315
- def table_hex(table_name)
316
- raise NoConfigError unless @config
317
- table = @config.table(table_name)
318
- raise NoTableError unless table
319
- "#{table.buffer.formatted}\n\nTotal Bytes Read: #{table.buffer.length}"
320
- end
321
-
322
- # @param table_name [String] Table name to write as a stand alone file
323
- # @param filename [String] Filename to write the table data to. Existing
324
- # files will be overwritten.
325
- def table_save(table_name, filename)
326
- raise NoConfigError unless @config
327
- result = table_check(table_name)
328
- unless result.empty?
329
- raise CoreError, "Errors in #{table_name}:\n#{result}"
330
- end
331
- File.open(filename, 'wb') do |file|
332
- file.write(@config.table(table_name).buffer)
333
- end
334
- end
335
-
336
- # Commit a table from the current configuration into a new binary
337
- #
338
- # @param table_name [String] Table name to commit to an existing binary
339
- # @param bin_file [String] Binary file to open
340
- # @param def_file [String] Definition file to use when opening
341
- def table_commit(table_name, bin_file, def_file)
342
- raise NoConfigError unless @config
343
- save_table = @config.table(table_name)
344
- raise NoTableError unless save_table
345
-
346
- result = table_check(table_name)
347
- unless result.empty?
348
- raise CoreError, "Errors in #{table_name}:\n#{result}"
349
- end
350
-
351
- config = TableConfig.new
352
- begin
353
- config.process_file(def_file)
354
- rescue => err
355
- raise CoreError,
356
- "The table definition file:#{def_file} has the following errors:\n#{err}"
357
- end
358
-
359
- if !config.table_names.include?(table_name.upcase)
360
- raise NoTableError,
361
- "#{table_name} not found in #{def_file} table definition file."
362
- end
363
-
364
- saved_config = @config
365
- @config = config
366
- open_and_load_binary_file(bin_file)
367
-
368
- # Store the saved table data in the new table definition
369
- table = config.table(save_table.table_name)
370
- table.buffer = save_table.buffer[0...table.length]
371
- file_save(bin_file)
372
- @config = saved_config
373
- end
374
-
375
- protected
376
-
377
- # Set all the binary data in the table to the default values
378
- def set_binary_data_to_default(table_name)
379
- table = @config.table(table_name)
380
- raise NoTableError unless table
381
- table.restore_defaults
382
- end
383
-
384
- # Get the binary filename equivalent for the given definition filename
385
- def def_to_bin_filename(def_path)
386
- if File.basename(def_path) =~ /_def\.txt$/
387
- # Remove _def.txt if present (should be)
388
- basename = File.basename(def_path)[0...-8]
389
- else
390
- # Remove any extension if present
391
- basename = File.basename(def_path, File.extname(def_path))
392
- end
393
- "#{basename}.bin"
394
- end
395
-
396
- # Opens the given binary file and populates the table definition.
397
- # The filename parameter should be a properly formatted file path.
398
- def open_and_load_binary_file(filename)
399
- begin
400
- data = nil
401
-
402
- # read the binary file and store it into an array
403
- File.open(filename, 'rb') { |file| data = file.read }
404
- rescue => err
405
- raise "Unable to open and load #{filename} due to #{err}."
406
- end
407
-
193
+ def self.load_binary(config, data)
408
194
  binary_data_index = 0
409
195
  total_table_length = 0
410
- @config.tables.each do |table_name, table|
196
+ config.tables.each do |table_name, table|
411
197
  total_table_length += table.length
412
198
  end
413
- @config.tables.each do |table_name, table|
199
+ config.tables.each do |table_name, table|
414
200
  if binary_data_index + table.length > data.length
415
201
  table.buffer = data[binary_data_index..-1]
416
202
  raise MismatchError,
417
- "Binary size of #{data.length} not large enough to fully represent table definition of length #{total_table_length}. The remaining table definition (starting with byte #{data.length - binary_data_index} in #{table.table_name}) will be filled with 0."
203
+ "Binary size of #{data.length} not large enough to fully represent table definition of length #{total_table_length}. "+
204
+ "The remaining table definition (starting with byte #{data.length - binary_data_index} in #{table.table_name}) will be filled with 0."
418
205
  end
419
- table.buffer =
420
- data[binary_data_index...binary_data_index + table.length]
206
+ table.buffer = data[binary_data_index...binary_data_index + table.length]
421
207
  binary_data_index += table.length
422
208
  end
423
209
  if binary_data_index < data.length
424
210
  raise MismatchError,
425
- "Binary size of #{data.length} larger than table definition of length #{total_table_length}. Discarding the remaing #{data.length - binary_data_index} bytes."
211
+ "Binary size of #{data.length} larger than table definition of length #{total_table_length}. "+
212
+ "Discarding the remaing #{data.length - binary_data_index} bytes."
426
213
  end
427
214
  end
215
+
216
+ # TODO: Potentially useful methods?
217
+ # # @return [String] Success string if parameters all check. Raises
218
+ # # a CoreError if errors are found.
219
+ # def file_check
220
+ # raise NoConfigError unless @config
221
+ # result = ''
222
+ # @config.table_names.each do |name|
223
+ # table_result = table_check(name)
224
+ # unless table_result.empty?
225
+ # result << "Errors in #{name}:\n" + table_result
226
+ # end
227
+ # end
228
+ # raise CoreError, result unless result.empty?
229
+ # 'All parameters are within their constraints.'
230
+ # end
231
+
232
+ # # Create a hex formatted string of all the file data
233
+ # def file_hex
234
+ # raise NoConfigError unless @config
235
+ # data = ''
236
+ # @config.tables.values.each { |table| data << table.buffer }
237
+ # "#{data.formatted}\n\nTotal Bytes Read: #{data.length}"
238
+ # end
239
+
240
+ # # @param table_name [String] Name of the table to check for out of range values
241
+ # def table_check(table_name)
242
+ # raise NoConfigError unless @config
243
+ # table = @config.table(table_name)
244
+ # raise NoTableError unless table
245
+
246
+ # result = ''
247
+ # table_items = table.sorted_items
248
+
249
+ # # Check the ranges and constraints for each item in the table
250
+ # # We go through it this way (by row and columns) so we can grab the actual
251
+ # # user input when we display any errors found
252
+ # (0...table.num_rows).each do |r|
253
+ # (0...table.num_columns).each do |c|
254
+ # # get the table item definition so we know how to save it
255
+ # table_item = table_items[r * table.num_columns + c]
256
+
257
+ # value = table.read(table_item.name)
258
+ # unless table_item.range.nil?
259
+ # # If the item has states which include the value, then convert
260
+ # # the state back to the numeric value for range checking
261
+ # if table_item.states && table_item.states.include?(value)
262
+ # value = table_item.states[value]
263
+ # end
264
+
265
+ # # check to see if the value lies within its valid range
266
+ # unless table_item.range.include?(value)
267
+ # if table_item.format_string
268
+ # value = table.read(table_item.name, :FORMATTED)
269
+ # range_first =
270
+ # sprintf(table_item.format_string, table_item.range.first)
271
+ # range_last =
272
+ # sprintf(table_item.format_string, table_item.range.last)
273
+ # else
274
+ # range_first = table_item.range.first
275
+ # range_last = table_item.range.last
276
+ # end
277
+ # result <<
278
+ # " #{table_item.name}: #{value} outside valid range of #{range_first}..#{range_last}\n"
279
+ # end
280
+ # end
281
+ # end # end each column
282
+ # end # end each row
283
+ # result
284
+ # end
285
+
286
+ # # @param table_name [String] Create a hex formatted string of the given table data
287
+ # def table_hex(table_name)
288
+ # raise NoConfigError unless @config
289
+ # table = @config.table(table_name)
290
+ # raise NoTableError unless table
291
+ # "#{table.buffer.formatted}\n\nTotal Bytes Read: #{table.buffer.length}"
292
+ # end
293
+
294
+ # # Commit a table from the current configuration into a new binary
295
+ # #
296
+ # # @param table_name [String] Table name to commit to an existing binary
297
+ # # @param bin_file [String] Binary file to open
298
+ # # @param def_file [String] Definition file to use when opening
299
+ # def table_commit(table_name, bin_file, def_file)
300
+ # raise NoConfigError unless @config
301
+ # save_table = @config.table(table_name)
302
+ # raise NoTableError unless save_table
303
+
304
+ # result = table_check(table_name)
305
+ # unless result.empty?
306
+ # raise CoreError, "Errors in #{table_name}:\n#{result}"
307
+ # end
308
+
309
+ # config = TableConfig.new
310
+ # begin
311
+ # config.process_file(def_file)
312
+ # rescue => err
313
+ # raise CoreError,
314
+ # "The table definition file:#{def_file} has the following errors:\n#{err}"
315
+ # end
316
+
317
+ # if !config.table_names.include?(table_name.upcase)
318
+ # raise NoTableError,
319
+ # "#{table_name} not found in #{def_file} table definition file."
320
+ # end
321
+
322
+ # saved_config = @config
323
+ # @config = config
324
+ # open_and_load_binary_file(bin_file)
325
+
326
+ # # Store the saved table data in the new table definition
327
+ # table = config.table(save_table.table_name)
328
+ # table.buffer = save_table.buffer[0...table.length]
329
+ # file_save(bin_file)
330
+ # @config = saved_config
331
+ # end
428
332
  end
429
333
  end
@@ -0,0 +1,68 @@
1
+ # encoding: ascii-8bit
2
+
3
+ # Copyright 2022 Ball Aerospace & Technologies Corp.
4
+ # All Rights Reserved.
5
+ #
6
+ # This program is free software; you can modify and/or redistribute it
7
+ # under the terms of the GNU Affero General Public License
8
+ # as published by the Free Software Foundation; version 3 with
9
+ # attribution addendums as found in the LICENSE.txt
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU Affero General Public License for more details.
15
+ #
16
+ # This program may also be used under the terms of a commercial or
17
+ # enterprise edition license of COSMOS if purchased from the
18
+ # copyright holder
19
+
20
+ require 'cosmos/topics/topic'
21
+
22
+ module Cosmos
23
+ class ConfigTopic < Topic
24
+ PRIMARY_KEY = "__CONFIG"
25
+
26
+ # Helper method to initialize the stream and ensure a consistent key
27
+ def self.initialize_stream(scope)
28
+ self.initialize_streams(["#{scope}#{PRIMARY_KEY}"])
29
+ end
30
+
31
+ # Write a configuration change to the topic
32
+ # @param config [Hash] Hash with required keys 'kind', 'name', 'type'
33
+ def self.write(config, scope:)
34
+ unless config.keys.include?(:kind)
35
+ raise "ConfigTopic error, required key kind: not given"
36
+ end
37
+ unless ['created', 'deleted'].include?(config[:kind])
38
+ raise "ConfigTopic error unknown kind: #{config[:kind]}"
39
+ end
40
+ unless config.keys.include?(:name)
41
+ raise "ConfigTopic error, required key name: not given"
42
+ end
43
+ unless config.keys.include?(:type)
44
+ raise "ConfigTopic error, required key type: not given"
45
+ end
46
+ # Limit the configuration topics to 1000 entries
47
+ Topic.write_topic("#{scope}#{PRIMARY_KEY}", config, '*', 1000)
48
+ end
49
+
50
+ def self.read(offset = nil, count: 100, scope:)
51
+ topic = "#{scope}#{PRIMARY_KEY}"
52
+ if offset
53
+ result = Topic.read_topics([topic], [offset], nil, count)
54
+ if result.empty?
55
+ [] # We want to return an empty array rather than an empty hash
56
+ else
57
+ # result is a hash with the topic key followed by an array of results
58
+ # This returns just the array of arrays [[offset, hash], [offset, hash], ...]
59
+ result[topic]
60
+ end
61
+ else
62
+ result = Topic.get_newest_message(topic)
63
+ return [result] if result
64
+ return []
65
+ end
66
+ end
67
+ end
68
+ end