///|
/// Change class for a cache-aware WebBIM delivery.
pub(all) enum DeliveryChange {
AddedAsset
RemovedAsset
InvalidatedAsset
RetainedAsset
} derive(Debug, Eq)
///|
/// One manifest path and the revision impact that applies to it.
pub(all) struct DeliveryAssetChange {
path : String
change : DeliveryChange
before_checksum : Int?
after_checksum : Int?
affected_products : Array[Int]
byte_delta : Int
} derive(Debug)
///|
/// Cache invalidation plan generated from two model revisions.
pub(all) struct RevisionDelivery {
before_hash : Int
after_hash : Int
changes : Array[DeliveryAssetChange]
affected_products : Array[Int]
invalidated_count : Int
retained_count : Int
added_count : Int
removed_count : Int
bytes_added : Int
bytes_removed : Int
} derive(Debug)
///|
pub fn delivery_change_name(change : DeliveryChange) -> String {
match change {
AddedAsset => "added"
RemovedAsset => "removed"
InvalidatedAsset => "invalidated"
RetainedAsset => "retained"
}
}
///|
fn delivery_entry(
manifest : AssetManifest,
path : String,
) -> AssetManifestEntry? {
manifest.entry(path)
}
///|
fn delivery_product_union(
left : AssetManifestEntry?,
right : AssetManifestEntry?,
) -> Array[Int] {
let result : Array[Int] = []
match left {
Some(entry) => result.append(entry.product_ids)
None => ()
}
match right {
Some(entry) =>
for id in entry.product_ids {
if !result.contains(id) {
result.push(id)
}
}
None => ()
}
result
}
///|
fn delivery_change_for_entries(
before : AssetManifestEntry?,
after : AssetManifestEntry?,
) -> DeliveryChange {
match (before, after) {
(None, Some(_)) => AddedAsset
(Some(_), None) => RemovedAsset
(Some(left), Some(right)) if left.checksum != right.checksum =>
InvalidatedAsset
(Some(_), Some(_)) => RetainedAsset
(None, None) => RetainedAsset
}
}
///|
fn delivery_byte_delta(
before : AssetManifestEntry?,
after : AssetManifestEntry?,
) -> Int {
let old_size = match before {
Some(entry) => entry.byte_length
None => 0
}
let new_size = match after {
Some(entry) => entry.byte_length
None => 0
}
new_size - old_size
}
///|
fn delivery_paths(
before : AssetManifest,
after : AssetManifest,
) -> Array[String] {
let result = before.paths()
for path in after.paths() {
if !result.contains(path) {
result.push(path)
}
}
result
}
///|
/// Compare two generated manifests and classify every cache path.
pub fn revision_delivery(
before : Model,
after : Model,
options? : AssetManifestOptions = default_asset_manifest_options(),
) -> RevisionDelivery raise ParseError {
let old_manifest = asset_manifest(before, options~)
let new_manifest = asset_manifest(after, options~)
let changes : Array[DeliveryAssetChange] = []
for path in delivery_paths(old_manifest, new_manifest) {
let old_entry = delivery_entry(old_manifest, path)
let new_entry = delivery_entry(new_manifest, path)
let change = delivery_change_for_entries(old_entry, new_entry)
changes.push({
path,
change,
before_checksum: old_entry.map(entry => entry.checksum),
after_checksum: new_entry.map(entry => entry.checksum),
affected_products: delivery_product_union(old_entry, new_entry),
byte_delta: delivery_byte_delta(old_entry, new_entry),
})
}
let invalidated_count = changes.fold(init=0, (count, item) => {
if item.change == InvalidatedAsset {
count + 1
} else {
count
}
})
let retained_count = changes.fold(init=0, (count, item) => {
if item.change == RetainedAsset {
count + 1
} else {
count
}
})
let added_count = changes.fold(init=0, (count, item) => {
if item.change == AddedAsset {
count + 1
} else {
count
}
})
let removed_count = changes.fold(init=0, (count, item) => {
if item.change == RemovedAsset {
count + 1
} else {
count
}
})
let impact = revision_impact(before, after)
{
before_hash: old_manifest.model_hash,
after_hash: new_manifest.model_hash,
changes,
affected_products: impact.affected_products,
invalidated_count,
retained_count,
added_count,
removed_count,
bytes_added: changes.fold(init=0, (sum, item) => {
if item.byte_delta > 0 {
sum + item.byte_delta
} else {
sum
}
}),
bytes_removed: changes.fold(init=0, (sum, item) => {
if item.byte_delta < 0 {
sum - item.byte_delta
} else {
sum
}
}),
}
}
///|
pub fn RevisionDelivery::is_empty(self : RevisionDelivery) -> Bool {
self.before_hash == self.after_hash &&
self.invalidated_count == 0 &&
self.added_count == 0 &&
self.removed_count == 0
}
///|
pub fn RevisionDelivery::change_count(self : RevisionDelivery) -> Int {
self.changes.length()
}
///|
pub fn RevisionDelivery::paths_for(
self : RevisionDelivery,
change : DeliveryChange,
) -> Array[String] {
self.changes.filter(item => item.change == change).map(item => item.path)
}
///|
pub fn RevisionDelivery::products_for_path(
self : RevisionDelivery,
path : String,
) -> Array[Int] {
for item in self.changes {
if item.path == path {
return item.affected_products
}
}
[]
}
///|
pub fn RevisionDelivery::requires_geometry_refresh(
self : RevisionDelivery,
) -> Bool {
self.changes.any(item => {
item.change == InvalidatedAsset &&
(
item.path.has_prefix("assets/geometry/") ||
item.path.has_prefix("assets/spatial/")
)
})
}
///|
pub fn RevisionDelivery::requires_property_refresh(
self : RevisionDelivery,
) -> Bool {
self.changes.any(item => {
item.change == InvalidatedAsset &&
item.path.has_prefix("assets/properties/")
})
}
///|
pub fn RevisionDelivery::byte_delta(self : RevisionDelivery) -> Int {
self.bytes_added - self.bytes_removed
}
///|
pub fn RevisionDelivery::headline(self : RevisionDelivery) -> String {
"changes=" +
self.change_count().to_string() +
" invalidated=" +
self.invalidated_count.to_string() +
" retained=" +
self.retained_count.to_string() +
" added=" +
self.added_count.to_string() +
" removed=" +
self.removed_count.to_string() +
" byte_delta=" +
self.byte_delta().to_string()
}
///|
fn delivery_checksum_json(value : Int?) -> String {
match value {
Some(checksum) => checksum.to_string()
None => "null"
}
}
///|
pub fn RevisionDelivery::to_json(self : RevisionDelivery) -> String {
let changes = self.changes
.map(item => {
"{\"path\":\"" +
item.path +
"\",\"change\":\"" +
delivery_change_name(item.change) +
"\",\"before_checksum\":" +
delivery_checksum_json(item.before_checksum) +
",\"after_checksum\":" +
delivery_checksum_json(item.after_checksum) +
",\"byte_delta\":" +
item.byte_delta.to_string() +
",\"affected_products\":[" +
item.affected_products.map(id => id.to_string()).join(",") +
"]}"
})
.join(",")
"{\"before_hash\":" +
self.before_hash.to_string() +
",\"after_hash\":" +
self.after_hash.to_string() +
",\"affected_products\":[" +
self.affected_products.map(id => id.to_string()).join(",") +
"],\"changes\":[" +
changes +
"]}"
}
///|
pub fn RevisionDelivery::to_markdown(self : RevisionDelivery) -> String {
let builder = StringBuilder()
builder <+ "# Revision delivery\n\n"
builder <+ "- \{self.headline()}\n"
builder <+ "- Geometry refresh: \{self.requires_geometry_refresh()}\n"
builder <+ "- Property refresh: \{self.requires_property_refresh()}\n"
builder <+ "- Affected products: \{self.affected_products.length()}\n\n"
builder <+ "| Path | Change | Affected products | Byte delta |\n"
builder <+ "| --- | --- | ---: | ---: |\n"
for item in self.changes {
builder <+
"| \{item.path} | \{delivery_change_name(item.change)} | \{item.affected_products.length()} | \{item.byte_delta} |\n"
}
builder.to_string()
}
///|
pub fn revision_delivery_paths(
before : Model,
after : Model,
) -> Array[String] raise ParseError {
revision_delivery(before, after).changes.map(item => item.path)
}
///|
pub fn revision_delivery_invalidated_paths(
before : Model,
after : Model,
) -> Array[String] raise ParseError {
revision_delivery(before, after).paths_for(InvalidatedAsset)
}
///|
pub fn revision_delivery_summary(
before : Model,
after : Model,
) -> Array[String] raise ParseError {
let delivery = revision_delivery(before, after)
[
delivery.headline(),
"empty=" + delivery.is_empty().to_string(),
"geometry_refresh=" + delivery.requires_geometry_refresh().to_string(),
"property_refresh=" + delivery.requires_property_refresh().to_string(),
]
}