diff --git a/.github/workflows/test-publish.yml b/.github/workflows/test-publish.yml
index e33c449..8c686f3 100644
--- a/.github/workflows/test-publish.yml
+++ b/.github/workflows/test-publish.yml
@@ -21,7 +21,7 @@ jobs:
uses: mymindstorm/setup-emsdk@v14
with:
# Make sure to set a version number!
- version: 3.1.68
+ version: 4.0.23
# This is the name of the cache folder.
# The cache folder will be placed in the build directory,
# so make sure it doesn't conflict with anything!
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 57f493e..1410794 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -6,13 +6,13 @@ project(H5WASM
LANGUAGES CXX C
)
-set (BASE_URL "https://github.com/usnistgov/libhdf5-wasm/releases/download/v0.5.0_3.1.68" CACHE STRING "")
+set (BASE_URL "https://github.com/usnistgov/libhdf5-wasm/releases/download/v0.6.0_4.0.23" CACHE STRING "")
# set (BASE_URL "$ENV{HOME}/dev/libhdf5-wasm" CACHE STRING "")
FetchContent_Declare(
libhdf5-wasm
- URL ${BASE_URL}/HDF5-1.14.6-Emscripten.tar.gz
- URL_HASH SHA256=90c26256f3d1fd0b421b7e0defebe1894f179b2a50252aeb059aaa1fa6bdb7e9
+ URL ${BASE_URL}/HDF5-2.0.0-Emscripten.tar.gz
+ URL_HASH SHA256=d4dc6719ef164679728d9a50d6a4d97c826f563e22a40c4dec63e485b29781be
)
if (NOT libhdf5-wasm_POPULATED)
FetchContent_MakeAvailable(libhdf5-wasm)
@@ -22,7 +22,7 @@ set(HDF5_DIR ${libhdf5-wasm_SOURCE_DIR}/cmake)
find_package(HDF5 REQUIRED CONFIG)
add_executable(hdf5_util src/hdf5_util.cc)
-target_link_libraries(hdf5_util hdf5_hl-static)
+target_link_libraries(hdf5_util hdf5::hdf5_hl-static)
set (EXPORTED_FUNCTIONS)
list (APPEND EXPORTED_FUNCTIONS
@@ -36,6 +36,7 @@ list (APPEND EXPORTED_FUNCTIONS
pthread_create pthread_mutex_lock pthread_mutex_unlock pthread_atfork pthread_once
pthread_cond_init pthread_barrier_init pthread_attr_init pthread_attr_setdetachstate
H5Pget_chunk H5Tget_class H5Sget_simple_extent_dims
+ H5_libinit_g H5_libterm_g
H5Pget_filter_by_id2 H5Pmodify_filter H5Pexist
H5Tget_size H5Tget_native_type H5Tget_order
H5Epush2 siprintf getTempRet0 __wasm_setjmp
@@ -67,7 +68,7 @@ set_target_properties(hdf5_util PROPERTIES
-s SINGLE_FILE \
-s EXPORT_ES6=1 \
-s FORCE_FILESYSTEM=1 \
- -s EXPORTED_RUNTIME_METHODS=\"['ccall', 'cwrap', 'FS', 'AsciiToString', 'UTF8ToString']\" \
+ -s EXPORTED_RUNTIME_METHODS=\"['ccall', 'cwrap', 'FS', 'AsciiToString', 'UTF8ToString', 'HEAPU8']\" \
-s EXPORTED_FUNCTIONS=\"${EXPORTED_FUNCTIONS_STRING}\""
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/dist/esm
RUNTIME_OUTPUT_NAME hdf5_util
@@ -75,7 +76,7 @@ set_target_properties(hdf5_util PROPERTIES
)
add_executable(hdf5_util_node src/hdf5_util.cc)
-target_link_libraries(hdf5_util_node hdf5_hl-static)
+target_link_libraries(hdf5_util_node hdf5::hdf5_hl-static)
set_target_properties(hdf5_util_node PROPERTIES
LINK_FLAGS "-O2 --bind \
-s MAIN_MODULE=2 \
@@ -92,7 +93,7 @@ set_target_properties(hdf5_util_node PROPERTIES
-s SINGLE_FILE \
-s EXPORT_ES6=1 \
-s ASSERTIONS=1 \
- -s EXPORTED_RUNTIME_METHODS=\"['ccall', 'cwrap', 'FS', 'AsciiToString', 'UTF8ToString']\" \
+ -s EXPORTED_RUNTIME_METHODS=\"['ccall', 'cwrap', 'FS', 'AsciiToString', 'UTF8ToString', 'HEAPU8']\" \
-s EXPORTED_FUNCTIONS=\"${EXPORTED_FUNCTIONS_STRING}\""
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/dist/node
RUNTIME_OUTPUT_NAME hdf5_util
diff --git a/README.md b/README.md
index 1efcff4..362470d 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@ a zero-dependency WebAssembly-powered library for [reading](#reading) and [writi
(built on the [HDF5 C API](http://portal.hdfgroup.org/pages/viewpage.action?pageId=50073943))
The built binaries (esm and node) will be attached to the [latest release](https://github.com/usnistgov/h5wasm/releases/latest/) as h5wasm-{version}.tgz
-
+
The wasm-compiled libraries `libhdf5.a`, `libhdf5_cpp.a` ... and the related `include/` folder are retrieved from [libhdf5-wasm](https://github.com/usnistgov/libhdf5-wasm) during the build.
Instead of importing a namespace "*", it is now possible to import the important h5wasm components in an object, from the default export:
@@ -14,6 +14,8 @@ export const h5wasm = {
File,
Group,
Dataset,
+ Datatype,
+ DatasetRegion,
ready,
ACCESS_MODES
}
@@ -21,9 +23,29 @@ export const h5wasm = {
The Emscripten filesystem is important for operations, and it can be accessed after the WASM is loaded as below.
+## Contents
+
+- [QuickStart](#quickstart)
+ - [Browser (no-build)](#browser-no-build)
+ - [Worker usage](#worker-usage)
+ - [Browser target (build system)](#browser-target-build-system)
+ - [Node.js](#nodejs)
+- [User Guide](#user-guide)
+ - [Opening a file](#opening-a-file)
+ - [Reading](#reading)
+ - [Writing](#writing)
+ - [Editing](#editing)
+ - [SWMR (single writer multiple readers)](#swmr-single-writer-multiple-readers)
+ - [Links](#links)
+ - [Library version bounds (libver)](#library-version-bounds-libver)
+- [Web Helpers](#web-helpers)
+- [Persistent file store (web)](#persistent-file-store-web)
+
+# QuickStart
+
## Browser (no-build)
```js
-import h5wasm from "https://cdn.jsdelivr.net/npm/h5wasm@0latest/dist/esm/hdf5_hl.js";
+import h5wasm from "https://cdn.jsdelivr.net/npm/h5wasm@latest/dist/esm/hdf5_hl.js";
// the WASM loads asychronously, and you can get the module like this:
const Module = await h5wasm.ready;
@@ -31,7 +53,7 @@ const Module = await h5wasm.ready;
// then you can get the FileSystem object from the Module:
const { FS } = Module;
-// Or, you can directly get the FS if you don't care about the rest
+// Or, you can directly get the FS if you don't care about the rest
// of the module:
// const { FS } = await h5wasm.ready;
@@ -45,13 +67,13 @@ let f = new h5wasm.File("sans59510.nxs.ngv", "r");
// File {path: "/", file_id: 72057594037927936n, filename: "data.h5", mode: "r"}
```
-### Worker usage
+## Worker usage
Since ESM is not supported in all web worker contexts (e.g. Firefox), an additional ```./dist/iife/h5wasm.js``` is provided in the package for `h5wasm>=0.4.8`; it can be loaded in a worker and used as in the example below (which uses the WORKERFS file system for random access on local files):
```js
// worker.js
onmessage = async function(e) {
const { FS } = await h5wasm.ready;
-
+
// send in a file opened from an
const f_in = e.data[0];
@@ -85,7 +107,7 @@ The host filesystem is made available through Emscripten "NODERAWFS=1".
Enabling BigInt support may be required for nodejs < 16
```bash
npm i h5wasm
-node --experimental-wasm-bigint
+node
```
@@ -100,12 +122,37 @@ File {
file_id: 72057594037927936n,
filename: '/home/brian/Downloads/sans59510.nxs.ngv',
mode: 'r'
-}
+}
*/
```
-## Usage
+# User Guide
_(all examples are written in ESM - for Typescript some type casting is probably required, as `get` returns either Group or Dataset)_
+
+## Opening a file
+
+```js
+new h5wasm.File(filename, mode?, options?)
+```
+
+| Argument | Type | Default | Description |
+|---|---|---|---|
+| `filename` | `string` | — | Path to the HDF5 file |
+| `mode` | `string` | `"r"` | Access mode (see table below) |
+| `options.track_order` | `boolean` | `false` | Preserve insertion order of groups and attributes |
+| `options.libver` | `string \| [string, string]` | — | Library version bound(s) for new objects (see [libver](#library-version-bounds-libver)) |
+
+Available modes:
+
+| Mode | Description |
+|---|---|
+| `"r"` | Read-only |
+| `"a"` | Read/write (file must exist) |
+| `"w"` | Create / truncate |
+| `"x"` | Create, fail if exists |
+| `"Sw"` | SWMR write |
+| `"Sr"` | SWMR read |
+
### Reading
```js
let f = new h5wasm.File("sans59510.nxs.ngv", "r");
@@ -121,7 +168,7 @@ let data = f.get("entry/instrument/detector_MR/data")
// Dataset {path: "/entry/instrument/detector_MR/data", file_id: 72057594037927936n}
data.metadata
-/*
+/*
{
"signed": true,
"vlen": false,
@@ -187,19 +234,6 @@ data.to_array()
*/
```
-### SWMR Read
-(single writer multiple readers)
-```js
-const swmr_file = new h5wasm.File("swmr.h5", "Sr");
-let dset = swmr_file.get("data");
-dset.shape;
-// 12
-// ...later
-dset.refresh();
-dset.shape;
-// 16
-```
-
### Writing
```js
let new_file = new h5wasm.File("myfile.h5", "w");
@@ -223,7 +257,7 @@ new_file.get("entry/data").value
//Float32Array(4) [3.0999999046325684, 4.099999904632568, 0, -1]
// create a dataset with shape=[2,2]
-// The dataset stored in the HDF5 file with the correct shape,
+// The dataset stored in the HDF5 file with the correct shape,
// but no attempt is made to make a 2x2 array out of it in javascript
new_file.get("entry").create_dataset({name: "square_data", data: [3.1, 4.1, 0.0, -1.0], shape: [2,2], dtype: ' max_memory) {
@@ -897,18 +934,61 @@ export class Group extends HasAttrs {
}
}
+export interface FileOptions {
+ track_order?: boolean;
+ libver?: LIBVER_BOUNDS;
+}
+
export class File extends Group {
filename: string;
mode: ACCESS_MODESTRING;
- constructor(filename: string, mode: ACCESS_MODESTRING = "r", track_order: boolean = false) {
+
+ constructor(filename: string, mode?: ACCESS_MODESTRING, options?: FileOptions);
+ /** @deprecated Use the `FileOptions` object overload instead: `new File(filename, mode, { track_order })` */
+ constructor(filename: string, mode?: ACCESS_MODESTRING, track_order?: boolean);
+ constructor(
+ filename: string,
+ mode: ACCESS_MODESTRING = "r",
+ optionsOrTrackOrder: FileOptions | boolean = false
+ ) {
+ let track_order: boolean;
+ let libver: LIBVER_BOUNDS | undefined;
+ if (optionsOrTrackOrder !== null && typeof optionsOrTrackOrder === "object") {
+ track_order = optionsOrTrackOrder.track_order ?? false;
+ libver = optionsOrTrackOrder.libver;
+ } else {
+ track_order = (optionsOrTrackOrder as boolean) ?? false;
+ }
const access_mode = ACCESS_MODES[mode];
const h5_mode = Module[access_mode];
- const file_id = Module.open(filename, h5_mode, track_order);
+
+ // Parse libver into numeric bounds
+ let libver_low = -1;
+ let libver_high = -1;
+
+ if (libver) {
+ if (Array.isArray(libver)) {
+ // Tuple: [low, high]
+ libver_low = parseLibverString(libver[0]);
+ libver_high = parseLibverString(libver[1]);
+ } else {
+ // Single string: both bounds set to same value
+ const ver = parseLibverString(libver);
+ libver_low = ver;
+ libver_high = ver;
+ }
+ }
+ const file_id = Module.open(filename, h5_mode, track_order, libver_low, libver_high);
super(file_id, "/");
this.filename = filename;
this.mode = mode;
}
+ get libver(): [LIBVER_BOUND, LIBVER_BOUND] {
+ const bounds = Module.get_libver_bounds(this.file_id);
+ return [convertToLibverString(bounds.low), convertToLibverString(bounds.high)];
+ }
+
flush() {
Module.flush(this.file_id);
}
diff --git a/src/hdf5_util.cc b/src/hdf5_util.cc
index 32f877e..6ec1d4f 100644
--- a/src/hdf5_util.cc
+++ b/src/hdf5_util.cc
@@ -28,11 +28,23 @@ EM_JS(void, throw_error, (const char *string_error), {
// // pass
// }
-int64_t open(const std::string& filename_string, unsigned int h5_mode = H5F_ACC_RDONLY, bool track_order = false)
+int64_t open(const std::string& filename_string, unsigned int h5_mode = H5F_ACC_RDONLY, bool track_order = false, int libver_low = -1, int libver_high = -1)
{
const char *filename = filename_string.c_str();
hid_t file_id;
hid_t fcpl_id = H5Pcreate(H5P_FILE_CREATE);
+ hid_t fapl_id = H5Pcreate(H5P_FILE_ACCESS);
+
+ // SWMR_WRITE requires RDWR, SWMR_READ works with RDONLY (which is 0)
+ if (h5_mode & H5F_ACC_SWMR_WRITE)
+ {
+ h5_mode |= H5F_ACC_RDWR;
+ }
+
+ if (libver_low >= 0 && libver_high >= 0)
+ {
+ H5Pset_libver_bounds(fapl_id, (H5F_libver_t)libver_low, (H5F_libver_t)libver_high);
+ }
if (track_order)
{
@@ -42,14 +54,15 @@ int64_t open(const std::string& filename_string, unsigned int h5_mode = H5F_ACC_
if (h5_mode == H5F_ACC_TRUNC || h5_mode == H5F_ACC_EXCL)
{
- file_id = H5Fcreate(filename, h5_mode, fcpl_id, H5P_DEFAULT);
+ file_id = H5Fcreate(filename, h5_mode, fcpl_id, fapl_id);
}
else
{
// then it is an existing file...
- file_id = H5Fopen(filename, h5_mode, H5P_DEFAULT);
+ file_id = H5Fopen(filename, h5_mode, fapl_id);
}
herr_t status = H5Pclose(fcpl_id);
+ status = H5Pclose(fapl_id);
return (int64_t)file_id;
}
@@ -59,6 +72,29 @@ int close_file(hid_t file_id)
return (int)status;
}
+val get_libver_bounds(hid_t file_id)
+{
+ hid_t fapl_id = H5Fget_access_plist(file_id);
+ if (fapl_id < 0) {
+ throw_error("Failed to get file access property list");
+ return val::null();
+ }
+
+ H5F_libver_t low, high;
+ herr_t status = H5Pget_libver_bounds(fapl_id, &low, &high);
+ H5Pclose(fapl_id);
+
+ if (status < 0) {
+ throw_error("Failed to get libver bounds");
+ return val::null();
+ }
+
+ val result = val::object();
+ result.set("low", (int)low);
+ result.set("high", (int)high);
+ return result;
+}
+
herr_t link_name_callback(hid_t loc_id, const char *name, const H5L_info_t *linfo, void *opdata)
{
std::vector *namelist = reinterpret_cast *>(opdata);
@@ -648,7 +684,7 @@ int read_write_dataset_data(hid_t loc_id, const std::string& dataset_name_string
else {
status = H5Dread(ds_id, memtype, memspace, dspace, H5P_DEFAULT, rwdata);
}
-
+
H5Dclose(ds_id);
H5Sclose(dspace);
H5Sclose(memspace);
@@ -949,7 +985,7 @@ int create_dataset(hid_t loc_id, std::string dset_name_string, uint64_t wdata_ui
int create_vlen_str_dataset(hid_t loc_id, std::string dset_name_string, val data, val dims_in, val maxdims_in, val chunks_in, int dtype, int dsize, bool is_signed, bool is_vlstr, bool track_order=false) {
uint64_t wdata_uint64; // ptr as uint64_t (webassembly will be 64-bit someday)
-
+
std::vector data_string_vec = vecFromJSArray(data);
std::vector data_char_vec;
data_char_vec.reserve(data_string_vec.size());
@@ -1246,8 +1282,8 @@ val get_region_metadata(hid_t loc_id, const val ref_data_in)
shape = val::array(); // elements of type hsize_t
for (int d = 0; d < rank; d++)
{
- hsize_t blocksize = (block.at(d) == NULL) ? 1 : block.at(d);
- shape.set(d, (double)(count.at(d) * blocksize));
+ hsize_t blocksize = (block.at(d) == NULL) ? 1 : block.at(d);
+ shape.set(d, (double)(count.at(d) * blocksize));
}
}
metadata.set("shape", shape);
@@ -1290,7 +1326,7 @@ int get_region_data(hid_t loc_id, val ref_data_in, uint64_t rdata_uint64)
htri_t success = H5Sget_regular_hyperslab(dspace, nullptr, nullptr, count.data(), block.data());
for (int d = 0; d < rank; d++)
{
- int blocksize = (block.at(d) == NULL) ? 1 : block.at(d);
+ int blocksize = (block.at(d) == NULL) ? 1 : block.at(d);
shape_out.at(d) = (count.at(d) * blocksize);
}
memspace = H5Screate_simple(shape_out.size(), &shape_out[0], nullptr);
@@ -1343,6 +1379,7 @@ EMSCRIPTEN_BINDINGS(hdf5)
{
function("open", &open);
function("close_file", &close_file);
+ function("get_libver_bounds", &get_libver_bounds);
function("get_keys", &get_keys_vector);
function("get_names", &get_child_names);
function("get_types", &get_child_types);
@@ -1424,9 +1461,17 @@ EMSCRIPTEN_BINDINGS(hdf5)
constant("H5F_ACC_EXCL", H5F_ACC_EXCL);
constant("H5F_ACC_CREAT", H5F_ACC_CREAT);
constant("H5F_ACC_SWMR_WRITE", H5F_ACC_SWMR_WRITE);
-
constant("H5F_ACC_SWMR_READ", H5F_ACC_SWMR_READ);
+ // Library version bounds
+ constant("H5F_LIBVER_EARLIEST", (int)H5F_LIBVER_EARLIEST);
+ constant("H5F_LIBVER_V18", (int)H5F_LIBVER_V18);
+ constant("H5F_LIBVER_V110", (int)H5F_LIBVER_V110);
+ constant("H5F_LIBVER_V112", (int)H5F_LIBVER_V112);
+ constant("H5F_LIBVER_V114", (int)H5F_LIBVER_V114);
+ constant("H5F_LIBVER_V200", (int)H5F_LIBVER_V200);
+ constant("H5F_LIBVER_LATEST", (int)H5F_LIBVER_LATEST);
+
constant("H5G_GROUP", (int)H5G_GROUP); // 0 Object is a group.
constant("H5G_DATASET", (int)H5G_DATASET); // 1 Object is a dataset.
constant("H5G_TYPE", (int)H5G_TYPE); // 2 Object is a named datatype.
diff --git a/src/hdf5_util_helpers.d.ts b/src/hdf5_util_helpers.d.ts
index 5322238..3777a2e 100644
--- a/src/hdf5_util_helpers.d.ts
+++ b/src/hdf5_util_helpers.d.ts
@@ -59,8 +59,9 @@ export interface VirtualSource {
}
export interface H5Module extends EmscriptenModule {
- open(filename: string, mode?: number, track_order?: boolean): bigint;
+ open(filename: string, mode?: number, track_order?: boolean, libver_low?: number, libver_high?: number): bigint;
close_file(file_id: bigint): number;
+ get_libver_bounds(file_id: bigint): {low: number, high: number};
create_dataset(file_id: bigint, arg1: string, arg2: bigint, shape: bigint[], maxshape: (bigint | null)[], chunks: bigint[] | null, type: number, size: number, signed: boolean, vlen: boolean, compression_id: number, compression_opts: number[], track_order?: boolean): number;
create_soft_link(file_id: bigint, link_target: string, link_name: string): number;
create_hard_link(file_id: bigint, link_target: string, link_name: string): number;
@@ -84,6 +85,13 @@ export interface H5Module extends EmscriptenModule {
H5F_ACC_CREAT: 16;
H5F_ACC_SWMR_WRITE: 32;
H5F_ACC_SWMR_READ: 64;
+ H5F_LIBVER_EARLIEST: number;
+ H5F_LIBVER_V18: number;
+ H5F_LIBVER_V110: number;
+ H5F_LIBVER_V112: number;
+ H5F_LIBVER_V114: number;
+ H5F_LIBVER_V200: number;
+ H5F_LIBVER_LATEST: number;
H5Z_FILTER_NONE: 0;
H5Z_FILTER_DEFLATE: 1;
H5Z_FILTER_SHUFFLE: 2;
@@ -139,7 +147,7 @@ export interface H5Module extends EmscriptenModule {
}
export declare type Filter = {
- id: number;
+ id: number;
name: string;
cd_values: number[];
}
diff --git a/test/libver_test.mjs b/test/libver_test.mjs
new file mode 100644
index 0000000..9f0c42a
--- /dev/null
+++ b/test/libver_test.mjs
@@ -0,0 +1,303 @@
+#!/usr/bin/env node
+
+import { strict as assert } from 'assert';
+import { existsSync, mkdirSync } from 'fs';
+import { join } from 'path';
+import { h5wasm, convertToLibverString } from "h5wasm/node";
+
+async function test_libver_v110() {
+ await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "libver_v110.h5");
+ const DATA = new Float32Array([1.0, 2.0, 3.0]);
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // Create file with libver="v110"
+ const f = new h5wasm.File(FILEPATH, "w", { libver: "v110" });
+ f.create_dataset({name: "data", data: DATA});
+ f.flush();
+ f.close();
+
+ // Verify we can read it back
+ const f_read = new h5wasm.File(FILEPATH, "r");
+ const dset = f_read.get("data");
+ assert.deepEqual([...dset.value], [...DATA]);
+ f_read.close();
+}
+
+async function test_libver_latest() {
+ await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "libver_latest.h5");
+ const DATA = new Float32Array([1.0, 2.0, 3.0]);
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // Create file with libver="latest"
+ const f = new h5wasm.File(FILEPATH, "w", { libver: "latest" });
+ f.create_dataset({name: "data", data: DATA});
+ f.flush();
+ f.close();
+
+ // Verify we can read it back
+ const f_read = new h5wasm.File(FILEPATH, "r");
+ const dset = f_read.get("data");
+ assert.deepEqual([...dset.value], [...DATA]);
+ f_read.close();
+}
+
+async function test_libver_v108() {
+ await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "libver_v18.h5");
+ const DATA = new Float32Array([1.0, 2.0, 3.0]);
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // Create file with libver="v108"
+ const f = new h5wasm.File(FILEPATH, "w", { libver: "v108" });
+ f.create_dataset({name: "data", data: DATA});
+ f.flush();
+ f.close();
+
+ // Verify we can read it back
+ const f_read = new h5wasm.File(FILEPATH, "r");
+ const dset = f_read.get("data");
+ assert.deepEqual([...dset.value], [...DATA]);
+ f_read.close();
+}
+
+async function test_libver_asymmetric() {
+ await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "libver_asymmetric.h5");
+ const DATA = new Float32Array([1.0, 2.0, 3.0]);
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // Create file with asymmetric libver bounds
+ const f = new h5wasm.File(FILEPATH, "w", { libver: ["v110", "latest"] });
+ f.create_dataset({name: "data", data: DATA});
+ f.flush();
+ f.close();
+
+ // Verify we can read it back
+ const f_read = new h5wasm.File(FILEPATH, "r");
+ const dset = f_read.get("data");
+ assert.deepEqual([...dset.value], [...DATA]);
+ f_read.close();
+}
+
+async function test_libver_swmr() {
+ await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "libver_swmr.h5");
+ const DATA = new Float32Array([1.0, 2.0, 3.0]);
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // Create file with superblock v3 (required for SWMR)
+ const f = new h5wasm.File(FILEPATH, "w", { libver: "v110" });
+ // Create an extensible chunked dataset (required for SWMR)
+ f.create_dataset({
+ name: "swmr_data",
+ data: DATA,
+ maxshape: [null],
+ chunks: [10]
+ });
+ f.flush();
+ f.close();
+
+ // Verify we can open in SWMR write mode
+ const f_swmr = new h5wasm.File(FILEPATH, "Sw");
+ const dset = f_swmr.get("swmr_data");
+ assert.deepEqual([...dset.value], [...DATA]);
+ f_swmr.close();
+}
+
+async function test_libver_with_track_order() {
+ await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "libver_track_order.h5");
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // Create file with track_order and explicit libver
+ const f = new h5wasm.File(FILEPATH, "w", { track_order: true, libver: "latest" });
+
+ // Create attributes in reverse alphabetical order
+ f.create_attribute("c", "first");
+ f.create_attribute("b", "second");
+ f.create_attribute("a", "third");
+
+ f.flush();
+ f.close();
+
+ // Verify order is preserved
+ const f_read = new h5wasm.File(FILEPATH, "r");
+ assert.deepEqual(Object.keys(f_read.attrs), ["c", "b", "a"]);
+ f_read.close();
+}
+
+async function test_libver_auto_with_track_order() {
+ await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "libver_auto.h5");
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // Create file with track_order but no explicit libver (should auto-set v18)
+ const f = new h5wasm.File(FILEPATH, "w", { track_order: true });
+
+ // Create attributes in reverse alphabetical order
+ f.create_attribute("c", "first");
+ f.create_attribute("b", "second");
+ f.create_attribute("a", "third");
+
+ f.flush();
+ f.close();
+
+ // Verify order is preserved (auto-set libver should work)
+ const f_read = new h5wasm.File(FILEPATH, "r");
+ assert.deepEqual(Object.keys(f_read.attrs), ["c", "b", "a"]);
+ f_read.close();
+}
+
+async function test_libver_case_insensitive() {
+ await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "libver_case.h5");
+ const DATA = new Float32Array([1.0, 2.0, 3.0]);
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // Test case-insensitive libver strings (uppercase/mixed case)
+ const f = new h5wasm.File(FILEPATH, "w", { libver: "LATEST" });
+ f.create_dataset({name: "data", data: DATA});
+ f.flush();
+ f.close();
+
+ const f_read = new h5wasm.File(FILEPATH, "r");
+ const dset = f_read.get("data");
+ assert.deepEqual([...dset.value], [...DATA]);
+ f_read.close();
+}
+
+async function test_libver_constants() {
+ const Module = await h5wasm.ready;
+
+ // Verify H5F_LIBVER constants are exported
+ assert.ok(typeof Module.H5F_LIBVER_EARLIEST === 'number');
+ assert.ok(typeof Module.H5F_LIBVER_V18 === 'number');
+ assert.ok(typeof Module.H5F_LIBVER_V110 === 'number');
+ assert.ok(typeof Module.H5F_LIBVER_V112 === 'number');
+ assert.ok(typeof Module.H5F_LIBVER_V114 === 'number');
+ assert.ok(typeof Module.H5F_LIBVER_V200 === 'number');
+ assert.ok(typeof Module.H5F_LIBVER_LATEST === 'number');
+}
+
+async function test_libver_getter() {
+ const Module = await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "libver_getter.h5");
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // look up latest as real version, e.g. "v200"
+ const latest = convertToLibverString(Module.H5F_LIBVER_LATEST);
+
+ // Test single libver value
+ const f1 = new h5wasm.File(FILEPATH, "w", { libver: "v108" });
+ assert.deepEqual(f1.libver, ["v108", "v108"]);
+ f1.close();
+
+ // Test asymmetric libver bounds
+ const f2 = new h5wasm.File(FILEPATH, "w", { libver: ["v110", "latest"] });
+ assert.deepEqual(f2.libver, ["v110", latest]);
+ // On close, because we didn't use any features that require v110,
+ // it falls back to "v108" for the low bound
+ f2.close();
+
+ // Test reading libver from existing file..
+ const f3 = new h5wasm.File(FILEPATH, "r");
+ assert.deepEqual(f3.libver, ["v108", latest]);
+ f3.close();
+
+ // Open a file in SWMR read/write mode:
+ const f4 = new h5wasm.File(FILEPATH, "Sw");
+ // If no libver is specified, hdf5 library will set lower bound
+ // to "v110" for SWMR compatibility
+ assert.deepEqual(f4.libver, ["v110", latest]);
+ f4.close();
+
+ // Open a file with track_order enabled:
+ const f5 = new h5wasm.File(FILEPATH, "w", { track_order: true });
+ // If no libver is specified, and track_order is used,
+ // a minimum version "v108" is set by hdf5 library
+ assert.deepEqual(f5.libver, ["v108", latest]);
+ f5.close();
+}
+
+export const tests = [
+ {
+ description: "Create file with libver='v110'",
+ test: test_libver_v110
+ },
+ {
+ description: "Create file with libver='latest'",
+ test: test_libver_latest
+ },
+ {
+ description: "Create file with libver='v108'",
+ test: test_libver_v108
+ },
+ {
+ description: "Create file with asymmetric libver bounds",
+ test: test_libver_asymmetric
+ },
+ {
+ description: "Create SWMR-compatible file and open in SWMR mode",
+ test: test_libver_swmr
+ },
+ {
+ description: "Create file with track_order and explicit libver",
+ test: test_libver_with_track_order
+ },
+ {
+ description: "Create file with track_order, auto-set libver",
+ test: test_libver_auto_with_track_order
+ },
+ {
+ description: "Test case-insensitive libver parsing",
+ test: test_libver_case_insensitive
+ },
+ {
+ description: "Verify H5F_LIBVER constants are exported",
+ test: test_libver_constants
+ },
+ {
+ description: "Test libver property getter",
+ test: test_libver_getter
+ }
+];
+
+export default tests;
diff --git a/test/swmr_test.mjs b/test/swmr_test.mjs
new file mode 100644
index 0000000..82166e9
--- /dev/null
+++ b/test/swmr_test.mjs
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+
+import { strict as assert } from 'assert';
+import { existsSync, mkdirSync } from 'fs';
+import { join } from 'path';
+import h5wasm from "h5wasm/node";
+
+async function test_swmr_write_read() {
+ await h5wasm.ready;
+ const PATH = join(".", "test", "tmp");
+ const FILEPATH = join(PATH, "swmr_test.h5");
+ const INITIAL_DATA = new Float32Array([1.0, 2.0, 3.0]);
+ const APPEND_DATA = new Float32Array([4.0, 5.0, 6.0]);
+
+ if (!(existsSync(PATH))) {
+ mkdirSync(PATH);
+ }
+
+ // Create file with SWMR-compatible format (v110 minimum)
+ const f_create = new h5wasm.File(FILEPATH, "w", { libver: "v110" });
+
+ // Create an extensible chunked dataset (required for SWMR)
+ f_create.create_dataset({
+ name: "data",
+ data: INITIAL_DATA,
+ maxshape: [null],
+ chunks: [10]
+ });
+ f_create.flush();
+ f_create.close();
+
+ // Open for SWMR write
+ const f_write = new h5wasm.File(FILEPATH, "Sw");
+
+ // Open for SWMR read
+ const f_read = new h5wasm.File(FILEPATH, "Sr");
+
+ // Verify initial data in both handles
+ const dset_write = f_write.get("data");
+ const dset_read = f_read.get("data");
+
+ assert.deepEqual([...dset_write.value], [...INITIAL_DATA]);
+ assert.deepEqual([...dset_read.value], [...INITIAL_DATA]);
+
+ // Append data using write handle
+ const new_size = INITIAL_DATA.length + APPEND_DATA.length;
+ dset_write.resize([new_size]);
+ dset_write.write_slice([[INITIAL_DATA.length, new_size]], APPEND_DATA);
+ f_write.flush();
+
+ // Before refresh, read handle should still see old data
+ assert.equal(dset_read.shape[0], INITIAL_DATA.length);
+
+ // Refresh the dataset in read handle
+ dset_read.refresh();
+
+ // After refresh, should see appended data
+ assert.equal(dset_read.shape[0], new_size);
+ const all_data = dset_read.value;
+ const expected = new Float32Array([...INITIAL_DATA, ...APPEND_DATA]);
+ assert.deepEqual([...all_data], [...expected]);
+
+ // Clean up
+ f_write.close();
+ f_read.close();
+}
+
+export const tests = [
+ {
+ description: "SWMR: Write and read with refresh",
+ test: test_swmr_write_read
+ }
+];
+
+export default tests;
diff --git a/test/test.mjs b/test/test.mjs
index 4dcf9d0..52da982 100644
--- a/test/test.mjs
+++ b/test/test.mjs
@@ -20,6 +20,8 @@ import test_throwing_error_handler from './test_throwing_error_handler.mjs';
import test_empty from './empty_dataset_and_attrs.mjs';
import vlen_test from './vlen_test.mjs';
import track_order from './track_order.mjs';
+import libver_test from './libver_test.mjs';
+import swmr_test from './swmr_test.mjs';
let tests = [];
const add_tests = (tests_in) => { /*global*/ tests = tests.concat(tests_in)}
@@ -43,6 +45,8 @@ add_tests(test_throwing_error_handler);
add_tests(test_empty);
add_tests(vlen_test);
add_tests(track_order);
+add_tests(libver_test);
+add_tests(swmr_test);
let passed = true;
async function run_test(test) {