@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.
- package/README.md +8 -7
- package/package.json +10 -8
- package/src/index.js +20 -0
- package/src/index.ts +2 -0
- package/src/lib/connector.ts +217 -50
- package/src/lib/datamodel.ts +430 -116
- package/src/lib/datasource.ts +54 -32
- package/src/lib/observe.ts +21 -0
- package/src/lib/query.ts +28 -52
- package/src/lib/session.ts +171 -0
- package/src/lib/workers/README.md +3 -0
- package/src/lib/zarr.ts +93 -31
- package/src/test/dataframe.test.ts +108 -0
- package/src/test/dataset.test.ts +138 -18
- package/src/test/datasource.test.ts +1 -1
- package/src/test/fixtures.ts +177 -49
- package/src/test/query.test.ts +4 -4
- package/tsconfig.lib.json +2 -1
- package/tsconfig.vitest-temp.json +61 -0
- package/typedoc.json +5 -1
- package/vite.config.ts +11 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/connector.d.ts +0 -97
- package/dist/lib/connector.d.ts.map +0 -1
- package/dist/lib/datamodel.d.ts +0 -123
- package/dist/lib/datamodel.d.ts.map +0 -1
- package/dist/lib/datasource.d.ts +0 -118
- package/dist/lib/datasource.d.ts.map +0 -1
- package/dist/lib/query.d.ts +0 -161
- package/dist/lib/query.d.ts.map +0 -1
- package/dist/lib/zarr.d.ts +0 -20
- package/dist/lib/zarr.d.ts.map +0 -1
- package/dist/test/fixtures.d.ts +0 -8
- package/dist/test/fixtures.d.ts.map +0 -1
- package/dist/tsconfig.lib.tsbuildinfo +0 -1
- package/src/lib/workers/sw.js +0 -44
- package/vitest.config.ts +0 -10
- /package/{eslint.config.js → eslint.config.cjs} +0 -0
package/src/lib/datasource.ts
CHANGED
|
@@ -4,37 +4,59 @@ import duration from "dayjs/plugin/duration";
|
|
|
4
4
|
|
|
5
5
|
import { DataVariable } from "./datamodel";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
*
|
|
71
|
+
* Coordinate map of the schema.
|
|
50
72
|
*/
|
|
51
|
-
coords?: Record<string,
|
|
73
|
+
coords?: Record<string, DatameshSchema>;
|
|
52
74
|
|
|
53
75
|
/**
|
|
54
76
|
* Data variables of the schema.
|
|
55
77
|
*/
|
|
56
|
-
data_vars
|
|
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,
|
|
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:
|
|
143
|
+
schema: DatameshSchema;
|
|
122
144
|
|
|
123
145
|
/**
|
|
124
|
-
* Coordinate
|
|
146
|
+
* Coordinate map for the data source.
|
|
125
147
|
*/
|
|
126
|
-
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
|
|
8
|
+
* GeoFilterType type representing types of geofilters.
|
|
9
9
|
*/
|
|
10
|
-
|
|
11
|
-
Feature = "feature",
|
|
12
|
-
Bbox = "bbox",
|
|
13
|
-
}
|
|
10
|
+
export type GeoFilterType = "feature" | "bbox";
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
|
-
* GeoFilterInterp
|
|
13
|
+
* GeoFilterInterp type representing interpolation methods for geofilters.
|
|
17
14
|
*/
|
|
18
|
-
|
|
19
|
-
Nearest = "nearest",
|
|
20
|
-
Linear = "linear",
|
|
21
|
-
}
|
|
15
|
+
export type GeoFilterInterp = "nearest" | "linear";
|
|
22
16
|
|
|
23
17
|
/**
|
|
24
|
-
* LevelFilterInterp
|
|
18
|
+
* LevelFilterInterp type representing interpolation methods for level filters.
|
|
25
19
|
*/
|
|
26
|
-
|
|
27
|
-
Nearest = "nearest",
|
|
28
|
-
Linear = "linear",
|
|
29
|
-
}
|
|
20
|
+
export type LevelFilterInterp = "nearest" | "linear";
|
|
30
21
|
|
|
31
22
|
/**
|
|
32
|
-
* TimeFilterType
|
|
23
|
+
* TimeFilterType type representing types of time filters.
|
|
33
24
|
*/
|
|
34
|
-
|
|
35
|
-
Range = "range",
|
|
36
|
-
Series = "series",
|
|
37
|
-
Trajectory = "trajectory",
|
|
38
|
-
}
|
|
25
|
+
export type TimeFilterType = "range" | "series" | "trajectory";
|
|
39
26
|
|
|
40
27
|
/**
|
|
41
|
-
* LevelFilterType
|
|
28
|
+
* LevelFilterType type representing types of level filters.
|
|
42
29
|
*/
|
|
43
|
-
|
|
44
|
-
Range = "range",
|
|
45
|
-
Series = "series",
|
|
46
|
-
}
|
|
30
|
+
export type LevelFilterType = "range" | "series";
|
|
47
31
|
|
|
48
32
|
/**
|
|
49
|
-
* ResampleType
|
|
33
|
+
* ResampleType type representing types of resampling.
|
|
50
34
|
*/
|
|
51
|
-
|
|
52
|
-
Mean = "mean",
|
|
53
|
-
Nearest = "nearest",
|
|
54
|
-
Slinear = "linear",
|
|
55
|
-
}
|
|
35
|
+
export type ResampleType = "mean" | "nearest" | "linear";
|
|
56
36
|
|
|
57
37
|
/**
|
|
58
|
-
* AggregateOps
|
|
38
|
+
* AggregateOps type representing aggregation operations.
|
|
59
39
|
*/
|
|
60
|
-
|
|
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
|
|
43
|
+
* Container type representing data container types.
|
|
70
44
|
*/
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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[]> |
|
|
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 ||
|
|
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
|
|
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
|
|
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
|
+
}
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
89
|
+
if (data) delete this._pending[key];
|
|
90
|
+
if (this._pending[key]) {
|
|
53
91
|
await delay(200);
|
|
54
|
-
|
|
92
|
+
//console.debug("Zarr pending:" + key);
|
|
93
|
+
if (retry > this.timeout) {
|
|
55
94
|
await del_cache(key, this.cache);
|
|
56
|
-
|
|
95
|
+
delete this._pending[key];
|
|
96
|
+
console.error("Zarr timeout");
|
|
97
|
+
return undefined;
|
|
57
98
|
} else {
|
|
58
|
-
return await this.get(item, options, retry +
|
|
99
|
+
return await this.get(item, options, retry + 200);
|
|
59
100
|
}
|
|
60
101
|
}
|
|
61
102
|
}
|
|
62
|
-
if (!data
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
}
|
|
76
|
-
|
|
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
|
}
|