···11+(** Bridge between Note FRP signals and Lwt async operations. *)
22+33+type 'a state =
44+ | Idle
55+ | Loading of string
66+ | Ready of 'a
77+ | Error of string
88+99+let state_eq eq a b = match a, b with
1010+ | Idle, Idle -> true
1111+ | Loading a, Loading b -> String.equal a b
1212+ | Ready a, Ready b -> eq a b
1313+ | Error a, Error b -> String.equal a b
1414+ | _ -> false
1515+1616+let async_bind
1717+ (type a b)
1818+ ?(eq : b -> b -> bool = fun _ _ -> false)
1919+ (signal : a option Note.signal)
2020+ (f : a -> (string -> unit) -> b Lwt.t)
2121+ : b state Note.signal =
2222+ let (out_signal, set_out) = Note.S.create ~eq:(state_eq eq) Idle in
2323+ let generation = ref 0 in
2424+ (* Observe the input signal: when it changes, kick off the async work *)
2525+ let logr = Note.Logr.create (
2626+ Note.Logr.app (Note.Logr.const (fun v ->
2727+ incr generation;
2828+ let my_gen = !generation in
2929+ match v with
3030+ | None ->
3131+ set_out Idle
3232+ | Some input ->
3333+ set_out (Loading "starting...");
3434+ let progress msg =
3535+ if !generation = my_gen then
3636+ set_out (Loading msg)
3737+ in
3838+ Lwt.async (fun () ->
3939+ Lwt.catch
4040+ (fun () ->
4141+ let open Lwt.Syntax in
4242+ let+ result = f input progress in
4343+ if !generation = my_gen then
4444+ set_out (Ready result))
4545+ (fun exn ->
4646+ if !generation = my_gen then
4747+ set_out (Error (Printexc.to_string exn));
4848+ Lwt.return_unit))))
4949+ (Note.S.obs signal)
5050+ ) in
5151+ Note.Logr.hold logr;
5252+ out_signal
+23
lib/frp_async.mli
···11+(** Bridge between Note FRP signals and Lwt async operations. *)
22+33+type 'a state =
44+ | Idle (** No input / initial state *)
55+ | Loading of string (** Async operation in progress, with status message *)
66+ | Ready of 'a (** Operation completed successfully *)
77+ | Error of string (** Operation failed *)
88+99+val async_bind :
1010+ ?eq:('b -> 'b -> bool) ->
1111+ 'a option Note.signal ->
1212+ ('a -> (string -> unit) -> 'b Lwt.t) ->
1313+ 'b state Note.signal
1414+(** [async_bind signal f] maps a signal through an async function.
1515+1616+ When [signal] changes to [Some v], starts [f v progress] asynchronously.
1717+ [progress] can be called to update the loading status message.
1818+ The output signal transitions through [Loading msg] → [Ready result].
1919+2020+ When [signal] changes to [None], the output is [Idle].
2121+2222+ If [signal] changes again before [f] completes, the previous
2323+ computation's result is silently discarded (stale cancellation). *)
+121
lib/tessera_zarr_jsoo.ml
···11+open Js_of_ocaml
22+33+(* Async HTTP fetch using XHR with Lwt promises.
44+ Multiple shard fetches run in parallel via Lwt.join. *)
55+let fetch url ?off ?len () =
66+ let xhr = XmlHttpRequest.create () in
77+ xhr##.responseType := Js.string "arraybuffer";
88+ xhr##_open (Js.string "GET") (Js.string url) Js._true;
99+ (match off, len with
1010+ | Some o, Some l ->
1111+ xhr##setRequestHeader (Js.string "Range")
1212+ (Js.string (Printf.sprintf "bytes=%d-%d" o (o + l - 1)))
1313+ | None, Some l ->
1414+ (* Suffix range: last l bytes *)
1515+ xhr##setRequestHeader (Js.string "Range")
1616+ (Js.string (Printf.sprintf "bytes=-%d" l))
1717+ | _ -> ());
1818+ let (p, resolver) = Lwt.wait () in
1919+ xhr##.onload := Dom.handler (fun _ ->
2020+ let data = match xhr##.status with
2121+ | 200 | 206 ->
2222+ Js.Opt.case
2323+ (File.CoerceTo.arrayBuffer xhr##.response)
2424+ (fun () -> failwith ("Empty response from " ^ url))
2525+ (fun b -> Typed_array.String.of_arrayBuffer b)
2626+ | code ->
2727+ failwith (Printf.sprintf "HTTP %d fetching %s" code url)
2828+ in
2929+ Lwt.wakeup resolver data;
3030+ Js._false);
3131+ xhr##.onerror := Dom.handler (fun _ ->
3232+ Lwt.wakeup_exn resolver
3333+ (Failure (Printf.sprintf "XHR error fetching %s" url));
3434+ Js._false);
3535+ xhr##send Js.null;
3636+ p
3737+3838+(* Synchronous HTTP fetch for web worker context.
3939+ Blocks the worker thread but allows notebook cells to get results directly. *)
4040+let fetch_sync url ?off ?len () =
4141+ let xhr = XmlHttpRequest.create () in
4242+ xhr##.responseType := Js.string "arraybuffer";
4343+ xhr##_open (Js.string "GET") (Js.string url) Js._false;
4444+ (match off, len with
4545+ | Some o, Some l ->
4646+ xhr##setRequestHeader (Js.string "Range")
4747+ (Js.string (Printf.sprintf "bytes=%d-%d" o (o + l - 1)))
4848+ | None, Some l ->
4949+ xhr##setRequestHeader (Js.string "Range")
5050+ (Js.string (Printf.sprintf "bytes=-%d" l))
5151+ | _ -> ());
5252+ xhr##send Js.null;
5353+ match xhr##.status with
5454+ | 200 | 206 ->
5555+ Lwt.return (
5656+ Js.Opt.case
5757+ (File.CoerceTo.arrayBuffer xhr##.response)
5858+ (fun () -> failwith ("Empty response from " ^ url))
5959+ (fun b -> Typed_array.String.of_arrayBuffer b))
6060+ | code ->
6161+ failwith (Printf.sprintf "HTTP %d fetching %s" code url)
6262+6363+(* Zstd decompressor via fzstd.js (must be loaded via importScripts).
6464+ fzstd.decompress takes a Uint8Array and returns a Uint8Array. *)
6565+let zstd_decompress data =
6666+ let fzstd = Js.Unsafe.global##.fzstd in
6767+ if not (Js.Optdef.test fzstd) then
6868+ failwith "fzstd not loaded. Add importScripts for fzstd in setup cell.";
6969+ (* Convert OCaml string → Uint8Array *)
7070+ let input = Typed_array.Bytes.to_uint8Array (Bytes.of_string data) in
7171+ (* Call fzstd.decompress(uint8array) → uint8array *)
7272+ let output : Typed_array.uint8Array Js.t =
7373+ Js.Unsafe.meth_call fzstd "decompress"
7474+ [| Js.Unsafe.inject input |] in
7575+ (* Convert Uint8Array → OCaml string via ArrayBuffer *)
7676+ Typed_array.String.of_uint8Array output
7777+7878+let codecs name =
7979+ match name with
8080+ | "zstd" -> Some zstd_decompress
8181+ | _ -> None
8282+8383+(* Build the store URL from a base URL and year.
8484+ MegaZarr: base_url ends in ".zarr", store is just the base_url
8585+ Legacy: base_url is a directory, store is "{base_url}/{year}.zarr" *)
8686+let store_url base_url year =
8787+ if String.length base_url > 5 &&
8888+ String.sub base_url (String.length base_url - 5) 5 = ".zarr" then
8989+ (* MegaZarr: URL already points to the store *)
9090+ base_url
9191+ else
9292+ (* Legacy per-year: append {year}.zarr *)
9393+ Printf.sprintf "%s/%d.zarr" base_url year
9494+9595+let open_store ?(base_url = "https://dl2.geotessera.org/zarr/v2/store.zarr")
9696+ ?(year = 2024) () =
9797+ let url = store_url base_url year in
9898+ Zarr_v3.Store.open_store ~fetch ~codecs url
9999+100100+let fetch_region ?progress ?(base_url = "https://dl2.geotessera.org/zarr/v2/store.zarr")
101101+ ?(year = 2024) bbox =
102102+ let open Lwt.Syntax in
103103+ let* store = open_store ~base_url ~year () in
104104+ Tessera_zarr.fetch_region ?progress ~year ~store bbox
105105+106106+(* Synchronous version for notebook cells.
107107+ Uses synchronous XHR so all Lwt promises resolve immediately. *)
108108+let fetch_region_sync ?(base_url = "https://dl2.geotessera.org/zarr/v2/store.zarr")
109109+ ?(year = 2024) bbox =
110110+ let url = store_url base_url year in
111111+ let store_lwt = Zarr_v3.Store.open_store ~fetch:fetch_sync ~codecs url in
112112+ let store = match Lwt.poll store_lwt with
113113+ | Some s -> s
114114+ | None -> failwith "Tessera_zarr_jsoo: unexpected async in store open"
115115+ in
116116+ let result_lwt = Tessera_zarr.fetch_region ~year ~store bbox in
117117+ match Lwt.poll result_lwt with
118118+ | Some v -> v
119119+ | None -> failwith "Tessera_zarr_jsoo: unexpected async in fetch_region"
120120+121121+module Frp_async = Frp_async
+43
lib/tessera_zarr_jsoo.mli
···11+(** Browser backend for tessera-zarr.
22+33+ {b Warning:} This library was vibe-coded with AI assistance and has not
44+ been thoroughly reviewed or tested. Use at your own risk and expect
55+ breaking changes.
66+77+ Provides async and sync HTTP fetch with byte-range support,
88+ convenience wrappers for opening the GeoTessera Zarr store,
99+ and an FRP bridge for reactive notebooks.
1010+1111+ Defaults to the MegaZarr store ([store.zarr]) which is a single
1212+ store with year as a dimension. Pass [~base_url] ending in
1313+ [".zarr"] for MegaZarr, or a directory URL for legacy per-year
1414+ stores. *)
1515+1616+val fetch : Zarr_v3.Store.fetch
1717+(** Async HTTP fetch via [XMLHttpRequest] with [Range] header support. *)
1818+1919+val fetch_sync : Zarr_v3.Store.fetch
2020+(** Synchronous HTTP fetch. Returns immediately-resolved {!Lwt.t}. *)
2121+2222+val codecs : Zarr_v3.Store.codec_registry
2323+(** Codec registry. Currently returns [None] for all codecs. *)
2424+2525+val open_store : ?base_url:string -> ?year:int -> unit -> Zarr_v3.Store.store Lwt.t
2626+(** Open the GeoTessera Zarr store (async fetch).
2727+ For MegaZarr (default), [year] selects the time dimension.
2828+ For legacy per-year stores, [year] selects which store to open. *)
2929+3030+val fetch_region : ?progress:(string -> unit) -> ?base_url:string -> ?year:int ->
3131+ Geotessera.bbox -> (Linalg.mat * int * int * Geotessera.bbox) Lwt.t
3232+(** Async: opens the store and fetches embeddings for a bbox.
3333+ [progress] receives status messages during fetching.
3434+ Defaults to MegaZarr store; pass a legacy [base_url] for
3535+ backward compat. *)
3636+3737+val fetch_region_sync : ?base_url:string -> ?year:int ->
3838+ Geotessera.bbox -> Linalg.mat * int * int * Geotessera.bbox
3939+(** Synchronous convenience for imperative notebook cells. *)
4040+4141+(** {1 FRP bridge} *)
4242+4343+module Frp_async = Frp_async