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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9380c0cdfa32e9c73ef10d8170f6c5f1d40803c6677c428eb3f62c28fe31b5ff
4
- data.tar.gz: fe4d5f31d47a79d6f55bfe27c16f2977cd6c25013ada9e42ab29b3cb008e7be2
3
+ metadata.gz: e8591c37675a79819c12ff7318a285b0ac9f612ef93121aff96d02a0740ac15e
4
+ data.tar.gz: 982298cc00be52d53953dadd80fa74f023bae1c39573a6c4f307dc64330dcd8c
5
5
  SHA512:
6
- metadata.gz: c11d0a7a4602a1a958774c02948e76cbe8a229afe6bf95caa3196d25416cd291b6f6fdae9a189730ebc86012c9fe8a5edbfb7e82b06c04853ddb124c482fa310
7
- data.tar.gz: ae9a6729144cd991aaf06acaa58a7c374956f07648f5e45f5086ccf661c18890cecf6508c327d6acec8e49ac33e50f542718f0936f1aeb61eccd9cc2efa83d40
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "cgi"
5
- require "uri"
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
- @client_cache[entry] == hash
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)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skybolt
4
- VERSION = "3.3.0"
4
+ VERSION = "3.4.0"
5
5
  end
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.3.0
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-30 00:00:00.000000000 Z
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