class_name ScheduledJobRecord extends RefCounted const SCHEMA_VERSION := 1 const NO_REPEAT := 0 var data: Dictionary func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) static func create( job_id: StringName, job_type: StringName, due_tick: int, phase: int, entity_id: StringName, stable_sequence: int, payload: Dictionary = {}, dedupe_key: StringName = &"", repeat_interval: int = NO_REPEAT, occurrence: int = 0 ) -> ScheduledJobRecord: var payload_status := [true] var normalized_payload: Variant = _normalize_serializable(payload, payload_status) if not payload_status[0] or not normalized_payload is Dictionary: return null var record := ( ScheduledJobRecord . new( { "schema_version": SCHEMA_VERSION, "job_id": String(job_id), "job_type": String(job_type), "due_tick": due_tick, "phase": phase, "entity_id": String(entity_id), "stable_sequence": stable_sequence, "dedupe_key": String(dedupe_key), "repeat_interval": repeat_interval, "occurrence": occurrence, "payload": normalized_payload, } ) ) return record if record.is_valid() else null static func from_dictionary(record_data: Dictionary) -> ScheduledJobRecord: if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION: return null if not ( record_data . has_all( [ "job_id", "job_type", "due_tick", "phase", "entity_id", "stable_sequence", "dedupe_key", "repeat_interval", "occurrence", "payload", ] ) ): return null if not record_data["payload"] is Dictionary: return null return create( StringName(record_data["job_id"]), StringName(record_data["job_type"]), int(record_data["due_tick"]), int(record_data["phase"]), StringName(record_data["entity_id"]), int(record_data["stable_sequence"]), record_data["payload"], StringName(record_data["dedupe_key"]), int(record_data["repeat_interval"]), int(record_data["occurrence"]) ) func is_valid() -> bool: return ( not get_job_id().is_empty() and not get_job_type().is_empty() and get_due_tick() >= 0 and get_phase() >= 0 and not get_entity_id().is_empty() and get_stable_sequence() >= 0 and get_repeat_interval() >= NO_REPEAT and get_occurrence() >= 0 and data["payload"] is Dictionary ) func get_job_id() -> StringName: return StringName(data["job_id"]) func get_job_type() -> StringName: return StringName(data["job_type"]) func get_due_tick() -> int: return int(data["due_tick"]) func get_phase() -> int: return int(data["phase"]) func get_entity_id() -> StringName: return StringName(data["entity_id"]) func get_stable_sequence() -> int: return int(data["stable_sequence"]) func get_dedupe_key() -> StringName: return StringName(data["dedupe_key"]) func get_repeat_interval() -> int: return int(data["repeat_interval"]) func get_occurrence() -> int: return int(data["occurrence"]) func get_payload() -> Dictionary: return data["payload"].duplicate(true) func is_repeating() -> bool: return get_repeat_interval() > NO_REPEAT func create_next_occurrence(next_stable_sequence: int = -1) -> ScheduledJobRecord: if not is_repeating(): return null var sequence := get_stable_sequence() if next_stable_sequence < 0 else next_stable_sequence var next_occurrence := get_occurrence() + 1 return create( &"%s#%d" % [get_job_id().get_slice("#", 0), next_occurrence], get_job_type(), get_due_tick() + get_repeat_interval(), get_phase(), get_entity_id(), sequence, get_payload(), get_dedupe_key(), get_repeat_interval(), next_occurrence ) func to_dictionary() -> Dictionary: return data.duplicate(true) static func compare(first: ScheduledJobRecord, second: ScheduledJobRecord) -> bool: if first.get_due_tick() != second.get_due_tick(): return first.get_due_tick() < second.get_due_tick() if first.get_phase() != second.get_phase(): return first.get_phase() < second.get_phase() if first.get_entity_id() != second.get_entity_id(): return String(first.get_entity_id()) < String(second.get_entity_id()) return first.get_stable_sequence() < second.get_stable_sequence() static func _normalize_serializable(value: Variant, status: Array) -> Variant: match typeof(value): TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_STRING: return value TYPE_STRING_NAME: return String(value) TYPE_FLOAT: if not is_finite(float(value)): status[0] = false return value TYPE_ARRAY: var normalized_array: Array = [] for item in value: normalized_array.append(_normalize_serializable(item, status)) return normalized_array TYPE_DICTIONARY: return _normalize_dictionary(value, status) status[0] = false return null static func _normalize_dictionary(value: Dictionary, status: Array) -> Dictionary: var source_keys: Dictionary = {} for raw_key in value: if not raw_key is String and not raw_key is StringName: status[0] = false return {} var key := String(raw_key) if source_keys.has(key): status[0] = false return {} source_keys[key] = raw_key var keys: Array = source_keys.keys() keys.sort() var normalized: Dictionary = {} for key in keys: normalized[key] = _normalize_serializable(value[source_keys[key]], status) return normalized