Files
bun.sh/src/bake/DevServer/Assets.zig
Zack Radisic 71e2161591 Split DevServer.zig into multiple files (#21299)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-07-23 05:29:22 +00:00

182 lines
6.7 KiB
Zig

/// Storage for hashed assets on `/_bun/asset/{hash}.ext`
pub const Assets = @This();
/// Keys are absolute paths, sharing memory with the keys in IncrementalGraph(.client)
/// Values are indexes into files
path_map: bun.StringArrayHashMapUnmanaged(EntryIndex),
/// Content-addressable store. Multiple paths can point to the same content
/// hash, which is tracked by the `refs` array. One reference is held to
/// contained StaticRoute instances when they are stored.
files: AutoArrayHashMapUnmanaged(u64, *StaticRoute),
/// Indexed by the same index of `files`. The value is never `0`.
refs: ArrayListUnmanaged(u32),
/// When mutating `files`'s keys, the map must be reindexed to function.
needs_reindex: bool = false,
pub const EntryIndex = bun.GenericIndex(u30, Assets);
fn owner(assets: *Assets) *DevServer {
return @alignCast(@fieldParentPtr("assets", assets));
}
pub fn getHash(assets: *Assets, path: []const u8) ?u64 {
assert(assets.owner().magic == .valid);
return if (assets.path_map.get(path)) |idx|
assets.files.keys()[idx.get()]
else
null;
}
/// When an asset is overwritten, it receives a new URL to get around browser caching.
/// The old URL is immediately revoked.
pub fn replacePath(
assets: *Assets,
/// not allocated
abs_path: []const u8,
/// Ownership is transferred to this function
contents: *const AnyBlob,
mime_type: *const MimeType,
/// content hash of the asset
content_hash: u64,
) !EntryIndex {
assert(assets.owner().magic == .valid);
defer assert(assets.files.count() == assets.refs.items.len);
const alloc = assets.owner().allocator;
debug.log("replacePath {} {} - {s}/{s} ({s})", .{
bun.fmt.quote(abs_path),
content_hash,
DevServer.asset_prefix,
&std.fmt.bytesToHex(std.mem.asBytes(&content_hash), .lower),
mime_type.value,
});
const gop = try assets.path_map.getOrPut(alloc, abs_path);
if (!gop.found_existing) {
// Locate a stable pointer for the file path
const stable_abs_path = (try assets.owner().client_graph.insertEmpty(abs_path, .unknown)).key;
gop.key_ptr.* = stable_abs_path;
} else {
const entry_index = gop.value_ptr.*;
// When there is one reference to the asset, the entry can be
// replaced in-place with the new asset.
if (assets.refs.items[entry_index.get()] == 1) {
const slice = assets.files.entries.slice();
const prev = slice.items(.value)[entry_index.get()];
prev.deref();
slice.items(.key)[entry_index.get()] = content_hash;
slice.items(.value)[entry_index.get()] = StaticRoute.initFromAnyBlob(contents, .{
.mime_type = mime_type,
.server = assets.owner().server orelse unreachable,
});
comptime assert(@TypeOf(slice.items(.hash)[0]) == void);
assets.needs_reindex = true;
return entry_index;
} else {
assets.refs.items[entry_index.get()] -= 1;
assert(assets.refs.items[entry_index.get()] > 0);
}
}
try assets.reindexIfNeeded(alloc);
const file_index_gop = try assets.files.getOrPut(alloc, content_hash);
if (!file_index_gop.found_existing) {
try assets.refs.append(alloc, 1);
file_index_gop.value_ptr.* = StaticRoute.initFromAnyBlob(contents, .{
.mime_type = mime_type,
.server = assets.owner().server orelse unreachable,
});
} else {
assets.refs.items[file_index_gop.index] += 1;
var contents_mut = contents.*;
contents_mut.detach();
}
gop.value_ptr.* = .init(@intCast(file_index_gop.index));
return gop.value_ptr.*;
}
/// Returns a pointer to insert the *StaticRoute. If `null` is returned, then it
/// means there is already data here.
pub fn putOrIncrementRefCount(assets: *Assets, content_hash: u64, ref_count: u32) !?**StaticRoute {
defer assert(assets.files.count() == assets.refs.items.len);
const file_index_gop = try assets.files.getOrPut(assets.owner().allocator, content_hash);
if (!file_index_gop.found_existing) {
try assets.refs.append(assets.owner().allocator, ref_count);
return file_index_gop.value_ptr;
} else {
assets.refs.items[file_index_gop.index] += ref_count;
return null;
}
}
pub fn unrefByHash(assets: *Assets, content_hash: u64, dec_count: u32) void {
const index = assets.files.getIndex(content_hash) orelse
Output.panic("Asset double unref: {s}", .{std.fmt.fmtSliceHexLower(std.mem.asBytes(&content_hash))});
assets.unrefByIndex(.init(@intCast(index)), dec_count);
}
pub fn unrefByIndex(assets: *Assets, index: EntryIndex, dec_count: u32) void {
defer assert(assets.files.count() == assets.refs.items.len);
assert(dec_count > 0);
assets.refs.items[index.get()] -= dec_count;
if (assets.refs.items[index.get()] == 0) {
assets.files.values()[index.get()].deref();
assets.files.swapRemoveAt(index.get());
_ = assets.refs.swapRemove(index.get());
}
}
pub fn unrefByPath(assets: *Assets, path: []const u8) void {
const entry = assets.path_map.fetchSwapRemove(path) orelse return;
assets.unrefByIndex(entry.value, 1);
}
pub fn reindexIfNeeded(assets: *Assets, alloc: Allocator) !void {
if (assets.needs_reindex) {
try assets.files.reIndex(alloc);
assets.needs_reindex = false;
}
}
pub fn get(assets: *Assets, content_hash: u64) ?*StaticRoute {
assert(assets.owner().magic == .valid);
assert(assets.files.count() == assets.refs.items.len);
return assets.files.get(content_hash);
}
pub fn deinit(assets: *Assets, alloc: Allocator) void {
assets.path_map.deinit(alloc);
for (assets.files.values()) |blob| blob.deref();
assets.files.deinit(alloc);
assets.refs.deinit(alloc);
}
pub fn memoryCost(assets: *Assets) usize {
var cost: usize = 0;
cost += memoryCostArrayHashMap(assets.path_map);
for (assets.files.values()) |blob| cost += blob.memoryCost();
cost += memoryCostArrayHashMap(assets.files);
cost += memoryCostArrayList(assets.refs);
return cost;
}
const bun = @import("bun");
const Output = bun.Output;
const assert = bun.assert;
const bake = bun.bake;
const jsc = bun.jsc;
const MimeType = bun.http.MimeType;
const StaticRoute = bun.api.server.StaticRoute;
const AnyBlob = jsc.WebCore.Blob.Any;
const DevServer = bake.DevServer;
const debug = DevServer.debug;
const memoryCostArrayHashMap = DevServer.memoryCostArrayHashMap;
const memoryCostArrayList = DevServer.memoryCostArrayList;
const std = @import("std");
const ArrayListUnmanaged = std.ArrayListUnmanaged;
const AutoArrayHashMapUnmanaged = std.AutoArrayHashMapUnmanaged;
const Allocator = std.mem.Allocator;