@oceanum/datamesh 0.1.1 → 0.4.2

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.
@@ -4,37 +4,59 @@ import duration from "dayjs/plugin/duration";
4
4
 
5
5
  import { DataVariable } from "./datamodel";
6
6
 
7
- /**
8
- * Represents a data variable.
9
- */
10
- enum Coordinate {
11
- "Station" = "s", // locations assumed stationary, datasource multigeometry coordinate indexed by station coordinate
12
- "Ensemble" = "e",
13
- "Raster band" = "b",
14
- "Category" = "c",
15
- "Quantile" = "q",
16
- "Season" = "n",
17
- "Month" = "m",
18
- "Time" = "t",
19
- "Vertical coordinate" = "z",
20
- "Horizontal northerly" = "y",
21
- "Horizontal easterly" = "x",
22
- "Geometry" = "g", // Abstract coordinate - a 2 or 3D geometry that defines a feature location
23
- "Frequency" = "f", // spectra
24
- "Direction" = "d", // spectra or stats
25
- "Coordinate_i" = "i",
26
- "Coordinate_j" = "j",
27
- "Coordinate_k" = "k",
28
- }
29
-
30
- type Coordinates = {
7
+ export type Coordinate =
8
+ | "s" // locations assumed stationary, datasource multigeometry coordinate indexed by station coordinate
9
+ | "e" // Ensemble
10
+ | "b" // Raster band
11
+ | "c" // Category
12
+ | "q" // Quantile
13
+ | "n" // Season
14
+ | "m" // Month
15
+ | "t" // Time
16
+ | "z" // Vertical coordinate
17
+ | "y" // Horizontal northerly
18
+ | "x" // Horizontal easterly
19
+ | "g" // Abstract coordinate - a 2 or 3D geometry that defines a feature location
20
+ | "f" // Frequency - spectra
21
+ | "d" // Direction - spectra or stats
22
+ | "i" // Coordinate_i
23
+ | "j" // Coordinate_j
24
+ | "k"; // Coordinate_k
25
+
26
+ export type Coordkeys = {
31
27
  [key in Coordinate]?: string;
32
28
  };
33
29
 
34
30
  /**
35
- * Represents the schema of a data source.
31
+ * Represents the internal schema of a data source.
36
32
  */
37
33
  export type Schema = {
34
+ /**
35
+ * Attributes of the schema.
36
+ */
37
+ attributes?: Record<string, string | number>;
38
+
39
+ /**
40
+ * Dimensions of the schema.
41
+ */
42
+ dimensions: Record<string, number>;
43
+
44
+ /**
45
+ * Coordinate map of the schema.
46
+ */
47
+ coordkeys?: Coordkeys;
48
+
49
+ /**
50
+ * Data variables of the schema.
51
+ */
52
+ variables: Record<string, DataVariable>;
53
+ };
54
+
55
+ /**
56
+ * Datamesh schema
57
+ */
58
+
59
+ export type DatameshSchema = {
38
60
  /**
39
61
  * Attributes of the schema.
40
62
  */
@@ -46,14 +68,14 @@ export type Schema = {
46
68
  dims: Record<string, number>;
47
69
 
48
70
  /**
49
- * Coordinates of the schema.
71
+ * Coordinate map of the schema.
50
72
  */
51
- coords?: Record<string, DataVariable>;
73
+ coords?: Record<string, DatameshSchema>;
52
74
 
53
75
  /**
54
76
  * Data variables of the schema.
55
77
  */
56
- data_vars: Record<string, DataVariable>;
78
+ data_vars?: Record<string, DatameshSchema>;
57
79
  };
58
80
 
59
81
  /**
@@ -78,7 +100,7 @@ export type Datasource = {
78
100
  /**
79
101
  * Parameters associated with the data source.
80
102
  */
81
- parameters?: Record<string, unknown>;
103
+ parameters?: Record<string, string | number>;
82
104
 
83
105
  /**
84
106
  * Geometric representation of the data source.
@@ -118,12 +140,12 @@ export type Datasource = {
118
140
  /**
119
141
  * Schema information for the data source.
120
142
  */
121
- schema: Schema;
143
+ schema: DatameshSchema;
122
144
 
123
145
  /**
124
- * Coordinate mappings for the data source.
146
+ * Coordinate map for the data source.
125
147
  */
126
- coordinates: Coordinates;
148
+ coordinates: Coordkeys;
127
149
 
128
150
  /**
129
151
  * Additional details about the data source.
@@ -0,0 +1,21 @@
1
+ /** @ignore */
2
+ export function measureTime(
3
+ target: any,
4
+ propertyKey: string,
5
+ descriptor: PropertyDescriptor
6
+ ) {
7
+ const originalMethod = descriptor.value;
8
+
9
+ descriptor.value = async function (...args: any[]) {
10
+ const start = Date.now();
11
+ const result = await originalMethod.apply(this, args);
12
+ const end = Date.now();
13
+ const executionTime = end - start;
14
+
15
+ console.debug(`${propertyKey} took ${executionTime}ms`);
16
+
17
+ return result;
18
+ };
19
+
20
+ return descriptor;
21
+ }
package/src/lib/query.ts CHANGED
@@ -5,73 +5,47 @@ import duration from "dayjs/plugin/duration";
5
5
  dayjs.extend(duration);
6
6
 
7
7
  /**
8
- * GeoFilterType enum representing types of geofilters.
8
+ * GeoFilterType type representing types of geofilters.
9
9
  */
10
- enum GeoFilterType {
11
- Feature = "feature",
12
- Bbox = "bbox",
13
- }
10
+ export type GeoFilterType = "feature" | "bbox";
14
11
 
15
12
  /**
16
- * GeoFilterInterp enum representing interpolation methods for geofilters.
13
+ * GeoFilterInterp type representing interpolation methods for geofilters.
17
14
  */
18
- enum GeoFilterInterp {
19
- Nearest = "nearest",
20
- Linear = "linear",
21
- }
15
+ export type GeoFilterInterp = "nearest" | "linear";
22
16
 
23
17
  /**
24
- * LevelFilterInterp enum representing interpolation methods for level filters.
18
+ * LevelFilterInterp type representing interpolation methods for level filters.
25
19
  */
26
- enum LevelFilterInterp {
27
- Nearest = "nearest",
28
- Linear = "linear",
29
- }
20
+ export type LevelFilterInterp = "nearest" | "linear";
30
21
 
31
22
  /**
32
- * TimeFilterType enum representing types of time filters.
23
+ * TimeFilterType type representing types of time filters.
33
24
  */
34
- enum TimeFilterType {
35
- Range = "range",
36
- Series = "series",
37
- Trajectory = "trajectory",
38
- }
25
+ export type TimeFilterType = "range" | "series" | "trajectory";
39
26
 
40
27
  /**
41
- * LevelFilterType enum representing types of level filters.
28
+ * LevelFilterType type representing types of level filters.
42
29
  */
43
- enum LevelFilterType {
44
- Range = "range",
45
- Series = "series",
46
- }
30
+ export type LevelFilterType = "range" | "series";
47
31
 
48
32
  /**
49
- * ResampleType enum representing types of resampling.
33
+ * ResampleType type representing types of resampling.
50
34
  */
51
- enum ResampleType {
52
- Mean = "mean",
53
- Nearest = "nearest",
54
- Slinear = "linear",
55
- }
35
+ export type ResampleType = "mean" | "nearest" | "linear";
56
36
 
57
37
  /**
58
- * AggregateOps enum representing aggregation operations.
38
+ * AggregateOps type representing aggregation operations.
59
39
  */
60
- enum AggregateOps {
61
- Mean = "mean",
62
- Min = "min",
63
- Max = "max",
64
- Std = "std",
65
- Sum = "sum",
66
- }
40
+ export type AggregateOps = "mean" | "min" | "max" | "std" | "sum";
67
41
 
68
42
  /**
69
- * Container enum representing data container types.
43
+ * Container type representing data container types.
70
44
  */
71
- enum Container {
72
- GeoDataFrame = "geodataframe",
73
- DataFrame = "dataframe",
74
- Dataset = "dataset",
45
+ export type Container = "geodataframe" | "dataframe" | "dataset";
46
+
47
+ export interface GeoFilterFeature extends Omit<Feature, "properties"> {
48
+ properties?: Record<string, unknown> | undefined;
75
49
  }
76
50
 
77
51
  /**
@@ -79,7 +53,7 @@ enum Container {
79
53
  */
80
54
  export type GeoFilter = {
81
55
  type: GeoFilterType;
82
- geom: Array<number[]> | Feature;
56
+ geom: Array<number[]> | GeoFilterFeature;
83
57
  interp?: GeoFilterInterp;
84
58
  resolution?: number;
85
59
  alltouched?: boolean;
@@ -88,7 +62,7 @@ export type GeoFilter = {
88
62
  /**
89
63
  * LevelFilter type representing a vertical subset or interpolation.
90
64
  */
91
- type LevelFilter = {
65
+ export type LevelFilter = {
92
66
  type: LevelFilterType;
93
67
  levels: Array<number | null>;
94
68
  interp?: LevelFilterInterp;
@@ -126,7 +100,7 @@ const timeFilterValidate = (timefilter: TimeFilter): TimeFilter => {
126
100
  const times = timefilter.times.map((t) => stringifyTime(t));
127
101
 
128
102
  return {
129
- type: timefilter.type || TimeFilterType.Range,
103
+ type: timefilter.type || "range",
130
104
  times,
131
105
  resolution: timefilter.resolution,
132
106
  resample: timefilter.resample,
@@ -136,7 +110,7 @@ const timeFilterValidate = (timefilter: TimeFilter): TimeFilter => {
136
110
  /**
137
111
  * Aggregate type representing aggregation operations.
138
112
  */
139
- type Aggregate = {
113
+ export type Aggregate = {
140
114
  operations: AggregateOps[];
141
115
  spatial?: boolean;
142
116
  temporal?: boolean;
@@ -145,7 +119,7 @@ type Aggregate = {
145
119
  /**
146
120
  * CoordSelector type representing coordinate selection.
147
121
  */
148
- type CoordSelector = {
122
+ export type CoordSelector = {
149
123
  coord: string;
150
124
  values: Array<string | number>;
151
125
  };
@@ -155,7 +129,7 @@ type CoordSelector = {
155
129
  */
156
130
  export interface IQuery {
157
131
  datasource: string;
158
- parameters?: Record<string, number | string | number[] | string[]>;
132
+ parameters?: Record<string, number | string>;
159
133
  description?: string;
160
134
  variables?: string[];
161
135
  timefilter?: TimeFilter;
@@ -171,6 +145,7 @@ export interface IQuery {
171
145
  /**
172
146
  * Stage interface representing the result of staging a query.
173
147
  */
148
+ /** @ignore */
174
149
  export type Stage = {
175
150
  query: Query;
176
151
  qhash: string;
@@ -178,6 +153,7 @@ export type Stage = {
178
153
  size: number;
179
154
  dlen: number;
180
155
  coordmap: Record<string, string>;
156
+ coordkeys: Record<string, string>;
181
157
  container: Container;
182
158
  sig: string;
183
159
  };
@@ -187,7 +163,7 @@ export type Stage = {
187
163
  */
188
164
  export class Query implements IQuery {
189
165
  datasource: string;
190
- parameters?: Record<string, number | string | number[] | string[]>;
166
+ parameters?: Record<string, number | string>;
191
167
  description?: string;
192
168
  variables?: string[];
193
169
  timefilter?: TimeFilter;
@@ -0,0 +1,171 @@
1
+ import { measureTime } from "./observe";
2
+
3
+ /**
4
+ * Session class for datamesh connections.
5
+ *
6
+ * Sessions are used to manage authentication and resource allocation
7
+ * for datamesh operations.
8
+ */
9
+ export class Session {
10
+ id!: string;
11
+ user!: string;
12
+ creationTime!: Date;
13
+ endTime!: Date;
14
+ write!: boolean;
15
+ verified: boolean = false;
16
+ private _connection!: any;
17
+
18
+ /**
19
+ * Acquire a session from the connection.
20
+ *
21
+ * @param connection - Connection object to acquire session from.
22
+ * @param options - Session options.
23
+ * @param options.duration - The desired length of time for the session in hours. Defaults to 1 hour.
24
+ * @returns A new session instance.
25
+ * @throws {Error} - If the session cannot be acquired.
26
+ */
27
+ @measureTime
28
+ static async acquire(
29
+ connection: any,
30
+ options: { duration?: number } = {}
31
+ ): Promise<Session> {
32
+ // Check if the connection supports sessions (v1 API)
33
+ if (!connection._isV1) {
34
+ const session = new Session();
35
+ session.id = "dummy_session";
36
+ session.user = "dummy_user";
37
+ session.creationTime = new Date();
38
+ session.endTime = new Date(
39
+ Date.now() + (options.duration || 1) * 60 * 60 * 1000
40
+ );
41
+ session.write = false;
42
+ session.verified = false;
43
+ session._connection = connection;
44
+
45
+ // Register cleanup function for when the process exits
46
+ if (typeof process !== "undefined" && process.on) {
47
+ process.on("beforeExit", () => {
48
+ session.close();
49
+ });
50
+ }
51
+
52
+ return session;
53
+ }
54
+
55
+ try {
56
+ const headers = { ...connection._authHeaders };
57
+ headers["Cache-Control"] = "no-store";
58
+ const params = { duration: options.duration || 1 };
59
+ const response = await fetch(
60
+ `${connection._gateway}/session/?${params}`,
61
+ { headers }
62
+ );
63
+
64
+ if (response.status !== 200) {
65
+ throw new Error(`Failed to create session: ${await response.text()}`);
66
+ }
67
+
68
+ const data = await response.json();
69
+ const session = new Session();
70
+ session.id = data.id;
71
+ session.user = data.user;
72
+ session.creationTime = new Date(data.creation_time);
73
+ session.endTime = new Date(data.end_time);
74
+ session.write = data.write;
75
+ session.verified = data.verified || false;
76
+ session._connection = connection;
77
+
78
+ // Register cleanup function for when the process exits
79
+ if (typeof process !== "undefined" && process.on) {
80
+ process.on("beforeExit", () => {
81
+ session.close();
82
+ });
83
+ }
84
+
85
+ return session;
86
+ } catch (error) {
87
+ throw new Error(`Error when acquiring datamesh session: ${error}`);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Get the session header for requests.
93
+ *
94
+ * @returns The session header object.
95
+ */
96
+ get header(): Record<string, string> {
97
+ return { "X-DATAMESH-SESSIONID": this.id };
98
+ }
99
+
100
+ /**
101
+ * Add session header to an existing headers object.
102
+ *
103
+ * @param headers - The headers object to add the session header to.
104
+ * @returns The updated headers object.
105
+ */
106
+ addHeader(headers: Record<string, string>): Record<string, string> {
107
+ return { ...headers, ...this.header };
108
+ }
109
+
110
+ /**
111
+ * Close the session.
112
+ *
113
+ * @param finaliseWrite - Whether to finalise any write operations. Defaults to false.
114
+ * @throws {Error} - If the session cannot be closed and finaliseWrite is true.
115
+ */
116
+ async close(finaliseWrite: boolean = false): Promise<void> {
117
+ // Back-compatibility with beta version (ignoring)
118
+ if (!this._connection._isV1) {
119
+ return;
120
+ }
121
+
122
+ try {
123
+ // Remove the beforeExit handler if possible
124
+ if (typeof process !== "undefined" && process.off) {
125
+ process.off("beforeExit", this.close);
126
+ }
127
+
128
+ const response = await fetch(
129
+ `${this._connection._gateway}/session/${this.id}`,
130
+ {
131
+ method: "DELETE",
132
+ headers: this.header,
133
+ body: JSON.stringify({ finalise_write: finaliseWrite }),
134
+ }
135
+ );
136
+
137
+ if (response.status !== 204) {
138
+ if (finaliseWrite) {
139
+ throw new Error(`Failed to finalise write: ${await response.text()}`);
140
+ }
141
+ console.warn(`Failed to close session: ${await response.text()}`);
142
+ }
143
+ } catch (error) {
144
+ if (finaliseWrite) {
145
+ throw new Error(`Error when closing datamesh session: ${error}`);
146
+ }
147
+ console.warn(`Error when closing datamesh session: ${error}`);
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Enter a session context.
153
+ *
154
+ * @returns The session instance.
155
+ */
156
+ async enter(): Promise<Session> {
157
+ return this;
158
+ }
159
+
160
+ /**
161
+ * Exit a session context.
162
+ *
163
+ * @param error - Any error that occurred during the session.
164
+ * @returns A promise that resolves when the session is closed.
165
+ */
166
+ async exit(error?: any): Promise<void> {
167
+ // When using context manager, close the session
168
+ // and finalise the write if no exception was raised
169
+ await this.close(error === undefined);
170
+ }
171
+ }
@@ -0,0 +1,3 @@
1
+ This is an optional service worker to act as a caching proxy with the datamesh service.
2
+
3
+ !! Work in progress - not available yet !!!
package/src/lib/zarr.ts CHANGED
@@ -12,36 +12,73 @@ function delay(t: number): Promise<void> {
12
12
  return new Promise((resolve) => setTimeout(resolve, t));
13
13
  }
14
14
 
15
+ interface CachedHTTPStoreOptions {
16
+ parameters?: Record<string, string | number>;
17
+ chunks?: string;
18
+ downsample?: Record<string, number>;
19
+ nocache?: boolean;
20
+ timeout?: number;
21
+ }
22
+
15
23
  export class CachedHTTPStore implements AsyncReadable {
16
24
  cache: UseStore | undefined;
17
25
  url: string;
26
+ params: Record<string, string>;
18
27
  cache_prefix: string;
19
28
  fetchOptions: RequestInit;
29
+ timeout: number;
30
+ _pending: Record<string, boolean> = {};
31
+
20
32
  constructor(
21
33
  root: string,
22
34
  authHeaders: Record<string, string>,
23
- parameters?: Record<string, string | number>,
24
- chunks?: string,
25
- downsample?: Record<string, number>,
26
- nocache?: boolean
35
+ options: CachedHTTPStoreOptions = {}
27
36
  ) {
37
+ // Create a copy of the auth headers to avoid modifying the original
38
+ // Important: We need to preserve all auth headers, including session headers
28
39
  const headers = { ...authHeaders };
29
- if (parameters) headers["x-parameters"] = JSON.stringify(parameters);
30
- if (chunks) headers["x-chunks"] = chunks;
31
- if (downsample) headers["x-downsample"] = JSON.stringify(downsample);
40
+
41
+ // Add parameters, chunks, and downsample as headers if provided
42
+ if (options.parameters)
43
+ headers["x-parameters"] = JSON.stringify(options.parameters);
44
+ if (options.chunks) headers["x-chunks"] = options.chunks;
45
+ if (options.downsample)
46
+ headers["x-downsample"] = JSON.stringify(options.downsample);
32
47
  headers["x-filtered"] = "True";
48
+
49
+ this.params = {};
50
+ if (authHeaders["x-datamesh-auth"]) {
51
+ this.params["auth"] = authHeaders["x-datamesh-auth"];
52
+ }
53
+ if (authHeaders["x-datamesh-sig"]) {
54
+ this.params["sig"] = authHeaders["x-datamesh-sig"];
55
+ }
56
+
33
57
  this.fetchOptions = { headers };
58
+
34
59
  this.url = root;
35
- if (nocache) {
60
+ const datasource = root.split("/").pop();
61
+
62
+ // Determine if caching should be used
63
+ if (options.nocache || typeof window === "undefined") {
36
64
  this.cache = undefined;
37
65
  } else {
38
66
  this.cache = createStore("zarr", "cache");
39
67
  }
40
- this.cache_prefix = hash({ ...parameters, chunks, downsample });
68
+
69
+ // Create a cache prefix based on datasource and options
70
+ // Note: We don't include auth headers in the cache key to avoid leaking sensitive information
71
+ this.cache_prefix = hash({
72
+ datasource,
73
+ ...options.parameters,
74
+ chunks: options.chunks,
75
+ downsample: options.downsample,
76
+ });
77
+ this.timeout = options.timeout || 60000;
41
78
  }
42
79
 
43
80
  async get(
44
- item: string,
81
+ item: AbsolutePath,
45
82
  options?: RequestInit,
46
83
  retry = 0
47
84
  ): Promise<Uint8Array | undefined> {
@@ -49,36 +86,61 @@ export class CachedHTTPStore implements AsyncReadable {
49
86
  let data = null;
50
87
  if (this.cache) {
51
88
  data = await get_cache(key, this.cache);
52
- if (data === "pending") {
89
+ if (data) delete this._pending[key];
90
+ if (this._pending[key]) {
53
91
  await delay(200);
54
- if (retry > 5 * 60) {
92
+ //console.debug("Zarr pending:" + key);
93
+ if (retry > this.timeout) {
55
94
  await del_cache(key, this.cache);
56
- throw new Error("Zarr timeout");
95
+ delete this._pending[key];
96
+ console.error("Zarr timeout");
97
+ return undefined;
57
98
  } else {
58
- return await this.get(item, options, retry + 1);
99
+ return await this.get(item, options, retry + 200);
59
100
  }
60
101
  }
61
102
  }
62
- if (!data || !this.cache) {
63
- if (this.cache) set_cache(key, "pending", this.cache);
64
- //console.log(`${this.url}/${item}`);
65
- //console.log(this.fetchOptions.headers);
66
- const response = await fetch(`${this.url}${item}`, {
67
- ...this.fetchOptions,
68
- ...options,
69
- });
70
-
71
- if (response.status === 404) {
72
- // Item is not found
103
+ if (!data) {
104
+ this._pending[key] = true;
105
+ try {
106
+ // Ensure we're preserving the headers from fetchOptions when making the request
107
+ const requestOptions = {
108
+ ...this.fetchOptions,
109
+ ...options,
110
+ headers: {
111
+ ...(this.fetchOptions.headers || {}),
112
+ ...(options?.headers || {}),
113
+ },
114
+ signal: AbortSignal.timeout(this.timeout),
115
+ };
116
+ const query = new URLSearchParams(this.params).toString();
117
+ const response = await fetch(
118
+ `${this.url}${item}?${query}`,
119
+ requestOptions
120
+ );
121
+
122
+ if (response.status === 404) {
123
+ // Item is not found
124
+ if (this.cache) await del_cache(key, this.cache);
125
+ return undefined;
126
+ }
127
+ if (response.status >= 400) {
128
+ throw new Error(`HTTP error: ${response.status}`);
129
+ }
130
+ data = new Uint8Array(await response.arrayBuffer());
131
+ if (this.cache) await set_cache(key, data, this.cache);
132
+ } catch (e) {
133
+ console.debug("Zarr retry:" + key);
134
+ if (retry < this.timeout / 200) {
135
+ delete this._pending[key];
136
+ return await this.get(item, options, retry + 200);
137
+ }
73
138
  if (this.cache) await del_cache(key, this.cache);
139
+ console.error(e);
74
140
  return undefined;
75
- } else if (response.status !== 200) {
76
- // Item is found but there was an error
77
- if (this.cache) await del_cache(key, this.cache);
78
- throw new Error(String(response.status));
141
+ } finally {
142
+ delete this._pending[key];
79
143
  }
80
- data = new Uint8Array(await response.arrayBuffer());
81
- if (this.cache) await set_cache(key, data, this.cache);
82
144
  }
83
145
  return data;
84
146
  }