1313
1414open Types
1515
16+ // Fetch API bindings (browser global)
17+ module Fetch = {
18+ type response
19+
20+ @val external fetch : string => promise <response > = "fetch"
21+
22+ module Response = {
23+ @get external ok : response => bool = "ok"
24+ @send external json : response => promise <JSON .t > = "json"
25+ }
26+ }
27+
1628type databaseVersion = {
1729 version : string ,
1830 lastUpdated : string ,
@@ -31,144 +43,7 @@ type updateStatus =
3143 | UpdateRequired (string ) // Reason for requirement
3244 | UpdateFailed (string ) // Error message
3345
34- // Check for database updates from GitHub releases
35- let checkForUpdates = async (currentVersion : string ): updateStatus => {
36- try {
37- // Fetch latest release from GitHub API
38- let response = await Fetch .fetch (
39- "https://api.github.com/repos/hyperpolymath/fireflag/releases/latest" ,
40- )
41-
42- if ! response -> Fetch .Response .ok {
43- UpdateFailed ("Failed to fetch update information" )
44- } else {
45- let json = await response -> Fetch .Response .json
46- let latestVersion = json
47- -> Js .Json .decodeObject
48- -> Option .flatMap (obj => Js .Dict .get (obj , "tag_name" ))
49- -> Option .flatMap (Js .Json .decodeString )
50- -> Option .getOr (currentVersion )
51-
52- if latestVersion > currentVersion {
53- UpdateAvailable ({
54- version : latestVersion ,
55- lastUpdated : Js .Date .now ()-> Js .Date .fromFloat -> Js .Date .toISOString ,
56- minimumGeckoVersion : "109.0" ,
57- maximumGeckoVersion : None ,
58- })
59- } else {
60- UpToDate
61- }
62- }
63- } catch {
64- | exn => UpdateFailed (Js .Exn .message (exn )-> Option .getOr ("Unknown error" ))
65- }
66- }
67-
68- // Download and validate updated database
69- let downloadDatabase = async (version : databaseVersion ): result <flagDatabase , string > => {
70- try {
71- let url = ` https://github.com/hyperpolymath/fireflag/releases/download/${version.version}/flags-database.json`
72-
73- let response = await Fetch .fetch (url )
74-
75- if ! response -> Fetch .Response .ok {
76- Error ("Failed to download database" )
77- } else {
78- let json = await response -> Fetch .Response .json
79- let db = parseFlagDatabase (json ) // Validate schema
80-
81- Ok (db )
82- }
83- } catch {
84- | exn => Error (Js .Exn .message (exn )-> Option .getOr ("Download failed" ))
85- }
86- }
87-
88- // Validate flag availability against current browser version
89- let validateFlagForBrowser = (flag : flag , browserVersion : string ): bool => {
90- // Check minimum version
91- let meetsMin = switch flag .geckoMinVersion {
92- | Some (minVer ) => compareVersions (browserVersion , minVer ) >= 0
93- | None => true
94- }
95-
96- // Check maximum version (if flag was removed)
97- let meetsMax = switch flag .geckoMaxVersion {
98- | Some (maxVer ) => compareVersions (browserVersion , maxVer ) <= 0
99- | None => true
100- }
101-
102- meetsMin && meetsMax
103- }
104-
105- // Compare semantic versions (returns -1, 0, or 1)
106- let compareVersions = (a : string , b : string ): int => {
107- let parseVersion = v =>
108- v
109- -> String .split ("." )
110- -> Array .map (s => Int .fromString (s )-> Option .getOr (0 ))
111-
112- let aParts = parseVersion (a )
113- let bParts = parseVersion (b )
114- let maxLen = Int .max (Array .length (aParts ), Array .length (bParts ))
115-
116- let rec compare = (i : int ): int => {
117- if i >= maxLen {
118- 0
119- } else {
120- let aVal = Array .get (aParts , i )-> Option .getOr (0 )
121- let bVal = Array .get (bParts , i )-> Option .getOr (0 )
122-
123- if aVal < bVal {
124- - 1
125- } else if aVal > bVal {
126- 1
127- } else {
128- compare (i + 1 )
129- }
130- }
131- }
132-
133- compare (0 )
134- }
135-
136- // Filter database for current browser version
137- let filterDatabaseForBrowser = (db : flagDatabase , browserVersion : string ): flagDatabase => {
138- {
139- ... db ,
140- flags : db .flags -> Array .filter (flag => validateFlagForBrowser (flag , browserVersion )),
141- }
142- }
143-
144- // Detect deprecated flags (have maxVersion)
145- let findDeprecatedFlags = (db : flagDatabase ): array <flag > => {
146- db .flags -> Array .filter (flag => flag .geckoMaxVersion -> Option .isSome )
147- }
148-
149- // Detect flags unavailable in current version
150- let findUnavailableFlags = (db : flagDatabase , browserVersion : string ): array <flag > => {
151- db .flags -> Array .filter (flag => ! validateFlagForBrowser (flag , browserVersion ))
152- }
153-
154- // Auto-update workflow
155- let autoUpdate = async (currentVersion : string , enableAutoUpdate : bool ): result <
156- flagDatabase ,
157- string ,
158- > => {
159- if ! enableAutoUpdate {
160- Error ("Auto-update disabled" )
161- } else {
162- let status = await checkForUpdates (currentVersion )
163-
164- switch status {
165- | UpToDate => Error ("Already up to date" )
166- | UpdateFailed (msg ) => Error (msg )
167- | UpdateRequired (reason ) => Error (` Update required: ${reason}` )
168- | UpdateAvailable (version ) => await downloadDatabase (version )
169- }
170- }
171- }
46+ // --- JSON parsing helpers (must be defined before callers) ---
17247
17348// Parse safety level string to variant
17449let parseSafetyLevel = (s : string ): safetyLevel =>
@@ -313,3 +188,147 @@ let parseFlagDatabase = (json: Js.Json.t): flagDatabase =>
313188
314189 {version , lastUpdated , categories , flags }
315190 }
191+
192+ // --- Version comparison (must be before validateFlagForBrowser) ---
193+
194+ // Compare semantic versions (returns -1, 0, or 1)
195+ let compareVersions = (a : string , b : string ): int => {
196+ let parseVersion = v =>
197+ v
198+ -> String .split ("." )
199+ -> Array .map (s => Int .fromString (s )-> Option .getOr (0 ))
200+
201+ let aParts = parseVersion (a )
202+ let bParts = parseVersion (b )
203+ let lenA = Array .length (aParts )
204+ let lenB = Array .length (bParts )
205+ let maxLen = lenA > lenB ? lenA : lenB
206+
207+ let rec compare = (i : int ): int => {
208+ if i >= maxLen {
209+ 0
210+ } else {
211+ let aVal = Array .get (aParts , i )-> Option .getOr (0 )
212+ let bVal = Array .get (bParts , i )-> Option .getOr (0 )
213+
214+ if aVal < bVal {
215+ - 1
216+ } else if aVal > bVal {
217+ 1
218+ } else {
219+ compare (i + 1 )
220+ }
221+ }
222+ }
223+
224+ compare (0 )
225+ }
226+
227+ // --- Network and validation functions ---
228+
229+ // Check for database updates from GitHub releases
230+ let checkForUpdates = async (currentVersion : string ): updateStatus => {
231+ try {
232+ let response = await Fetch .fetch (
233+ "https://api.github.com/repos/hyperpolymath/fireflag/releases/latest" ,
234+ )
235+
236+ if ! (response -> Fetch .Response .ok ) {
237+ UpdateFailed ("Failed to fetch update information" )
238+ } else {
239+ let json = await response -> Fetch .Response .json
240+ let latestVersion = json
241+ -> Js .Json .decodeObject
242+ -> Option .flatMap (obj => Js .Dict .get (obj , "tag_name" ))
243+ -> Option .flatMap (Js .Json .decodeString )
244+ -> Option .getOr (currentVersion )
245+
246+ if latestVersion > currentVersion {
247+ UpdateAvailable ({
248+ version : latestVersion ,
249+ lastUpdated : Js .Date .now ()-> Js .Date .fromFloat -> Js .Date .toISOString ,
250+ minimumGeckoVersion : "109.0" ,
251+ maximumGeckoVersion : None ,
252+ })
253+ } else {
254+ UpToDate
255+ }
256+ }
257+ } catch {
258+ | JsExn (exn ) => UpdateFailed (JsExn .message (exn )-> Option .getOr ("Unknown error" ))
259+ | _ => UpdateFailed ("Unknown error" )
260+ }
261+ }
262+
263+ // Download and validate updated database
264+ let downloadDatabase = async (version : databaseVersion ): result <flagDatabase , string > => {
265+ try {
266+ let url = ` https://github.com/hyperpolymath/fireflag/releases/download/${version.version}/flags-database.json`
267+
268+ let response = await Fetch .fetch (url )
269+
270+ if ! (response -> Fetch .Response .ok ) {
271+ Error ("Failed to download database" )
272+ } else {
273+ let json = await response -> Fetch .Response .json
274+ let db = parseFlagDatabase (json )
275+
276+ Ok (db )
277+ }
278+ } catch {
279+ | JsExn (exn ) => Error (JsExn .message (exn )-> Option .getOr ("Download failed" ))
280+ | _ => Error ("Download failed" )
281+ }
282+ }
283+
284+ // Validate flag availability against current browser version
285+ let validateFlagForBrowser = (flag : flag , browserVersion : string ): bool => {
286+ let meetsMin = switch flag .geckoMinVersion {
287+ | Some (minVer ) => compareVersions (browserVersion , minVer ) >= 0
288+ | None => true
289+ }
290+
291+ let meetsMax = switch flag .geckoMaxVersion {
292+ | Some (maxVer ) => compareVersions (browserVersion , maxVer ) <= 0
293+ | None => true
294+ }
295+
296+ meetsMin && meetsMax
297+ }
298+
299+ // Filter database for current browser version
300+ let filterDatabaseForBrowser = (db : flagDatabase , browserVersion : string ): flagDatabase => {
301+ {
302+ ... db ,
303+ flags : db .flags -> Array .filter (flag => validateFlagForBrowser (flag , browserVersion )),
304+ }
305+ }
306+
307+ // Detect deprecated flags (have maxVersion)
308+ let findDeprecatedFlags = (db : flagDatabase ): array <flag > => {
309+ db .flags -> Array .filter (flag => flag .geckoMaxVersion -> Option .isSome )
310+ }
311+
312+ // Detect flags unavailable in current version
313+ let findUnavailableFlags = (db : flagDatabase , browserVersion : string ): array <flag > => {
314+ db .flags -> Array .filter (flag => ! validateFlagForBrowser (flag , browserVersion ))
315+ }
316+
317+ // Auto-update workflow
318+ let autoUpdate = async (currentVersion : string , enableAutoUpdate : bool ): result <
319+ flagDatabase ,
320+ string ,
321+ > => {
322+ if ! enableAutoUpdate {
323+ Error ("Auto-update disabled" )
324+ } else {
325+ let status = await checkForUpdates (currentVersion )
326+
327+ switch status {
328+ | UpToDate => Error ("Already up to date" )
329+ | UpdateFailed (msg ) => Error (msg )
330+ | UpdateRequired (reason ) => Error (` Update required: ${reason}` )
331+ | UpdateAvailable (version ) => await downloadDatabase (version )
332+ }
333+ }
334+ }
0 commit comments