1use std::fmt;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12use crate::{
13 ComponentInstanceId, ComponentRootId, ComponentStructuralRegionId, ComputedCacheSlotId,
14 ComputedDirtyFlagId, FormInstanceId, IrStorageId, SemanticId,
15};
16
17macro_rules! resume_id {
18 ($name:ident) => {
19 #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
20 #[serde(transparent)]
21 pub struct $name(String);
22
23 impl $name {
24 #[must_use]
25 pub fn as_str(&self) -> &str {
26 &self.0
27 }
28 }
29
30 impl fmt::Display for $name {
31 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
32 self.0.fmt(formatter)
33 }
34 }
35
36 impl FromStr for $name {
37 type Err = ResumeIdentityParseError;
38
39 fn from_str(value: &str) -> Result<Self, Self::Err> {
40 if value.is_empty() {
41 Err(ResumeIdentityParseError)
42 } else {
43 Ok(Self(value.to_string()))
44 }
45 }
46 }
47 };
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub struct ResumeIdentityParseError;
52
53impl fmt::Display for ResumeIdentityParseError {
54 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55 formatter.write_str("resume identity must not be empty")
56 }
57}
58
59impl std::error::Error for ResumeIdentityParseError {}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
62pub enum ResumeBoundaryKind {
63 ApplicationRoot,
64 ComponentInstance,
65 StructuralRegion,
66 FormInstance,
67 Interaction,
68}
69
70impl ResumeBoundaryKind {
71 const fn label(self) -> &'static str {
72 match self {
73 Self::ApplicationRoot => "ApplicationRoot",
74 Self::ComponentInstance => "ComponentInstance",
75 Self::StructuralRegion => "StructuralRegion",
76 Self::FormInstance => "FormInstance",
77 Self::Interaction => "Interaction",
78 }
79 }
80}
81
82#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
83pub enum ResumeActivationRootKind {
84 Eager,
85 Event,
86 Visible,
87 Manual,
88}
89
90impl ResumeActivationRootKind {
91 const fn label(self) -> &'static str {
92 match self {
93 Self::Eager => "Eager",
94 Self::Event => "Event",
95 Self::Visible => "Visible",
96 Self::Manual => "Manual",
97 }
98 }
99}
100
101resume_id!(ResumeBoundaryId);
102resume_id!(ResumeSlotId);
103resume_id!(ResumeSchemaId);
104resume_id!(ResumeCaptureProgramId);
105resume_id!(ResumeRestoreProgramId);
106resume_id!(ResumeChunkId);
107resume_id!(ResumeChunkGroupId);
108resume_id!(ResumeActivationId);
109resume_id!(ResumeAnchorId);
110resume_id!(ResumeEventId);
111resume_id!(ResumeSnapshotId);
112resume_id!(ResumeBuildId);
113resume_id!(ResumeValueRecordId);
114
115#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
118pub struct TemplateInstanceTargetId {
119 component_instance_id: ComponentInstanceId,
120 template_entity_id: SemanticId,
121}
122
123impl TemplateInstanceTargetId {
124 #[must_use]
125 pub fn for_component_instance_template_entity(
126 component_instance_id: ComponentInstanceId,
127 template_entity_id: SemanticId,
128 ) -> Self {
129 Self {
130 component_instance_id,
131 template_entity_id,
132 }
133 }
134
135 #[must_use]
136 pub const fn component_instance_id(&self) -> &ComponentInstanceId {
137 &self.component_instance_id
138 }
139
140 #[must_use]
141 pub const fn template_entity_id(&self) -> &SemanticId {
142 &self.template_entity_id
143 }
144}
145
146impl fmt::Display for TemplateInstanceTargetId {
147 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148 write!(
149 formatter,
150 "{}/template-target:{}",
151 self.component_instance_id,
152 percent_encode(self.template_entity_id.as_str())
153 )
154 }
155}
156
157#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
159pub struct TemplateInstanceBindingId {
160 component_instance_id: ComponentInstanceId,
161 binding_id: SemanticId,
162}
163
164impl TemplateInstanceBindingId {
165 #[must_use]
166 pub fn for_component_instance_binding(
167 component_instance_id: ComponentInstanceId,
168 binding_id: SemanticId,
169 ) -> Self {
170 Self {
171 component_instance_id,
172 binding_id,
173 }
174 }
175
176 #[must_use]
177 pub const fn component_instance_id(&self) -> &ComponentInstanceId {
178 &self.component_instance_id
179 }
180
181 #[must_use]
182 pub const fn binding_id(&self) -> &SemanticId {
183 &self.binding_id
184 }
185}
186
187impl fmt::Display for TemplateInstanceBindingId {
188 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
189 write!(
190 formatter,
191 "{}/template-binding:{}",
192 self.component_instance_id,
193 percent_encode(self.binding_id.as_str())
194 )
195 }
196}
197
198#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
201pub struct ComputedInstanceCacheSlotId {
202 component_instance_id: ComponentInstanceId,
203 cache_slot_id: ComputedCacheSlotId,
204}
205
206impl ComputedInstanceCacheSlotId {
207 #[must_use]
208 pub fn for_component_instance_cache_slot(
209 component_instance_id: ComponentInstanceId,
210 cache_slot_id: ComputedCacheSlotId,
211 ) -> Self {
212 Self {
213 component_instance_id,
214 cache_slot_id,
215 }
216 }
217
218 #[must_use]
219 pub const fn component_instance_id(&self) -> &ComponentInstanceId {
220 &self.component_instance_id
221 }
222
223 #[must_use]
224 pub const fn cache_slot_id(&self) -> &ComputedCacheSlotId {
225 &self.cache_slot_id
226 }
227}
228
229impl fmt::Display for ComputedInstanceCacheSlotId {
230 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
231 write!(
232 formatter,
233 "{}/computed-cache:{}",
234 self.component_instance_id,
235 percent_encode(self.cache_slot_id.as_str())
236 )
237 }
238}
239
240#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
243pub struct ComputedInstanceDirtySlotId {
244 component_instance_id: ComponentInstanceId,
245 dirty_flag_id: ComputedDirtyFlagId,
246}
247
248impl ComputedInstanceDirtySlotId {
249 #[must_use]
250 pub fn for_component_instance_dirty_flag(
251 component_instance_id: ComponentInstanceId,
252 dirty_flag_id: ComputedDirtyFlagId,
253 ) -> Self {
254 Self {
255 component_instance_id,
256 dirty_flag_id,
257 }
258 }
259
260 #[must_use]
261 pub const fn component_instance_id(&self) -> &ComponentInstanceId {
262 &self.component_instance_id
263 }
264
265 #[must_use]
266 pub const fn dirty_flag_id(&self) -> &ComputedDirtyFlagId {
267 &self.dirty_flag_id
268 }
269}
270
271impl fmt::Display for ComputedInstanceDirtySlotId {
272 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
273 write!(
274 formatter,
275 "{}/computed-dirty:{}",
276 self.component_instance_id,
277 percent_encode(self.dirty_flag_id.as_str())
278 )
279 }
280}
281
282#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
285pub struct StateInstanceSlotId {
286 component_instance_id: ComponentInstanceId,
287 storage_id: IrStorageId,
288}
289
290impl StateInstanceSlotId {
291 #[must_use]
292 pub fn for_component_instance_storage(
293 component_instance_id: ComponentInstanceId,
294 storage_id: IrStorageId,
295 ) -> Self {
296 Self {
297 component_instance_id,
298 storage_id,
299 }
300 }
301
302 #[must_use]
303 pub const fn component_instance_id(&self) -> &ComponentInstanceId {
304 &self.component_instance_id
305 }
306
307 #[must_use]
308 pub const fn storage_id(&self) -> &IrStorageId {
309 &self.storage_id
310 }
311}
312
313impl fmt::Display for StateInstanceSlotId {
314 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
315 write!(
316 formatter,
317 "{}/state-slot:{}",
318 self.component_instance_id,
319 percent_encode(self.storage_id.as_str())
320 )
321 }
322}
323
324impl ResumeBoundaryId {
325 #[must_use]
326 pub fn application_root(root: &ComponentRootId) -> Self {
327 Self::new(
328 root.as_str(),
329 ResumeBoundaryKind::ApplicationRoot,
330 root.as_str(),
331 )
332 }
333 #[must_use]
334 pub fn component_instance(instance: &ComponentInstanceId) -> Self {
335 Self::new(
336 instance.as_str(),
337 ResumeBoundaryKind::ComponentInstance,
338 instance.as_str(),
339 )
340 }
341 #[must_use]
342 pub fn structural_region(
343 owner: &ComponentInstanceId,
344 region: &ComponentStructuralRegionId,
345 ) -> Self {
346 Self::new(
347 owner.as_str(),
348 ResumeBoundaryKind::StructuralRegion,
349 region.as_str(),
350 )
351 }
352 #[must_use]
353 pub fn form_instance(instance: &FormInstanceId) -> Self {
354 Self::new(
355 instance.as_str(),
356 ResumeBoundaryKind::FormInstance,
357 instance.as_str(),
358 )
359 }
360 #[must_use]
361 pub fn interaction(owner: &ComponentInstanceId, event: &SemanticId) -> Self {
362 Self::new(
363 owner.as_str(),
364 ResumeBoundaryKind::Interaction,
365 event.as_str(),
366 )
367 }
368 fn new(owner: &str, kind: ResumeBoundaryKind, local: &str) -> Self {
369 Self(format!("resume-boundary:{owner}:{}:{local}", kind.label()))
370 }
371}
372
373impl ResumeSlotId {
374 #[must_use]
375 pub fn for_existing_storage(slot: &str) -> Self {
376 Self(format!("resume-slot:{slot}"))
377 }
378}
379impl ResumeSchemaId {
380 #[must_use]
381 pub fn for_boundary(boundary: &ResumeBoundaryId) -> Self {
382 Self(format!("resume-schema:{boundary}"))
383 }
384}
385impl ResumeCaptureProgramId {
386 #[must_use]
387 pub fn for_boundary(boundary: &ResumeBoundaryId) -> Self {
388 Self(format!("resume-capture:{boundary}"))
389 }
390}
391impl ResumeRestoreProgramId {
392 #[must_use]
393 pub fn for_boundary(boundary: &ResumeBoundaryId) -> Self {
394 Self(format!("resume-restore:{boundary}"))
395 }
396}
397impl ResumeActivationId {
398 #[must_use]
399 pub fn for_boundary(boundary: &ResumeBoundaryId) -> Self {
400 Self(format!("resume-activation:{boundary}"))
401 }
402}
403impl ResumeEventId {
404 #[must_use]
405 pub fn for_existing_event(event: &str) -> Self {
406 Self(format!("resume-event:{event}"))
407 }
408}
409impl ResumeSnapshotId {
410 #[must_use]
411 pub fn for_build(build: &ResumeBuildId) -> Self {
412 Self(format!("resume-snapshot:{build}"))
413 }
414}
415impl ResumeValueRecordId {
416 #[must_use]
417 pub fn for_slot(slot: &ResumeSlotId) -> Self {
418 Self(format!("resume-value:{slot}"))
419 }
420}
421impl ResumeChunkId {
422 #[must_use]
423 pub fn for_activation_root(kind: ResumeActivationRootKind, root: &str) -> Self {
424 Self(format!("resume-chunk:{}:{root}", kind.label()))
425 }
426}
427impl ResumeChunkGroupId {
428 #[must_use]
429 pub fn for_ordered_chunks(chunks: &[ResumeChunkId]) -> Self {
430 Self(format!(
431 "resume-chunk-group:{:016x}",
432 canonical_hash(
433 &chunks
434 .iter()
435 .map(ToString::to_string)
436 .collect::<Vec<_>>()
437 .join("\n")
438 )
439 ))
440 }
441}
442impl ResumeAnchorId {
443 #[must_use]
444 pub fn for_target(boundary: &ResumeBoundaryId, kind: &str, target: &str) -> Self {
445 Self(format!(
446 "presolve-r:{:016x}",
447 canonical_hash(&format!("{boundary}\n{kind}\n{target}"))
448 ))
449 }
450}
451impl ResumeBuildId {
452 pub const ZERO_SENTINEL: &'static str =
453 "resume-build:0000000000000000000000000000000000000000000000000000000000000000";
454
455 #[must_use]
456 pub fn for_public_inputs(inputs: &str) -> Self {
457 let digest = Sha256::digest(inputs.as_bytes());
458 Self(format!("resume-build:{digest:x}"))
459 }
460
461 #[must_use]
462 pub fn zero_sentinel() -> Self {
463 Self(Self::ZERO_SENTINEL.to_string())
464 }
465}
466
467fn canonical_hash(value: &str) -> u64 {
468 value
469 .as_bytes()
470 .iter()
471 .fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
472 (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
473 })
474}
475fn percent_encode(value: &str) -> String {
476 value.bytes().fold(String::new(), |mut encoded, byte| {
477 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
478 encoded.push(char::from(byte));
479 } else {
480 use std::fmt::Write as _;
481 write!(encoded, "%{byte:02X}").expect("writing to a String cannot fail");
482 }
483 encoded
484 })
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::{ComponentRootId, FormId};
491
492 #[test]
493 fn j1_resume_identities_are_typed_deterministic_and_instance_qualified() {
494 let component = SemanticId::component(Some("x-one"), "One");
495 let root = ComponentRootId::for_component(&component);
496 let first = ComponentInstanceId::for_root(&root);
497 let second = ComponentInstanceId::for_invocation(
498 &first,
499 &crate::ComponentInvocationId::for_template_entity(&component, "child"),
500 );
501 let form = FormId::for_owner(&component, "profile");
502 let first_form = FormInstanceId::for_component_instance(&first, &form);
503 let second_form = FormInstanceId::for_component_instance(&second, &form);
504 assert_ne!(
505 ResumeBoundaryId::component_instance(&first),
506 ResumeBoundaryId::component_instance(&second)
507 );
508 assert_ne!(
509 ResumeBoundaryId::form_instance(&first_form),
510 ResumeBoundaryId::form_instance(&second_form)
511 );
512 let template_entity = component.template().template_entity("element", "root");
513 let binding = component.template().template_entity("binding", "root.0");
514 assert_ne!(
515 TemplateInstanceTargetId::for_component_instance_template_entity(
516 first.clone(),
517 template_entity.clone(),
518 ),
519 TemplateInstanceTargetId::for_component_instance_template_entity(
520 second.clone(),
521 template_entity,
522 )
523 );
524 assert!(
525 TemplateInstanceBindingId::for_component_instance_binding(first, binding)
526 .to_string()
527 .contains("/template-binding:")
528 );
529 let event = SemanticId::component(Some("x-one-event"), "Event");
530 assert_eq!(
531 ResumeEventId::for_existing_event(event.as_str()).to_string(),
532 format!("resume-event:{event}")
533 );
534 let slot = ResumeSlotId::for_existing_storage("runtime-slot:one");
535 assert_eq!(
536 ResumeValueRecordId::for_slot(&slot).to_string(),
537 "resume-value:resume-slot:runtime-slot:one"
538 );
539 let chunk =
540 ResumeChunkId::for_activation_root(ResumeActivationRootKind::Event, event.as_str());
541 assert_eq!(
542 ResumeChunkGroupId::for_ordered_chunks(std::slice::from_ref(&chunk)),
543 ResumeChunkGroupId::for_ordered_chunks(&[chunk])
544 );
545 assert!("".parse::<ResumeBuildId>().is_err());
546 }
547
548 #[test]
549 fn j1a_state_slot_identity_preserves_the_exact_typed_pair() {
550 let component = SemanticId::component(Some("x-state"), "State");
551 let instance = ComponentInstanceId::for_root(&ComponentRootId::for_component(&component));
552 let storage = IrStorageId::for_semantic_origin(&component.state_field("count/value"));
553 let slot =
554 StateInstanceSlotId::for_component_instance_storage(instance.clone(), storage.clone());
555 assert_eq!(slot.component_instance_id(), &instance);
556 assert_eq!(slot.storage_id(), &storage);
557 assert_eq!(
558 slot.to_string(),
559 format!("{instance}/state-slot:storage%3Acomponent%3Ax-state%2Fstate%3Acount%2Fvalue")
560 );
561 }
562}