julewire-core 1.0.0 → 1.0.1

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/docs/advanced-configuration.md +2 -1
  4. data/docs/context-and-propagation.md +6 -4
  5. data/docs/contracts.md +2 -1
  6. data/docs/extensions-and-api.md +9 -6
  7. data/docs/internals.md +3 -3
  8. data/docs/outputs-and-lifecycle.md +5 -2
  9. data/docs/records-and-data-policy.md +2 -2
  10. data/lib/julewire/core/cli/log_formats/record_decoder.rb +1 -1
  11. data/lib/julewire/core/context_store.rb +11 -6
  12. data/lib/julewire/core/destinations/destination.rb +3 -3
  13. data/lib/julewire/core/destinations/synchronized_output.rb +51 -15
  14. data/lib/julewire/core/diagnostics/doctor.rb +11 -11
  15. data/lib/julewire/core/diagnostics/failure_snapshot.rb +3 -1
  16. data/lib/julewire/core/execution/handle.rb +2 -0
  17. data/lib/julewire/core/execution/lineage.rb +10 -6
  18. data/lib/julewire/core/execution/scope.rb +4 -4
  19. data/lib/julewire/core/execution/scope_fields.rb +2 -2
  20. data/lib/julewire/core/facade_methods.rb +2 -2
  21. data/lib/julewire/core/fields/field_set.rb +32 -6
  22. data/lib/julewire/core/fields/field_stack.rb +40 -14
  23. data/lib/julewire/core/fields/internal.rb +41 -13
  24. data/lib/julewire/core/fields/stack_set.rb +2 -2
  25. data/lib/julewire/core/integration/destination_health.rb +13 -2
  26. data/lib/julewire/core/propagation/carrier.rb +63 -9
  27. data/lib/julewire/core/propagation.rb +3 -2
  28. data/lib/julewire/core/records/draft.rb +12 -6
  29. data/lib/julewire/core/records/record.rb +43 -13
  30. data/lib/julewire/core/runtime.rb +30 -11
  31. data/lib/julewire/core/serialization/bounded_transform.rb +11 -0
  32. data/lib/julewire/core/serialization/bounded_traversal.rb +33 -27
  33. data/lib/julewire/core/serialization/serializer.rb +1 -1
  34. data/lib/julewire/core/serialization/truncation_metadata.rb +142 -0
  35. data/lib/julewire/core/serialization/value_copy.rb +292 -51
  36. data/lib/julewire/core/testing/contracts/integration.rb +1 -1
  37. data/lib/julewire/core/version.rb +1 -1
  38. metadata +2 -1
@@ -13,17 +13,20 @@ module Julewire
13
13
  class Layer
14
14
  attr_reader :fields, :parent
15
15
 
16
- def initialize(parent, fields, delete_paths: nil, clear_parent_deletes: true)
16
+ def initialize(parent, fields, delete_paths: nil, clear_parent_deletes: true, owned: false)
17
17
  @parent = parent
18
18
  @fields = fields
19
19
  @delete_paths = delete_paths
20
20
  @clear_parent_deletes = clear_parent_deletes
21
+ @owned = owned
21
22
  @active_delete_paths_computed = false
22
23
  @active_delete_paths = nil
23
24
  @snapshot = nil
24
25
  @value_cache = nil
25
26
  end
26
27
 
28
+ def owned? = @owned
29
+
27
30
  def snapshot
28
31
  @snapshot ||= build_snapshot
29
32
  end
@@ -35,7 +38,7 @@ module Julewire
35
38
  FieldSet.value_for(snapshot, key, default: MISSING)
36
39
  else
37
40
  field_value = FieldSet.value_for(@fields, key, default: MISSING)
38
- field_value.equal?(MISSING) ? parent_value_for(key) : Fields::Internal.frozen_copy(field_value)
41
+ field_value.equal?(MISSING) ? parent_value_for(key) : frozen_field_value(field_value)
39
42
  end
40
43
  (@value_cache ||= {})[key] = value
41
44
  end
@@ -56,6 +59,14 @@ module Julewire
56
59
  @clear_parent_deletes ? @delete_paths : active_delete_paths
57
60
  end
58
61
 
62
+ def merge_into(snapshot)
63
+ if @owned
64
+ Fields::Internal.merge_owned!(snapshot, FieldSet.deep_symbolize_owned_keys(@fields))
65
+ else
66
+ FieldSet.merge!(snapshot, @fields)
67
+ end
68
+ end
69
+
59
70
  private
60
71
 
61
72
  def build_snapshot
@@ -64,30 +75,35 @@ module Julewire
64
75
 
65
76
  snapshot = source_snapshot_base
66
77
  source_chain.reverse_each do |source|
67
- FieldSet.merge!(snapshot, source.fields)
78
+ source.merge_into(snapshot)
68
79
  paths = source.delete_paths_for_snapshot
69
80
  Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
70
81
  end
71
- Fields::Internal.frozen_copy(snapshot)
82
+ Fields::Internal.frozen_owned_copy(snapshot)
72
83
  end
73
84
 
74
85
  def build_direct_snapshot
75
- snapshot = FieldSet.merge!({}, @fields)
86
+ snapshot = merge_into({})
76
87
  paths = delete_paths_for_snapshot
77
88
  Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
78
- Fields::Internal.frozen_copy(snapshot)
89
+ Fields::Internal.frozen_owned_copy(snapshot)
79
90
  end
80
91
 
81
92
  def build_parent_snapshot
82
- snapshot = FieldSet.merge(@parent.snapshot, @fields)
93
+ snapshot = FieldSet.deep_dup_owned(@parent.snapshot)
94
+ merge_into(snapshot)
83
95
  paths = delete_paths_for_snapshot
84
96
  Fields::Internal.apply_delete_paths!(snapshot, paths) if paths
85
- Fields::Internal.frozen_copy(snapshot)
97
+ Fields::Internal.frozen_owned_copy(snapshot)
86
98
  end
87
99
 
88
100
  def source_snapshot_base
89
101
  source = source_chain_base
90
- source ? FieldSet.deep_dup(source.snapshot) : {}
102
+ source ? FieldSet.deep_dup_owned(source.snapshot) : {}
103
+ end
104
+
105
+ def frozen_field_value(value)
106
+ @owned ? Fields::Internal.frozen_owned_copy(value) : Fields::Internal.frozen_copy(value)
91
107
  end
92
108
 
93
109
  def source_chain
@@ -181,7 +197,7 @@ module Julewire
181
197
  return if fields.empty?
182
198
 
183
199
  fields = normalize_owned_keys(fields) if owned
184
- @source = Layer.new(@source, fields, clear_parent_deletes: true)
200
+ @source = Layer.new(@source, fields, clear_parent_deletes: true, owned: owned)
185
201
  invalidate_snapshot!
186
202
  end
187
203
 
@@ -199,7 +215,7 @@ module Julewire
199
215
  return yield if fields.empty?
200
216
 
201
217
  fields = normalize_owned_keys(fields) if owned
202
- with_layer(fields, &)
218
+ with_layer(fields, owned: owned, &)
203
219
  end
204
220
 
205
221
  def without(path, &)
@@ -226,9 +242,15 @@ module Julewire
226
242
  FieldSet.coerce(fields, keyword_fields)
227
243
  end
228
244
 
229
- def with_layer(fields, delete_paths: nil)
245
+ def with_layer(fields, delete_paths: nil, owned: false)
230
246
  previous_source = @source
231
- @source = Layer.new(previous_source, fields, delete_paths: delete_paths, clear_parent_deletes: false)
247
+ @source = Layer.new(
248
+ previous_source,
249
+ fields,
250
+ delete_paths: delete_paths,
251
+ clear_parent_deletes: false,
252
+ owned: owned
253
+ )
232
254
  invalidate_snapshot!
233
255
  begin
234
256
  yield
@@ -250,13 +272,17 @@ module Julewire
250
272
  unless @source.parent
251
273
  # Single-layer hits avoid Layer's delete-path/cache bookkeeping.
252
274
  field_value = FieldSet.value_for(@source.fields, key, default: MISSING)
253
- return Fields::Internal.frozen_copy(field_value) unless field_value.equal?(MISSING)
275
+ return frozen_source_value(field_value, @source.owned?) unless field_value.equal?(MISSING)
254
276
  end
255
277
 
256
278
  value = @source.value_for(key)
257
279
  value.equal?(MISSING) ? MISSING : value
258
280
  end
259
281
 
282
+ def frozen_source_value(value, owned)
283
+ owned ? Fields::Internal.frozen_owned_copy(value) : Fields::Internal.frozen_copy(value)
284
+ end
285
+
260
286
  def invalidate_snapshot!
261
287
  @version += 1
262
288
  @snapshot = nil
@@ -18,12 +18,29 @@ module Julewire
18
18
  end
19
19
 
20
20
  def frozen_copy(value)
21
- return EMPTY_HASH if value.is_a?(Hash) && value.empty?
22
- return EMPTY_ARRAY if value.is_a?(Array) && value.empty?
21
+ frozen_copy_with(value, preserve_truncation_metadata: false)
22
+ end
23
+
24
+ def frozen_owned_copy(value)
25
+ frozen_copy_with(value, preserve_truncation_metadata: true)
26
+ end
27
+
28
+ def frozen_deep_symbolize_keys(value)
29
+ frozen_deep_symbolize_keys_with(value, preserve_truncation_metadata: false)
30
+ end
23
31
 
24
- Serialization::ValueCopy.call(value, freeze_values: true)
32
+ def frozen_deep_symbolize_owned_keys(value)
33
+ frozen_deep_symbolize_keys_with(value, preserve_truncation_metadata: true)
25
34
  end
26
35
 
36
+ def delete_path!(target, path) = Deletion.delete_path!(target, path)
37
+
38
+ def apply_delete_paths!(target, paths) = Deletion.apply_delete_paths!(target, paths)
39
+
40
+ def clear_delete_paths!(paths, fields) = Deletion.clear_delete_paths!(paths, fields)
41
+
42
+ def normalize_path(path) = Deletion.normalize_path(path)
43
+
27
44
  def deep_merge(left, right)
28
45
  deep_merge!(FieldSet.deep_symbolize_keys(left), right)
29
46
  end
@@ -52,22 +69,33 @@ module Julewire
52
69
  merge_values!(target, fields) { |value, _existing| value }
53
70
  end
54
71
 
55
- def frozen_deep_symbolize_keys(value)
72
+ private
73
+
74
+ def frozen_copy_with(value, preserve_truncation_metadata:)
56
75
  return EMPTY_HASH if value.is_a?(Hash) && value.empty?
57
76
  return EMPTY_ARRAY if value.is_a?(Array) && value.empty?
58
77
 
59
- Serialization::ValueCopy.call(value, freeze_values: true, symbolize_keys: true)
78
+ Serialization::ValueCopy.call(
79
+ value,
80
+ freeze_values: true,
81
+ preserve_truncation_metadata: preserve_truncation_metadata
82
+ )
60
83
  end
61
84
 
62
- def delete_path!(target, path) = Deletion.delete_path!(target, path)
63
-
64
- def apply_delete_paths!(target, paths) = Deletion.apply_delete_paths!(target, paths)
65
-
66
- def clear_delete_paths!(paths, fields) = Deletion.clear_delete_paths!(paths, fields)
67
-
68
- def normalize_path(path) = Deletion.normalize_path(path)
85
+ def frozen_deep_symbolize_keys_with(value, preserve_truncation_metadata:)
86
+ return EMPTY_HASH if value.is_a?(Hash) && value.empty?
87
+ return EMPTY_ARRAY if value.is_a?(Array) && value.empty?
69
88
 
70
- private
89
+ Serialization::ValueCopy.call(
90
+ value,
91
+ freeze_values: true,
92
+ max_array_items: Serialization::Serializer::DEFAULT_MAX_ARRAY_ITEMS,
93
+ max_hash_keys: Serialization::Serializer::DEFAULT_MAX_HASH_KEYS,
94
+ max_string_bytes: Serialization::Serializer::DEFAULT_MAX_STRING_BYTES,
95
+ preserve_truncation_metadata: preserve_truncation_metadata,
96
+ symbolize_keys: true
97
+ )
98
+ end
71
99
 
72
100
  def merge_values!(target, fields)
73
101
  return target unless fields.is_a?(Hash)
@@ -48,8 +48,8 @@ module Julewire
48
48
  stack(section).delete(path)
49
49
  end
50
50
 
51
- def with(section, fields, owned: false, &)
52
- stack(section).with(fields, owned: owned, &)
51
+ def with(section, fields = nil, owned: false, **keyword_fields, &)
52
+ stack(section).with(fields, owned: owned, **keyword_fields, &)
53
53
  end
54
54
 
55
55
  def without(section, path, &)
@@ -5,9 +5,10 @@ module Julewire
5
5
  module Integration
6
6
  # @api integration_spi
7
7
  class DestinationHealth
8
- def initialize(counter_keys:, failure_counter: :failures)
8
+ def initialize(counter_keys:, callback_failure_counter: nil, failure_counter: :failures)
9
9
  @failure_counter = failure_counter
10
10
  @state = Diagnostics::Health.new(
11
+ callback_failure_counter: callback_failure_counter,
11
12
  counter_keys: counter_keys,
12
13
  failure_counter: failure_counter,
13
14
  track_failures: failure_counter == :failures
@@ -26,16 +27,26 @@ module Julewire
26
27
  @state.record_loss(reason: reason, counter: counter, degrade: false, **metadata)
27
28
  end
28
29
 
30
+ def record_callback_failure(callback_failure)
31
+ @state.record_callback_failure(callback_failure)
32
+ end
33
+
29
34
  def clear_degraded! = @state.clear_failures!
30
35
 
31
36
  def degraded? = @state.degraded?(status_from: :failure_or_loss)
32
37
 
38
+ def last_callback_failure = @state.last_callback_failure
39
+
33
40
  def last_loss = @state.last_loss
34
41
 
35
42
  def last_failure = @state.last_failure
36
43
 
37
44
  def snapshot(status: nil, **fields)
38
- @state.snapshot(status: status, status_from: :failure_or_loss, include_loss: true, **fields)
45
+ snapshot = @state.snapshot(status: status, status_from: :failure_or_loss, include_loss: true, **fields)
46
+ callback_failure = @state.last_callback_failure
47
+ return snapshot unless callback_failure
48
+
49
+ snapshot.merge(last_callback_failure: callback_failure).freeze
39
50
  end
40
51
  end
41
52
  end
@@ -8,8 +8,33 @@ module Julewire
8
8
  # @api public
9
9
  module Carrier
10
10
  DEFAULT_KEY = "julewire"
11
+ DEFAULT_MAX_BYTES = 65_536
11
12
  DEFAULT_ENVELOPE = Core.sentinel(:default_envelope)
13
+ class Extracted
14
+ attr_reader :envelope, :status, :reason, :error
15
+
16
+ def initialize(envelope:, status:, reason: nil, error: nil)
17
+ @envelope = envelope
18
+ @status = status
19
+ @reason = reason
20
+ @error = error
21
+ end
22
+
23
+ def failure? = !error.nil?
24
+ end
12
25
  private_constant :DEFAULT_ENVELOPE
26
+ private_constant :Extracted
27
+
28
+ # @api integration_spi
29
+ class ExtractionError < StandardError
30
+ attr_reader :status, :reason
31
+
32
+ def initialize(status, reason)
33
+ @status = status
34
+ @reason = reason
35
+ super(reason)
36
+ end
37
+ end
13
38
 
14
39
  class << self
15
40
  def encode(envelope: DEFAULT_ENVELOPE, max_bytes: nil)
@@ -31,18 +56,20 @@ module Julewire
31
56
  carrier
32
57
  end
33
58
 
34
- def extract(carrier, key: DEFAULT_KEY)
35
- value = carrier_value(carrier, key)
36
- return {} unless value
59
+ def extract(carrier, key: DEFAULT_KEY, max_bytes: DEFAULT_MAX_BYTES)
60
+ extract_result(carrier, key: key, max_bytes: max_bytes).envelope
61
+ end
37
62
 
38
- parsed = JSON.parse(value.to_s)
39
- parsed.is_a?(Hash) ? Fields::FieldSet.deep_symbolize_keys(parsed) : {}
40
- rescue StandardError
41
- {}
63
+ # @api integration_spi
64
+ def extract_result(carrier, key: DEFAULT_KEY, max_bytes: DEFAULT_MAX_BYTES)
65
+ Validation.validate_byte_limit!(max_bytes, name: :max_bytes)
66
+
67
+ extract_payload(carrier, key: key, max_bytes: max_bytes)
42
68
  end
43
69
 
44
- def restore(carrier, key: DEFAULT_KEY, link_executions: false, &)
45
- Propagation.restore(extract(carrier, key: key), link_executions: link_executions, &)
70
+ def restore(carrier, key: DEFAULT_KEY, link_executions: false, max_bytes: DEFAULT_MAX_BYTES, &)
71
+ envelope = extract_result(carrier, key: key, max_bytes: max_bytes).envelope
72
+ Propagation.restore(envelope, link_executions: link_executions, owned: true, &)
46
73
  end
47
74
 
48
75
  def serialized_envelope(envelope)
@@ -53,6 +80,33 @@ module Julewire
53
80
 
54
81
  private
55
82
 
83
+ def extract_payload(carrier, key:, max_bytes:)
84
+ value = carrier_value(carrier, key)
85
+ return extracted({}, :missing) unless value
86
+
87
+ string = value.to_s
88
+ if max_bytes && string.bytesize > max_bytes
89
+ return extracted_failure(:oversized, "carrier payload exceeds max_bytes")
90
+ end
91
+
92
+ parsed = JSON.parse(string)
93
+ return extracted_failure(:non_hash, "carrier payload must be a JSON object") unless parsed.is_a?(Hash)
94
+
95
+ extracted(Fields::FieldSet.deep_symbolize_owned_keys(parsed), :ok)
96
+ rescue StandardError => e
97
+ extracted_failure(:malformed, "carrier payload is not valid JSON", e)
98
+ end
99
+
100
+ def extracted(envelope, status)
101
+ Extracted.new(envelope: envelope, status: status)
102
+ end
103
+
104
+ def extracted_failure(status, reason, cause = nil)
105
+ error = ExtractionError.new(status, reason)
106
+ error.set_backtrace(cause.backtrace) if cause&.backtrace
107
+ Extracted.new(envelope: {}, status: status, reason: reason, error: error)
108
+ end
109
+
56
110
  def carrier_value(carrier, key)
57
111
  return unless carrier.respond_to?(:[])
58
112
 
@@ -13,10 +13,10 @@ module Julewire
13
13
  end
14
14
 
15
15
  def capture_local
16
- capture_with { Fields::FieldSet.deep_dup(it) }
16
+ capture_with { Fields::FieldSet.deep_dup_owned(it) }
17
17
  end
18
18
 
19
- def restore(envelope, link_executions: false, &)
19
+ def restore(envelope, link_executions: false, owned: false, &)
20
20
  raise ArgumentError, "block required" unless block_given?
21
21
 
22
22
  sections = FIELD_SECTIONS.to_h { |section| [section, hash_value(envelope, section)] }
@@ -25,6 +25,7 @@ module Julewire
25
25
  **sections,
26
26
  execution: execution,
27
27
  link_executions: link_executions,
28
+ owned: owned,
28
29
  &
29
30
  )
30
31
  end
@@ -90,10 +90,11 @@ module Julewire
90
90
  public
91
91
 
92
92
  def from_normalized_hash(data, lineage: nil, freeze_sections: true)
93
- normalized = Fields::FieldSet.deep_symbolize_keys(data)
93
+ Record.validate_normalized_hash!(data)
94
+ normalized = data.dup
94
95
  lineage ||= Execution::Lineage.from_execution_hash(normalized[:execution])
95
- normalized[:execution] = Execution::Lineage.clean_lazy_relationship_hash(normalized[:execution])
96
- normalized = Fields::Internal.frozen_deep_symbolize_keys(normalized) if freeze_sections
96
+ normalized[:execution] = Execution::Lineage.clean_normalized_lazy_relationship_hash(normalized[:execution])
97
+ normalized = Fields::Internal.frozen_owned_copy(normalized) if freeze_sections
97
98
  new(
98
99
  normalized,
99
100
  lineage: lineage,
@@ -182,7 +183,7 @@ module Julewire
182
183
 
183
184
  def each_key(&) = @data.each_key(&)
184
185
 
185
- def to_h = Fields::FieldSet.deep_dup(@data)
186
+ def to_h = Fields::FieldSet.deep_dup_owned(@data)
186
187
 
187
188
  def transform_field!(key)
188
189
  key = Fields::Internal.normalize_key(key)
@@ -236,7 +237,7 @@ module Julewire
236
237
  end
237
238
 
238
239
  def ensure_mutable_data!
239
- @data = Fields::FieldSet.deep_dup(@data) if @data.frozen?
240
+ @data = Fields::FieldSet.deep_dup_owned(@data) if @data.frozen?
240
241
  end
241
242
 
242
243
  def transformed_field_lineage(key, value)
@@ -553,7 +554,12 @@ module Julewire
553
554
  def normalized_hash(value)
554
555
  return empty_hash if value.is_a?(Hash) && value.empty?
555
556
 
556
- return value if @input_owned && !@freeze_sections && value.is_a?(Hash)
557
+ if @input_owned
558
+ return Fields::Internal.frozen_deep_symbolize_owned_keys(value) if @freeze_sections
559
+ return value if value.is_a?(Hash)
560
+
561
+ return Fields::FieldSet.deep_symbolize_owned_keys(value)
562
+ end
557
563
  return Fields::Internal.frozen_deep_symbolize_keys(value) if @freeze_sections
558
564
 
559
565
  Fields::FieldSet.deep_symbolize_keys(value)
@@ -19,21 +19,20 @@ module Julewire
19
19
 
20
20
  class << self
21
21
  def from_normalized_hash(record, lineage: nil)
22
- if record.is_a?(Hash)
23
- lineage ||= Execution::Lineage.from_execution_hash(record[:execution])
24
- record = record.merge(execution: Execution::Lineage.clean_lazy_relationship_hash(record[:execution]))
25
- end
26
22
  validate_normalized_hash!(record)
23
+ execution = record.fetch(:execution)
24
+ lineage ||= Execution::Lineage.from_execution_hash(execution)
25
+ record = record.merge(
26
+ execution: Execution::Lineage.clean_normalized_lazy_relationship_hash(execution)
27
+ )
27
28
  new(snapshot_hash(record), lineage: lineage)
28
29
  end
29
30
 
30
31
  def from_owned_hash(record, lineage: nil, trust_frozen: false)
31
- if record.is_a?(Hash)
32
- lineage ||= Execution::Lineage.from_execution_hash(record.fetch(:execution))
33
- execution = Execution::Lineage.clean_lazy_relationship_hash(record.fetch(:execution))
34
- record = record.frozen? ? record.merge(execution: execution) : replace_execution(record, execution)
35
- end
36
32
  validate_normalized_hash!(record)
33
+ lineage ||= Execution::Lineage.from_execution_hash(record.fetch(:execution))
34
+ execution = Execution::Lineage.clean_normalized_lazy_relationship_hash(record.fetch(:execution))
35
+ record = record.frozen? ? record.merge(execution: execution) : replace_execution(record, execution)
37
36
  new(Serialization::DeepFreeze.call(record, trust_frozen: trust_frozen), lineage: lineage)
38
37
  end
39
38
 
@@ -70,8 +69,38 @@ module Julewire
70
69
  end
71
70
 
72
71
  def validate_symbol_keys!(record)
73
- record.each_key do |key|
74
- raise TypeError, "record must not use string keys" if key.is_a?(String)
72
+ validate_value_symbol_keys!(record)
73
+ end
74
+
75
+ def validate_value_symbol_keys!(root)
76
+ queue = [[root, 0]]
77
+ seen = {}.compare_by_identity
78
+
79
+ queue.each do |value, depth|
80
+ break if depth == NORMALIZATION_MAX_DEPTH
81
+ next unless mark_symbol_key_container(value, seen)
82
+
83
+ enqueue_symbol_key_children(queue, value, depth + 1)
84
+ end
85
+ end
86
+
87
+ def mark_symbol_key_container(value, seen)
88
+ return unless value.is_a?(Hash) || value.is_a?(Array)
89
+ return if seen.key?(value)
90
+
91
+ seen[value] = nil
92
+ value
93
+ end
94
+
95
+ def enqueue_symbol_key_children(queue, value, depth)
96
+ if value.is_a?(Hash)
97
+ value.each do |key, item|
98
+ raise TypeError, "record must not use string keys" if key.is_a?(String)
99
+
100
+ queue << [item, depth]
101
+ end
102
+ else
103
+ value.each { queue << [it, depth] }
75
104
  end
76
105
  end
77
106
 
@@ -126,7 +155,8 @@ module Julewire
126
155
  def snapshot_hash(record)
127
156
  Serialization::ValueCopy.call(
128
157
  record,
129
- freeze_values: true
158
+ freeze_values: true,
159
+ preserve_truncation_metadata: true
130
160
  )
131
161
  end
132
162
  end
@@ -149,7 +179,7 @@ module Julewire
149
179
 
150
180
  def each(&) = @data.each(&)
151
181
 
152
- def to_h = Fields::FieldSet.deep_dup(@data)
182
+ def to_h = Fields::FieldSet.deep_dup_owned(@data)
153
183
 
154
184
  # @api internal
155
185
  def serializable_data = @data
@@ -80,22 +80,14 @@ module Julewire
80
80
  end
81
81
  end
82
82
 
83
- def emit_envelope(input:, context:, scope:, carry: {}, attributes: {}, neutral: {}, enforce_level: true)
83
+ def emit_envelope(input:, context:, scope:, carry: {}, attributes: {}, neutral: {}, enforce_level: true,
84
+ owned: false)
84
85
  reject_runtime_call_during_configure!(:emit_envelope)
85
86
  state = runtime_state
86
87
  return record_post_close_emit(state) if state.pipeline_closed
87
88
 
88
89
  begin
89
- record = Records::Draft.build(
90
- input,
91
- context: envelope_hash(context),
92
- attributes: envelope_hash(attributes),
93
- neutral: envelope_hash(neutral),
94
- carry: envelope_hash(carry),
95
- scope: scope,
96
- error_backtrace_lines: state.configuration.error_backtrace_lines,
97
- invalid_severity_reporter: @invalid_severity_reporter
98
- ).to_record
90
+ record = envelope_draft(input, context, attributes, neutral, carry, scope, state, owned: owned).to_record
99
91
  state.pipeline.emit_record(record, enforce_level: enforce_level)
100
92
  rescue StandardError => e
101
93
  notify_failure(e, state, action: :emit_envelope)
@@ -200,6 +192,33 @@ module Julewire
200
192
 
201
193
  private
202
194
 
195
+ def envelope_draft(input, context, attributes, neutral, carry, scope, state, owned:)
196
+ if owned
197
+ Records::Draft.build_pipeline_owned(
198
+ input,
199
+ context: envelope_hash(context),
200
+ attributes: envelope_hash(attributes),
201
+ neutral: envelope_hash(neutral),
202
+ carry: envelope_hash(carry),
203
+ scope: scope,
204
+ error_backtrace_lines: state.configuration.error_backtrace_lines,
205
+ input_owned: true,
206
+ invalid_severity_reporter: @invalid_severity_reporter
207
+ )
208
+ else
209
+ Records::Draft.build(
210
+ input,
211
+ context: envelope_hash(context),
212
+ attributes: envelope_hash(attributes),
213
+ neutral: envelope_hash(neutral),
214
+ carry: envelope_hash(carry),
215
+ scope: scope,
216
+ error_backtrace_lines: state.configuration.error_backtrace_lines,
217
+ invalid_severity_reporter: @invalid_severity_reporter
218
+ )
219
+ end
220
+ end
221
+
203
222
  def before_execution_boundary_call!(action)
204
223
  reject_runtime_call_during_configure!(action)
205
224
  end
@@ -49,6 +49,17 @@ module Julewire
49
49
  transformed = @transform.call(value, key: key, path: path, original: @root, depth: depth)
50
50
  transformed.equal?(CONTINUE) ? value : transformed
51
51
  end
52
+
53
+ def truncation_metadata(fields)
54
+ TruncationMetadata.build(
55
+ fields,
56
+ key_style: :symbol,
57
+ max_array_items: @max_array_items,
58
+ max_depth: @max_depth,
59
+ max_hash_keys: @max_hash_keys,
60
+ max_string_bytes: @max_string_bytes
61
+ )
62
+ end
52
63
  end
53
64
  end
54
65
  end