skybolt 3.3.0 → 3.4.0
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.
- checksums.yaml +4 -4
- data/lib/skybolt/cache_digest.rb +132 -0
- data/lib/skybolt/renderer.rb +5 -22
- data/lib/skybolt/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8591c37675a79819c12ff7318a285b0ac9f612ef93121aff96d02a0740ac15e
|
|
4
|
+
data.tar.gz: 982298cc00be52d53953dadd80fa74f023bae1c39573a6c4f307dc64330dcd8c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 683ea1a4a920b8bbda7cba0dde8ab621450c44699ca1557881b69ceb1ca015d4fb5f705cb05bcfebd04ab899a89944014bcf56edac7b9b8e663a0da08b0b32cd
|
|
7
|
+
data.tar.gz: 6b1ebd38514422a6ff28a35c688ebf46941599463daa9b2a791189cf47fbaf5394cb5a435d814ebaa4545ffa350aa339caf3e0a3cbec5667c1692b811adeb5b6
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
|
|
5
|
+
module Skybolt
|
|
6
|
+
# Cache Digest implementation using a Cuckoo filter.
|
|
7
|
+
#
|
|
8
|
+
# This is a read-only parser for digests created by the JavaScript client.
|
|
9
|
+
# It's used to determine which assets the client already has cached.
|
|
10
|
+
class CacheDigest
|
|
11
|
+
FINGERPRINT_BITS = 12
|
|
12
|
+
BUCKET_SIZE = 4
|
|
13
|
+
|
|
14
|
+
# Create a CacheDigest from a base64-encoded string.
|
|
15
|
+
#
|
|
16
|
+
# @param digest [String] URL-safe base64-encoded digest from sb_digest cookie
|
|
17
|
+
# @return [CacheDigest] A valid or invalid CacheDigest instance
|
|
18
|
+
def self.from_base64(digest)
|
|
19
|
+
new(digest)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Compute FNV-1a hash of a string (32-bit).
|
|
23
|
+
#
|
|
24
|
+
# @param str [String] Input string
|
|
25
|
+
# @return [Integer] 32-bit hash value
|
|
26
|
+
def self.fnv1a(str)
|
|
27
|
+
hash = 2166136261
|
|
28
|
+
str.each_byte do |byte|
|
|
29
|
+
hash ^= byte
|
|
30
|
+
hash = (hash * 16777619) & 0xFFFFFFFF
|
|
31
|
+
end
|
|
32
|
+
hash
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Compute fingerprint for Cuckoo filter.
|
|
36
|
+
#
|
|
37
|
+
# @param str [String] Input string
|
|
38
|
+
# @return [Integer] Fingerprint in range [1, 4095]
|
|
39
|
+
def self.fingerprint(str)
|
|
40
|
+
hash = fnv1a(str)
|
|
41
|
+
fp = hash & ((1 << FINGERPRINT_BITS) - 1)
|
|
42
|
+
fp == 0 ? 1 : fp
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Compute alternate bucket index for Cuckoo filter.
|
|
46
|
+
#
|
|
47
|
+
# @param bucket [Integer] Current bucket index
|
|
48
|
+
# @param fp [Integer] Fingerprint value
|
|
49
|
+
# @param num_buckets [Integer] Total number of buckets
|
|
50
|
+
# @return [Integer] Alternate bucket index
|
|
51
|
+
def self.compute_alternate_bucket(bucket, fp, num_buckets)
|
|
52
|
+
fp_hash = fnv1a(fp.to_s)
|
|
53
|
+
bucket_mask = num_buckets - 1
|
|
54
|
+
offset = (fp_hash | 1) & bucket_mask
|
|
55
|
+
(bucket ^ offset) & bucket_mask
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Boolean] Whether this is a valid digest
|
|
59
|
+
attr_reader :valid
|
|
60
|
+
alias valid? valid
|
|
61
|
+
|
|
62
|
+
# Check if an item exists in the digest.
|
|
63
|
+
#
|
|
64
|
+
# @param item [String] Item to look up (e.g., "src/css/main.css:hash123")
|
|
65
|
+
# @return [Boolean] True if item might be in the filter (may have false positives)
|
|
66
|
+
def lookup(item)
|
|
67
|
+
return false unless @valid
|
|
68
|
+
|
|
69
|
+
fp = self.class.fingerprint(item)
|
|
70
|
+
i1 = primary_bucket(item)
|
|
71
|
+
i2 = self.class.compute_alternate_bucket(i1, fp, @num_buckets)
|
|
72
|
+
bucket_contains?(i1, fp) || bucket_contains?(i2, fp)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def initialize(digest)
|
|
78
|
+
@valid = false
|
|
79
|
+
@buckets = []
|
|
80
|
+
@num_buckets = 0
|
|
81
|
+
|
|
82
|
+
parse_digest(digest)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parse_digest(digest)
|
|
86
|
+
return if digest.nil? || digest.empty?
|
|
87
|
+
|
|
88
|
+
# Handle URL-safe base64
|
|
89
|
+
normalized = digest.tr("-_", "+/")
|
|
90
|
+
# Add padding if needed
|
|
91
|
+
normalized += "=" * ((4 - normalized.length % 4) % 4)
|
|
92
|
+
|
|
93
|
+
begin
|
|
94
|
+
data = Base64.strict_decode64(normalized)
|
|
95
|
+
rescue ArgumentError
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
return if data.bytesize < 5
|
|
100
|
+
|
|
101
|
+
# Check version (must be 1)
|
|
102
|
+
return if data.getbyte(0) != 1
|
|
103
|
+
|
|
104
|
+
@num_buckets = (data.getbyte(1) << 8) | data.getbyte(2)
|
|
105
|
+
num_fingerprints = @num_buckets * BUCKET_SIZE
|
|
106
|
+
|
|
107
|
+
@buckets = []
|
|
108
|
+
num_fingerprints.times do |i|
|
|
109
|
+
offset = 5 + i * 2
|
|
110
|
+
if offset + 1 < data.bytesize
|
|
111
|
+
@buckets << ((data.getbyte(offset) << 8) | data.getbyte(offset + 1))
|
|
112
|
+
else
|
|
113
|
+
@buckets << 0
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@valid = true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def primary_bucket(str)
|
|
121
|
+
self.class.fnv1a(str) % @num_buckets
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def bucket_contains?(bucket_index, fp)
|
|
125
|
+
offset = bucket_index * BUCKET_SIZE
|
|
126
|
+
BUCKET_SIZE.times do |i|
|
|
127
|
+
return true if @buckets[offset + i] == fp
|
|
128
|
+
end
|
|
129
|
+
false
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
data/lib/skybolt/renderer.rb
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "cgi"
|
|
5
|
-
|
|
5
|
+
require_relative "cache_digest"
|
|
6
6
|
|
|
7
7
|
module Skybolt
|
|
8
8
|
# Skybolt asset renderer.
|
|
@@ -16,8 +16,10 @@ module Skybolt
|
|
|
16
16
|
def initialize(render_map_path, cookies: nil, cdn_url: nil)
|
|
17
17
|
json = File.read(render_map_path)
|
|
18
18
|
@map = JSON.parse(json)
|
|
19
|
-
@client_cache = parse_cookie((cookies || {})["sb_assets"] || "")
|
|
20
19
|
@cdn_url = cdn_url&.chomp("/")
|
|
20
|
+
|
|
21
|
+
# Parse Cache Digest from sb_digest cookie
|
|
22
|
+
@cache_digest = CacheDigest.from_base64((cookies || {})["sb_digest"] || "")
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
# Render CSS asset.
|
|
@@ -197,26 +199,7 @@ module Skybolt
|
|
|
197
199
|
end
|
|
198
200
|
|
|
199
201
|
def cached?(entry, hash)
|
|
200
|
-
@
|
|
201
|
-
end
|
|
202
|
-
|
|
203
|
-
def parse_cookie(cookie)
|
|
204
|
-
return {} if cookie.empty?
|
|
205
|
-
|
|
206
|
-
decoded = URI.decode_www_form_component(cookie)
|
|
207
|
-
cache = {}
|
|
208
|
-
|
|
209
|
-
decoded.split(",").each do |pair|
|
|
210
|
-
# Find last colon (hash doesn't contain colons, but paths might)
|
|
211
|
-
colon_pos = pair.rindex(":")
|
|
212
|
-
next if colon_pos.nil?
|
|
213
|
-
|
|
214
|
-
name = pair[0...colon_pos]
|
|
215
|
-
hash = pair[(colon_pos + 1)..]
|
|
216
|
-
cache[name] = hash
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
cache
|
|
202
|
+
@cache_digest.lookup("#{entry}:#{hash}")
|
|
220
203
|
end
|
|
221
204
|
|
|
222
205
|
def build_tag(tag, attrs)
|
data/lib/skybolt/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: skybolt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jens Roland
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-12-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Reads the render-map.json generated by @skybolt/vite-plugin and outputs
|
|
14
14
|
optimized HTML tags with intelligent caching via Service Workers.
|
|
@@ -21,6 +21,7 @@ files:
|
|
|
21
21
|
- LICENSE
|
|
22
22
|
- README.md
|
|
23
23
|
- lib/skybolt.rb
|
|
24
|
+
- lib/skybolt/cache_digest.rb
|
|
24
25
|
- lib/skybolt/renderer.rb
|
|
25
26
|
- lib/skybolt/version.rb
|
|
26
27
|
homepage: https://github.com/JensRoland/skybolt-ruby
|