@aics/vole-core 3.12.4
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/LICENSE.txt +26 -0
- package/README.md +119 -0
- package/es/Atlas2DSlice.js +224 -0
- package/es/Channel.js +264 -0
- package/es/FileSaver.js +31 -0
- package/es/FusedChannelData.js +192 -0
- package/es/Histogram.js +250 -0
- package/es/ImageInfo.js +127 -0
- package/es/Light.js +74 -0
- package/es/Lut.js +500 -0
- package/es/MarchingCubes.js +507 -0
- package/es/MeshVolume.js +334 -0
- package/es/NaiveSurfaceNets.js +251 -0
- package/es/PathTracedVolume.js +482 -0
- package/es/RayMarchedAtlasVolume.js +250 -0
- package/es/RenderToBuffer.js +31 -0
- package/es/ThreeJsPanel.js +633 -0
- package/es/Timing.js +28 -0
- package/es/TrackballControls.js +538 -0
- package/es/View3d.js +848 -0
- package/es/Volume.js +352 -0
- package/es/VolumeCache.js +161 -0
- package/es/VolumeDims.js +16 -0
- package/es/VolumeDrawable.js +702 -0
- package/es/VolumeMaker.js +101 -0
- package/es/VolumeRenderImpl.js +1 -0
- package/es/VolumeRenderSettings.js +203 -0
- package/es/constants/basicShaders.js +29 -0
- package/es/constants/colors.js +59 -0
- package/es/constants/denoiseShader.js +43 -0
- package/es/constants/lights.js +42 -0
- package/es/constants/materials.js +85 -0
- package/es/constants/pathtraceOutputShader.js +13 -0
- package/es/constants/scaleBarSVG.js +21 -0
- package/es/constants/time.js +34 -0
- package/es/constants/volumePTshader.js +153 -0
- package/es/constants/volumeRayMarchShader.js +123 -0
- package/es/constants/volumeSliceShader.js +115 -0
- package/es/index.js +21 -0
- package/es/loaders/IVolumeLoader.js +131 -0
- package/es/loaders/JsonImageInfoLoader.js +255 -0
- package/es/loaders/OmeZarrLoader.js +495 -0
- package/es/loaders/OpenCellLoader.js +65 -0
- package/es/loaders/RawArrayLoader.js +89 -0
- package/es/loaders/TiffLoader.js +219 -0
- package/es/loaders/VolumeLoadError.js +44 -0
- package/es/loaders/VolumeLoaderUtils.js +221 -0
- package/es/loaders/index.js +40 -0
- package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +143 -0
- package/es/loaders/zarr_utils/WrappedStore.js +51 -0
- package/es/loaders/zarr_utils/types.js +24 -0
- package/es/loaders/zarr_utils/utils.js +225 -0
- package/es/loaders/zarr_utils/validation.js +49 -0
- package/es/test/ChunkPrefetchIterator.test.js +208 -0
- package/es/test/RequestQueue.test.js +442 -0
- package/es/test/SubscribableRequestQueue.test.js +244 -0
- package/es/test/VolumeCache.test.js +118 -0
- package/es/test/VolumeRenderSettings.test.js +71 -0
- package/es/test/lut.test.js +671 -0
- package/es/test/num_utils.test.js +140 -0
- package/es/test/volume.test.js +98 -0
- package/es/test/zarr_utils.test.js +358 -0
- package/es/types/Atlas2DSlice.d.ts +41 -0
- package/es/types/Channel.d.ts +44 -0
- package/es/types/FileSaver.d.ts +6 -0
- package/es/types/FusedChannelData.d.ts +26 -0
- package/es/types/Histogram.d.ts +57 -0
- package/es/types/ImageInfo.d.ts +87 -0
- package/es/types/Light.d.ts +27 -0
- package/es/types/Lut.d.ts +67 -0
- package/es/types/MarchingCubes.d.ts +53 -0
- package/es/types/MeshVolume.d.ts +40 -0
- package/es/types/NaiveSurfaceNets.d.ts +11 -0
- package/es/types/PathTracedVolume.d.ts +65 -0
- package/es/types/RayMarchedAtlasVolume.d.ts +41 -0
- package/es/types/RenderToBuffer.d.ts +17 -0
- package/es/types/ThreeJsPanel.d.ts +107 -0
- package/es/types/Timing.d.ts +11 -0
- package/es/types/TrackballControls.d.ts +51 -0
- package/es/types/View3d.d.ts +357 -0
- package/es/types/Volume.d.ts +152 -0
- package/es/types/VolumeCache.d.ts +43 -0
- package/es/types/VolumeDims.d.ts +28 -0
- package/es/types/VolumeDrawable.d.ts +108 -0
- package/es/types/VolumeMaker.d.ts +49 -0
- package/es/types/VolumeRenderImpl.d.ts +22 -0
- package/es/types/VolumeRenderSettings.d.ts +98 -0
- package/es/types/constants/basicShaders.d.ts +4 -0
- package/es/types/constants/colors.d.ts +2 -0
- package/es/types/constants/denoiseShader.d.ts +40 -0
- package/es/types/constants/lights.d.ts +38 -0
- package/es/types/constants/materials.d.ts +20 -0
- package/es/types/constants/pathtraceOutputShader.d.ts +11 -0
- package/es/types/constants/scaleBarSVG.d.ts +2 -0
- package/es/types/constants/time.d.ts +19 -0
- package/es/types/constants/volumePTshader.d.ts +137 -0
- package/es/types/constants/volumeRayMarchShader.d.ts +117 -0
- package/es/types/constants/volumeSliceShader.d.ts +109 -0
- package/es/types/glsl.d.js +0 -0
- package/es/types/index.d.ts +28 -0
- package/es/types/loaders/IVolumeLoader.d.ts +113 -0
- package/es/types/loaders/JsonImageInfoLoader.d.ts +80 -0
- package/es/types/loaders/OmeZarrLoader.d.ts +87 -0
- package/es/types/loaders/OpenCellLoader.d.ts +9 -0
- package/es/types/loaders/RawArrayLoader.d.ts +33 -0
- package/es/types/loaders/TiffLoader.d.ts +45 -0
- package/es/types/loaders/VolumeLoadError.d.ts +18 -0
- package/es/types/loaders/VolumeLoaderUtils.d.ts +38 -0
- package/es/types/loaders/index.d.ts +22 -0
- package/es/types/loaders/zarr_utils/ChunkPrefetchIterator.d.ts +22 -0
- package/es/types/loaders/zarr_utils/WrappedStore.d.ts +24 -0
- package/es/types/loaders/zarr_utils/types.d.ts +94 -0
- package/es/types/loaders/zarr_utils/utils.d.ts +23 -0
- package/es/types/loaders/zarr_utils/validation.d.ts +7 -0
- package/es/types/test/ChunkPrefetchIterator.test.d.ts +1 -0
- package/es/types/test/RequestQueue.test.d.ts +1 -0
- package/es/types/test/SubscribableRequestQueue.test.d.ts +1 -0
- package/es/types/test/VolumeCache.test.d.ts +1 -0
- package/es/types/test/VolumeRenderSettings.test.d.ts +1 -0
- package/es/types/test/lut.test.d.ts +1 -0
- package/es/types/test/num_utils.test.d.ts +1 -0
- package/es/types/test/volume.test.d.ts +1 -0
- package/es/types/test/zarr_utils.test.d.ts +1 -0
- package/es/types/types.d.ts +115 -0
- package/es/types/utils/RequestQueue.d.ts +112 -0
- package/es/types/utils/SubscribableRequestQueue.d.ts +52 -0
- package/es/types/utils/num_utils.d.ts +43 -0
- package/es/types/workers/VolumeLoaderContext.d.ts +106 -0
- package/es/types/workers/types.d.ts +101 -0
- package/es/types/workers/util.d.ts +3 -0
- package/es/types.js +75 -0
- package/es/typings.d.js +0 -0
- package/es/utils/RequestQueue.js +267 -0
- package/es/utils/SubscribableRequestQueue.js +187 -0
- package/es/utils/num_utils.js +231 -0
- package/es/workers/FetchTiffWorker.js +153 -0
- package/es/workers/VolumeLoadWorker.js +129 -0
- package/es/workers/VolumeLoaderContext.js +271 -0
- package/es/workers/types.js +41 -0
- package/es/workers/util.js +8 -0
- package/package.json +83 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import { Vector3 } from "three";
|
|
3
|
+
import RequestQueue from "../utils/RequestQueue";
|
|
4
|
+
import { LoadSpec, loadSpecToString } from "../loaders/IVolumeLoader";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns a promise that resolves once the timeout (give in ms) is completed.
|
|
8
|
+
*/
|
|
9
|
+
async function sleep(timeoutMs) {
|
|
10
|
+
return new Promise(resolve => setTimeout(resolve, timeoutMs));
|
|
11
|
+
}
|
|
12
|
+
describe("test RequestQueue", () => {
|
|
13
|
+
describe("adding requests", () => {
|
|
14
|
+
it("can issue one request", async () => {
|
|
15
|
+
const rq = new RequestQueue();
|
|
16
|
+
const loadSpec = new LoadSpec();
|
|
17
|
+
let actionIsRun = false;
|
|
18
|
+
rq.addRequest(loadSpecToString(loadSpec), async () => {
|
|
19
|
+
// do something
|
|
20
|
+
await sleep(10);
|
|
21
|
+
actionIsRun = true;
|
|
22
|
+
return null;
|
|
23
|
+
});
|
|
24
|
+
expect(rq.hasRequest(loadSpecToString(loadSpec))).to.be.true;
|
|
25
|
+
await sleep(15);
|
|
26
|
+
expect(actionIsRun).to.be.true;
|
|
27
|
+
expect(rq.hasRequest(loadSpecToString(loadSpec))).to.be.false;
|
|
28
|
+
});
|
|
29
|
+
it("only runs request once", async () => {
|
|
30
|
+
const rq = new RequestQueue();
|
|
31
|
+
const loadSpec = new LoadSpec();
|
|
32
|
+
let count = 0;
|
|
33
|
+
const promises = [];
|
|
34
|
+
const work = async () => {
|
|
35
|
+
await sleep(100);
|
|
36
|
+
count++;
|
|
37
|
+
};
|
|
38
|
+
promises.push(rq.addRequest(loadSpecToString(loadSpec), work));
|
|
39
|
+
promises.push(rq.addRequest(loadSpecToString(loadSpec), work));
|
|
40
|
+
await Promise.all(promises);
|
|
41
|
+
expect(count).to.equal(1);
|
|
42
|
+
});
|
|
43
|
+
it("can issue delayed requests", async () => {
|
|
44
|
+
const rq = new RequestQueue();
|
|
45
|
+
const startTime = Date.now();
|
|
46
|
+
const delayMs = 15;
|
|
47
|
+
const immediatePromise = rq.addRequest("a", () => Promise.resolve());
|
|
48
|
+
const delayedPromise = rq.addRequest("b", () => Promise.resolve(), false, delayMs);
|
|
49
|
+
const promises = [];
|
|
50
|
+
promises.push(immediatePromise.then(() => {
|
|
51
|
+
expect(Date.now() - startTime).to.be.lessThan(delayMs);
|
|
52
|
+
}));
|
|
53
|
+
promises.push(delayedPromise.then(() => {
|
|
54
|
+
// Need to add a 1 ms buffer due to variation in setTimeout
|
|
55
|
+
expect(Date.now() - startTime).to.be.greaterThanOrEqual(delayMs - 1);
|
|
56
|
+
}));
|
|
57
|
+
await Promise.all(promises);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Test that same promise is returned
|
|
61
|
+
|
|
62
|
+
it("ignores duplicate requests", async () => {
|
|
63
|
+
const rq = new RequestQueue();
|
|
64
|
+
const loadSpec1 = new LoadSpec();
|
|
65
|
+
const loadSpec2 = new LoadSpec();
|
|
66
|
+
let count = 0;
|
|
67
|
+
const promises = [];
|
|
68
|
+
const work = async () => {
|
|
69
|
+
await sleep(100);
|
|
70
|
+
count++;
|
|
71
|
+
};
|
|
72
|
+
const promise1 = rq.addRequest(loadSpecToString(loadSpec1), work);
|
|
73
|
+
const promise2 = rq.addRequest(loadSpecToString(loadSpec2), work);
|
|
74
|
+
// Check that the promises are the same instance
|
|
75
|
+
expect(promise1).to.deep.equal(promise2);
|
|
76
|
+
promises.push(promise1);
|
|
77
|
+
promises.push(promise2);
|
|
78
|
+
await Promise.all(promises);
|
|
79
|
+
expect(count).to.equal(1);
|
|
80
|
+
});
|
|
81
|
+
it("handles multiple concurrent requests", async () => {
|
|
82
|
+
const rq = new RequestQueue();
|
|
83
|
+
const array = [false, false, false, false, false];
|
|
84
|
+
const promises = [];
|
|
85
|
+
for (let i = 0; i < array.length; i++) {
|
|
86
|
+
const work = async () => {
|
|
87
|
+
await sleep(10);
|
|
88
|
+
array[i] = true;
|
|
89
|
+
};
|
|
90
|
+
promises.push(rq.addRequest(`${i}`, work));
|
|
91
|
+
}
|
|
92
|
+
await Promise.all(promises);
|
|
93
|
+
for (let i = 0; i < array.length; i++) {
|
|
94
|
+
expect(array[i]).to.be.true;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
it("completes all tasks sequentially when max requests is set", async () => {
|
|
98
|
+
const rq = new RequestQueue(1);
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
const iterations = 10;
|
|
101
|
+
const delayMs = 5;
|
|
102
|
+
const counter = [];
|
|
103
|
+
const promises = [];
|
|
104
|
+
for (let i = 0; i < iterations; i++) {
|
|
105
|
+
const work = async () => {
|
|
106
|
+
await sleep(delayMs);
|
|
107
|
+
counter.push(i);
|
|
108
|
+
if (i % 2 === 1) {
|
|
109
|
+
throw new Error(`${i}`);
|
|
110
|
+
}
|
|
111
|
+
return i;
|
|
112
|
+
};
|
|
113
|
+
promises.push(rq.addRequest(`${i}`, work));
|
|
114
|
+
}
|
|
115
|
+
const results = await Promise.allSettled(promises);
|
|
116
|
+
|
|
117
|
+
// Should run only one task at a time, so time should be
|
|
118
|
+
// at LEAST the timeout * number of tasks
|
|
119
|
+
const duration = Date.now() - startTime;
|
|
120
|
+
expect(duration).to.be.greaterThanOrEqual(delayMs * iterations);
|
|
121
|
+
|
|
122
|
+
// Tasks should execute in sequential order, all tasks should run.
|
|
123
|
+
for (let i = 0; i < iterations; i++) {
|
|
124
|
+
expect(counter[i]).to.equal(i);
|
|
125
|
+
const result = results[i];
|
|
126
|
+
if (i % 2 === 0) {
|
|
127
|
+
expect(result.status).to.equal("fulfilled");
|
|
128
|
+
if (result.status === "fulfilled") {
|
|
129
|
+
expect(result.value).to.equal(i);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
expect(result.status).to.equal("rejected");
|
|
133
|
+
if (result.status === "rejected") {
|
|
134
|
+
expect(result.reason).to.be.instanceOf(Error);
|
|
135
|
+
expect(result.reason.message).to.equal(`${i}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
it("handles failing request actions", async () => {
|
|
141
|
+
const maxActiveRequests = 10;
|
|
142
|
+
const rq = new RequestQueue(maxActiveRequests);
|
|
143
|
+
const iterations = maxActiveRequests * 10;
|
|
144
|
+
const promises = [];
|
|
145
|
+
for (let i = 0; i < iterations; i++) {
|
|
146
|
+
promises.push(rq.addRequest(`${i}`, async () => {
|
|
147
|
+
await sleep(5);
|
|
148
|
+
throw new Error("Test error (should be caught)");
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
await Promise.allSettled(promises).catch(_ => {
|
|
152
|
+
return;
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
it("handles sequential requests", async () => {
|
|
156
|
+
const rq = new RequestQueue();
|
|
157
|
+
let count = 0;
|
|
158
|
+
const work = async () => {
|
|
159
|
+
count++;
|
|
160
|
+
};
|
|
161
|
+
await rq.addRequest("a", work);
|
|
162
|
+
expect(count).to.equal(1);
|
|
163
|
+
await sleep(10);
|
|
164
|
+
await rq.addRequest("a", work);
|
|
165
|
+
expect(count).to.equal(2);
|
|
166
|
+
});
|
|
167
|
+
it("ignores different promise return types.", async () => {
|
|
168
|
+
const rq = new RequestQueue();
|
|
169
|
+
const promise1 = rq.addRequest("a", async () => {
|
|
170
|
+
await sleep(10);
|
|
171
|
+
return "5";
|
|
172
|
+
});
|
|
173
|
+
const promise2 = rq.addRequest("a", async () => {
|
|
174
|
+
await sleep(10);
|
|
175
|
+
return 5;
|
|
176
|
+
});
|
|
177
|
+
expect(promise1).to.equal(promise2);
|
|
178
|
+
promise1.then(value => {
|
|
179
|
+
expect(value).to.be.a("string").and.to.equal("5");
|
|
180
|
+
});
|
|
181
|
+
promise2.then(value => {
|
|
182
|
+
expect(value).to.be.a("string").and.to.equal("5");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
it("immediately queues delayed requests when re-requesting", async () => {
|
|
186
|
+
const rq = new RequestQueue();
|
|
187
|
+
let count = 0;
|
|
188
|
+
const work = async () => {
|
|
189
|
+
count++;
|
|
190
|
+
return;
|
|
191
|
+
};
|
|
192
|
+
const delayMs = 1000;
|
|
193
|
+
const requests = [{
|
|
194
|
+
key: "a",
|
|
195
|
+
requestAction: work
|
|
196
|
+
}, {
|
|
197
|
+
key: "b",
|
|
198
|
+
requestAction: work
|
|
199
|
+
}];
|
|
200
|
+
const start = Date.now();
|
|
201
|
+
const promises = rq.addRequests(requests, false, delayMs);
|
|
202
|
+
rq.addRequest("b", work); // requesting this again should remove the delay
|
|
203
|
+
await Promise.allSettled(promises);
|
|
204
|
+
expect(Date.now() - start).to.be.lessThan(delayMs);
|
|
205
|
+
expect(count).to.equal(2);
|
|
206
|
+
});
|
|
207
|
+
it("queues all regular requests before any low-priority requests", async () => {
|
|
208
|
+
let regular = 0;
|
|
209
|
+
let lowPriority = 0;
|
|
210
|
+
const regularWork = async () => {
|
|
211
|
+
await sleep(5);
|
|
212
|
+
regular++;
|
|
213
|
+
};
|
|
214
|
+
const lowPriorityWork = async () => {
|
|
215
|
+
await sleep(5);
|
|
216
|
+
lowPriority++;
|
|
217
|
+
};
|
|
218
|
+
const rq = new RequestQueue(1);
|
|
219
|
+
const prom1 = rq.addRequest("a", regularWork);
|
|
220
|
+
const prom2 = rq.addRequest("b", lowPriorityWork, true);
|
|
221
|
+
const prom3 = rq.addRequest("c", regularWork);
|
|
222
|
+
await prom1;
|
|
223
|
+
expect(regular).to.equal(1);
|
|
224
|
+
expect(lowPriority).to.equal(0);
|
|
225
|
+
await prom3;
|
|
226
|
+
expect(regular).to.equal(2);
|
|
227
|
+
expect(lowPriority).to.equal(0);
|
|
228
|
+
await prom2;
|
|
229
|
+
expect(lowPriority).to.equal(1);
|
|
230
|
+
});
|
|
231
|
+
it("maintains a separate and lower concurrent task limit for low-priority requests", async () => {
|
|
232
|
+
let count = 0;
|
|
233
|
+
const work = async () => {
|
|
234
|
+
await sleep(5);
|
|
235
|
+
count++;
|
|
236
|
+
};
|
|
237
|
+
const rq = new RequestQueue(2, 1);
|
|
238
|
+
const prom1 = rq.addRequest("a", work, true);
|
|
239
|
+
const prom2 = rq.addRequest("b", work, true);
|
|
240
|
+
expect(rq.requestRunning("a")).to.be.true;
|
|
241
|
+
expect(rq.requestRunning("b")).to.be.false;
|
|
242
|
+
rq.addRequest("c", work);
|
|
243
|
+
expect(rq.requestRunning("c")).to.be.true;
|
|
244
|
+
await prom1;
|
|
245
|
+
await prom2;
|
|
246
|
+
expect(count).to.equal(3);
|
|
247
|
+
});
|
|
248
|
+
it("can promote a low-priority task to high priority", async () => {
|
|
249
|
+
let count = 0;
|
|
250
|
+
const work = async () => {
|
|
251
|
+
await sleep(5);
|
|
252
|
+
count++;
|
|
253
|
+
};
|
|
254
|
+
const rq = new RequestQueue(2, 1);
|
|
255
|
+
const prom1 = rq.addRequest("a", work);
|
|
256
|
+
const prom2 = rq.addRequest("b", work, true);
|
|
257
|
+
expect(rq.requestRunning("a")).to.be.true;
|
|
258
|
+
expect(rq.requestRunning("b")).to.be.false;
|
|
259
|
+
rq.addRequest("b", work);
|
|
260
|
+
expect(rq.requestRunning("b")).to.be.true;
|
|
261
|
+
await prom1;
|
|
262
|
+
await prom2;
|
|
263
|
+
expect(count).to.equal(2);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
describe("cancelling requests", () => {
|
|
267
|
+
it("can cancel a request", async () => {
|
|
268
|
+
const rq = new RequestQueue();
|
|
269
|
+
let count = 0;
|
|
270
|
+
const work = async key => {
|
|
271
|
+
await sleep(10);
|
|
272
|
+
// Computation steps that should NOT be run when
|
|
273
|
+
// a request is cancelled must be nested in a check for
|
|
274
|
+
// validity of the current request
|
|
275
|
+
if (rq.hasRequest(key)) {
|
|
276
|
+
count++;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const promise = rq.addRequest("a", () => work(0));
|
|
280
|
+
rq.cancelRequest("a");
|
|
281
|
+
let didReject = false;
|
|
282
|
+
await promise.catch(_ => {
|
|
283
|
+
didReject = true;
|
|
284
|
+
});
|
|
285
|
+
await sleep(10);
|
|
286
|
+
expect(count).to.equal(0);
|
|
287
|
+
expect(didReject).to.be.true;
|
|
288
|
+
expect(rq.hasRequest("a")).to.be.false;
|
|
289
|
+
});
|
|
290
|
+
it("does not resolve cancelled requests", async () => {
|
|
291
|
+
const rq = new RequestQueue();
|
|
292
|
+
const work = async () => {
|
|
293
|
+
await sleep(10);
|
|
294
|
+
throw new Error("some error message");
|
|
295
|
+
};
|
|
296
|
+
const promise = rq.addRequest("a", work);
|
|
297
|
+
const cancelReason = "test cancel";
|
|
298
|
+
rq.cancelRequest("a", cancelReason);
|
|
299
|
+
await promise.catch(err => {
|
|
300
|
+
expect(err).to.equal(cancelReason);
|
|
301
|
+
});
|
|
302
|
+
await sleep(15);
|
|
303
|
+
// Check that no unexpected error is thrown and that the rejection
|
|
304
|
+
// reason does not change.
|
|
305
|
+
await promise.catch(err => {
|
|
306
|
+
expect(err).to.equal(cancelReason);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
it("can cancel delayed requests", async () => {
|
|
310
|
+
const rq = new RequestQueue(1);
|
|
311
|
+
let workCount = 0;
|
|
312
|
+
const delayMs = 10;
|
|
313
|
+
const work = async () => {
|
|
314
|
+
workCount++;
|
|
315
|
+
return;
|
|
316
|
+
};
|
|
317
|
+
const promise = rq.addRequest("a", work, false, delayMs);
|
|
318
|
+
rq.cancelRequest("a");
|
|
319
|
+
await Promise.allSettled([promise]);
|
|
320
|
+
await sleep(delayMs);
|
|
321
|
+
expect(workCount).to.equal(0);
|
|
322
|
+
});
|
|
323
|
+
it("is not blocked by cancelled requests", async () => {
|
|
324
|
+
const rq = new RequestQueue(1);
|
|
325
|
+
const work = async () => {
|
|
326
|
+
await sleep(5);
|
|
327
|
+
throw new Error("promise rejection");
|
|
328
|
+
};
|
|
329
|
+
const promises = [];
|
|
330
|
+
for (let i = 0; i < 10; i++) {
|
|
331
|
+
promises.push(rq.addRequest(`${i}`, work, false, 0));
|
|
332
|
+
rq.cancelRequest(`${i}`);
|
|
333
|
+
}
|
|
334
|
+
await Promise.allSettled(promises);
|
|
335
|
+
});
|
|
336
|
+
it("can cancel all requests", async () => {
|
|
337
|
+
const rq = new RequestQueue(1);
|
|
338
|
+
const iterations = 10;
|
|
339
|
+
let count = 0;
|
|
340
|
+
const rejectionReason = "test reject";
|
|
341
|
+
const promises = [];
|
|
342
|
+
for (let i = 0; i < iterations; i++) {
|
|
343
|
+
const work = async () => {
|
|
344
|
+
await sleep(10);
|
|
345
|
+
if (rq.hasRequest(`${i}`)) {
|
|
346
|
+
count++;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
promises.push(rq.addRequest(`${i}`, work));
|
|
350
|
+
}
|
|
351
|
+
rq.cancelAllRequests(rejectionReason);
|
|
352
|
+
// Use allSettled so it does not stop on a rejection
|
|
353
|
+
await Promise.allSettled(promises).then(results => results.forEach(result => {
|
|
354
|
+
expect(result.status).to.equal("rejected");
|
|
355
|
+
if (result.status === "rejected") {
|
|
356
|
+
expect(result.reason).to.equal(rejectionReason);
|
|
357
|
+
}
|
|
358
|
+
}));
|
|
359
|
+
await sleep(10);
|
|
360
|
+
expect(count).to.equal(0);
|
|
361
|
+
});
|
|
362
|
+
async function mockLoader(loadSpec, maxDelayMs = 10.0) {
|
|
363
|
+
const {
|
|
364
|
+
x,
|
|
365
|
+
y,
|
|
366
|
+
z
|
|
367
|
+
} = loadSpec.subregion.getSize(new Vector3());
|
|
368
|
+
const data = new Uint8Array(x * y * z);
|
|
369
|
+
const delayMs = Math.random() * maxDelayMs;
|
|
370
|
+
await sleep(delayMs);
|
|
371
|
+
return data;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Creates an array of requests where the keys are Loadspecs with the given dimensions and range, and the
|
|
376
|
+
* requestAction is a mock loader that creates typed arrays after a random duration.
|
|
377
|
+
*/
|
|
378
|
+
function getLoadSpecRequests(startingFrame, frames, xDim, yDim, action) {
|
|
379
|
+
const requests = [];
|
|
380
|
+
for (let i = startingFrame; i < startingFrame + frames; i++) {
|
|
381
|
+
const loadSpec = new LoadSpec();
|
|
382
|
+
loadSpec.subregion.min.set(0, 0, i);
|
|
383
|
+
loadSpec.subregion.max.set(xDim, yDim, i + 1);
|
|
384
|
+
requests.push({
|
|
385
|
+
key: loadSpecToString(loadSpec),
|
|
386
|
+
requestAction: () => {
|
|
387
|
+
return action(loadSpec);
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return requests;
|
|
392
|
+
}
|
|
393
|
+
it("can issue and cancel mock loadspec requests", async () => {
|
|
394
|
+
const rq = new RequestQueue(10);
|
|
395
|
+
const xDim = 400;
|
|
396
|
+
const yDim = 600;
|
|
397
|
+
const numFrames = 30;
|
|
398
|
+
const maxDelayMs = 10;
|
|
399
|
+
let workCount = 0;
|
|
400
|
+
const action = async loadSpec => {
|
|
401
|
+
// Check if the work we were going to do has been cancelled.
|
|
402
|
+
if (rq.hasRequest(loadSpecToString(loadSpec))) {
|
|
403
|
+
const ret = await mockLoader(loadSpec, maxDelayMs);
|
|
404
|
+
workCount++;
|
|
405
|
+
return ret;
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
};
|
|
409
|
+
let requests = getLoadSpecRequests(0, numFrames, xDim, yDim, action);
|
|
410
|
+
let promises = rq.addRequests(requests);
|
|
411
|
+
// Allow some but not all requests to complete
|
|
412
|
+
await sleep(maxDelayMs * 0.5);
|
|
413
|
+
rq.cancelAllRequests();
|
|
414
|
+
await sleep(maxDelayMs); // Wait for all async actions to finish
|
|
415
|
+
expect(workCount).to.be.greaterThan(0);
|
|
416
|
+
expect(workCount).to.be.lessThan(numFrames - 1);
|
|
417
|
+
|
|
418
|
+
// Reissue overlapping requests
|
|
419
|
+
requests = getLoadSpecRequests(60, numFrames, xDim, yDim, action);
|
|
420
|
+
promises = rq.addRequests(requests);
|
|
421
|
+
await Promise.all(promises);
|
|
422
|
+
|
|
423
|
+
// Verify promise return types and dimensions
|
|
424
|
+
let promiseCount = 0;
|
|
425
|
+
for (const promise of promises) {
|
|
426
|
+
await promise.then(value => {
|
|
427
|
+
promiseCount++;
|
|
428
|
+
expect(value).to.be.a("Uint8Array");
|
|
429
|
+
expect(value).to.be.instanceOf(Uint8Array);
|
|
430
|
+
if (value instanceof Uint8Array) {
|
|
431
|
+
expect(value.buffer.byteLength).to.equal(xDim * yDim);
|
|
432
|
+
} else {
|
|
433
|
+
throw new Error("Value is not a Uint8Array");
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
expect(promiseCount).to.equal(numFrames);
|
|
438
|
+
// Expect some of the work to be cancelled correctly.
|
|
439
|
+
expect(workCount).to.be.lessThan(2 * numFrames).and.greaterThanOrEqual(numFrames);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
});
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { expect } from "chai";
|
|
2
|
+
import SubscribableRequestQueue from "../utils/SubscribableRequestQueue";
|
|
3
|
+
const TIMEOUT = 10;
|
|
4
|
+
const LONG_TIMEOUT = 20;
|
|
5
|
+
|
|
6
|
+
/** Returns a promise that resolves after `timeoutMs` milliseconds with value `result`. */
|
|
7
|
+
function delay(timeoutMs, result) {
|
|
8
|
+
return new Promise(resolve => setTimeout(() => resolve(result), timeoutMs));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Returns a promise that rejects after `timeoutMs` milliseconds with reason `reason`. */
|
|
12
|
+
function delayReject(timeoutMs, reason) {
|
|
13
|
+
return new Promise((_, reject) => setTimeout(() => reject(reason), timeoutMs));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Determines whether a promise has rejected. */
|
|
17
|
+
// From https://gist.github.com/tyru/29360dfa475d2fefaf6c4655a93c2cb0
|
|
18
|
+
function isRejected(promise) {
|
|
19
|
+
return Promise.race([delay(0, false), promise.then(() => false, () => true)]);
|
|
20
|
+
}
|
|
21
|
+
describe("SubscribableRequestQueue", () => {
|
|
22
|
+
describe("addSubscriber", () => {
|
|
23
|
+
it("adds a subscriber", () => {
|
|
24
|
+
const queue = new SubscribableRequestQueue();
|
|
25
|
+
const id = queue.addSubscriber();
|
|
26
|
+
expect(queue.hasSubscriber(id)).to.be.true;
|
|
27
|
+
});
|
|
28
|
+
it("returns unique ids", () => {
|
|
29
|
+
const queue = new SubscribableRequestQueue();
|
|
30
|
+
const id1 = queue.addSubscriber();
|
|
31
|
+
const id2 = queue.addSubscriber();
|
|
32
|
+
expect(id1).to.not.equal(id2);
|
|
33
|
+
});
|
|
34
|
+
it("never reuses ids of removed subscribers", () => {
|
|
35
|
+
const queue = new SubscribableRequestQueue();
|
|
36
|
+
const id1 = queue.addSubscriber();
|
|
37
|
+
const id2 = queue.addSubscriber();
|
|
38
|
+
queue.removeSubscriber(id1);
|
|
39
|
+
const id3 = queue.addSubscriber();
|
|
40
|
+
expect(id1).to.not.equal(id2);
|
|
41
|
+
expect(id3).to.not.equal(id2);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("addRequestToQueue", () => {
|
|
45
|
+
it("queues a request", async () => {
|
|
46
|
+
const queue = new SubscribableRequestQueue();
|
|
47
|
+
const id = queue.addSubscriber();
|
|
48
|
+
const promise = queue.addRequest("test", id, () => delay(TIMEOUT, "foo"));
|
|
49
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
50
|
+
const result = await promise;
|
|
51
|
+
expect(result).to.equal("foo");
|
|
52
|
+
});
|
|
53
|
+
it("does not queue a request if the subscriber id is invalid", () => {
|
|
54
|
+
const queue = new SubscribableRequestQueue();
|
|
55
|
+
expect(() => queue.addRequest("test", -1, () => delay(TIMEOUT, "foo"))).to.throw();
|
|
56
|
+
expect(() => queue.addRequest("test", 0, () => delay(TIMEOUT, "foo"))).to.throw();
|
|
57
|
+
});
|
|
58
|
+
it("does not queue a request if the subscriber has been deleted", () => {
|
|
59
|
+
const queue = new SubscribableRequestQueue();
|
|
60
|
+
const id = queue.addSubscriber();
|
|
61
|
+
queue.removeSubscriber(id);
|
|
62
|
+
expect(() => queue.addRequest("test", id, () => delay(TIMEOUT, "foo"))).to.throw();
|
|
63
|
+
});
|
|
64
|
+
it("creates unique promises without duplicating work when two subscribers queue the same key", async () => {
|
|
65
|
+
const queue = new SubscribableRequestQueue();
|
|
66
|
+
const id1 = queue.addSubscriber();
|
|
67
|
+
const id2 = queue.addSubscriber();
|
|
68
|
+
const promise1 = queue.addRequest("test", id1, () => delay(TIMEOUT, "foo"));
|
|
69
|
+
const promise2 = queue.addRequest("test", id2, () => delay(TIMEOUT, "bar"));
|
|
70
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
71
|
+
expect(promise1).to.not.equal(promise2);
|
|
72
|
+
const [result1, result2] = await Promise.all([promise1, promise2]);
|
|
73
|
+
expect(result1).to.equal("foo");
|
|
74
|
+
expect(result2).to.equal("foo");
|
|
75
|
+
});
|
|
76
|
+
it("creates different requests when one subscriber queues two keys", async () => {
|
|
77
|
+
const queue = new SubscribableRequestQueue();
|
|
78
|
+
const id = queue.addSubscriber();
|
|
79
|
+
const promise1 = queue.addRequest("test1", id, () => delay(TIMEOUT, "foo"));
|
|
80
|
+
const promise2 = queue.addRequest("test2", id, () => delay(TIMEOUT, "bar"));
|
|
81
|
+
expect(queue.hasRequest("test1")).to.be.true;
|
|
82
|
+
expect(queue.hasRequest("test2")).to.be.true;
|
|
83
|
+
expect(promise1).to.not.equal(promise2);
|
|
84
|
+
const [result1, result2] = await Promise.all([promise1, promise2]);
|
|
85
|
+
expect(result1).to.equal("foo");
|
|
86
|
+
expect(result2).to.equal("bar");
|
|
87
|
+
});
|
|
88
|
+
it("allows a subscriber to queue the same key twice", async () => {
|
|
89
|
+
const queue = new SubscribableRequestQueue();
|
|
90
|
+
const id = queue.addSubscriber();
|
|
91
|
+
const promise1 = queue.addRequest("test", id, () => delay(TIMEOUT, "foo"));
|
|
92
|
+
const promise2 = queue.addRequest("test", id, () => delay(TIMEOUT, "bar"));
|
|
93
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
94
|
+
expect(promise1).to.not.equal(promise2);
|
|
95
|
+
const result2 = await promise2;
|
|
96
|
+
expect(result2).to.equal("foo");
|
|
97
|
+
const result1 = await promise1;
|
|
98
|
+
expect(result1).to.equal("foo");
|
|
99
|
+
});
|
|
100
|
+
it("allows multiple rejections when the same key is queued multiple times by the same subscriber", async () => {
|
|
101
|
+
const queue = new SubscribableRequestQueue();
|
|
102
|
+
const id = queue.addSubscriber();
|
|
103
|
+
const promise1 = queue.addRequest("test", id, () => delayReject(TIMEOUT, "foo"));
|
|
104
|
+
const promise2 = queue.addRequest("test", id, () => delayReject(TIMEOUT, "bar"));
|
|
105
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
106
|
+
expect(promise1).to.not.equal(promise2);
|
|
107
|
+
let promise1RejectReason = "",
|
|
108
|
+
promise2RejectReason = "";
|
|
109
|
+
promise1.catch(reason => promise1RejectReason = reason);
|
|
110
|
+
promise2.catch(reason => promise2RejectReason = reason);
|
|
111
|
+
await Promise.allSettled([promise1, promise2]);
|
|
112
|
+
expect(promise1RejectReason).to.equal("foo");
|
|
113
|
+
expect(promise2RejectReason).to.equal("foo");
|
|
114
|
+
});
|
|
115
|
+
it("passes request rejections to all subscribers", async () => {
|
|
116
|
+
const queue = new SubscribableRequestQueue();
|
|
117
|
+
const id1 = queue.addSubscriber();
|
|
118
|
+
const id2 = queue.addSubscriber();
|
|
119
|
+
const promise1 = queue.addRequest("test", id1, () => delayReject(TIMEOUT, "foo"));
|
|
120
|
+
const promise2 = queue.addRequest("test", id2, () => delay(TIMEOUT, "bar"));
|
|
121
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
122
|
+
let promise1RejectReason = "",
|
|
123
|
+
promise2RejectReason = "";
|
|
124
|
+
promise1.catch(reason => promise1RejectReason = reason);
|
|
125
|
+
promise2.catch(reason => promise2RejectReason = reason);
|
|
126
|
+
await Promise.allSettled([promise1, promise2]);
|
|
127
|
+
expect(promise1RejectReason).to.equal("foo");
|
|
128
|
+
expect(promise2RejectReason).to.equal("foo");
|
|
129
|
+
});
|
|
130
|
+
it("completes requests in order when max requests is set", async () => {
|
|
131
|
+
const queue = new SubscribableRequestQueue(1);
|
|
132
|
+
const id = queue.addSubscriber();
|
|
133
|
+
const promise1 = queue.addRequest("test1", id, () => delay(TIMEOUT, "foo"));
|
|
134
|
+
const promise2 = queue.addRequest("test2", id, () => delay(LONG_TIMEOUT, "bar"));
|
|
135
|
+
expect(queue.hasRequest("test1")).to.be.true;
|
|
136
|
+
expect(queue.hasRequest("test2")).to.be.true;
|
|
137
|
+
expect(queue.requestRunning("test1")).to.be.true;
|
|
138
|
+
expect(queue.requestRunning("test2")).to.be.false;
|
|
139
|
+
const result1 = await promise1;
|
|
140
|
+
expect(result1).to.equal("foo");
|
|
141
|
+
expect(queue.hasRequest("test1")).to.be.false;
|
|
142
|
+
expect(queue.hasRequest("test2")).to.be.true;
|
|
143
|
+
expect(queue.requestRunning("test2")).to.be.true;
|
|
144
|
+
const result2 = await promise2;
|
|
145
|
+
expect(result2).to.equal("bar");
|
|
146
|
+
expect(queue.hasRequest("test2")).to.be.false;
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe("cancelRequest", () => {
|
|
150
|
+
it("cancels a request subscription", async () => {
|
|
151
|
+
const queue = new SubscribableRequestQueue();
|
|
152
|
+
const id = queue.addSubscriber();
|
|
153
|
+
const promise = queue.addRequest("test", id, () => delay(TIMEOUT, "foo"));
|
|
154
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
155
|
+
expect(queue.isSubscribed(id, "test")).to.be.true;
|
|
156
|
+
const cancelResult = queue.cancelRequest("test", id);
|
|
157
|
+
expect(cancelResult).to.be.true;
|
|
158
|
+
expect(queue.isSubscribed(id, "test")).to.be.false;
|
|
159
|
+
expect(await isRejected(promise)).to.be.true;
|
|
160
|
+
expect(queue.cancelRequest("test", id)).to.be.false;
|
|
161
|
+
});
|
|
162
|
+
it("does not cancel an underlying request if it is running", async () => {
|
|
163
|
+
const queue = new SubscribableRequestQueue();
|
|
164
|
+
const id1 = queue.addSubscriber();
|
|
165
|
+
const id2 = queue.addSubscriber();
|
|
166
|
+
const promise1 = queue.addRequest("test", id1, () => delay(TIMEOUT, "foo"));
|
|
167
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
168
|
+
expect(queue.requestRunning("test")).to.be.true;
|
|
169
|
+
expect(queue.isSubscribed(id1, "test")).to.be.true;
|
|
170
|
+
expect(queue.isSubscribed(id2, "test")).to.be.false;
|
|
171
|
+
queue.cancelRequest("test", id1);
|
|
172
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
173
|
+
expect(queue.isSubscribed(id1, "test")).to.be.false;
|
|
174
|
+
expect(await isRejected(promise1)).to.be.true;
|
|
175
|
+
const promise2 = queue.addRequest("test", id2, () => delay(TIMEOUT, "bar"));
|
|
176
|
+
expect(queue.isSubscribed(id2, "test")).to.be.true;
|
|
177
|
+
const result = await promise2;
|
|
178
|
+
expect(result).to.equal("foo");
|
|
179
|
+
});
|
|
180
|
+
it("does not cancel an underlying request if it has another subscription", async () => {
|
|
181
|
+
const queue = new SubscribableRequestQueue(1);
|
|
182
|
+
const id1 = queue.addSubscriber();
|
|
183
|
+
const id2 = queue.addSubscriber();
|
|
184
|
+
// `block` keeps the queue full, to keep `test` from running
|
|
185
|
+
const block = queue.addRequest("block", id1, () => delay(TIMEOUT, undefined));
|
|
186
|
+
const promise1 = queue.addRequest("test", id1, () => delay(LONG_TIMEOUT, "foo"));
|
|
187
|
+
const promise2 = queue.addRequest("test", id2, () => delay(LONG_TIMEOUT, "bar"));
|
|
188
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
189
|
+
expect(queue.requestRunning("test")).to.be.false;
|
|
190
|
+
expect(queue.isSubscribed(id1, "test")).to.be.true;
|
|
191
|
+
expect(queue.isSubscribed(id2, "test")).to.be.true;
|
|
192
|
+
queue.cancelRequest("test", id1);
|
|
193
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
194
|
+
expect(queue.isSubscribed(id1, "test")).to.be.false;
|
|
195
|
+
expect(queue.isSubscribed(id2, "test")).to.be.true;
|
|
196
|
+
expect(await isRejected(promise1)).to.be.true;
|
|
197
|
+
await block;
|
|
198
|
+
expect(queue.requestRunning("test")).to.be.true;
|
|
199
|
+
const result = await promise2;
|
|
200
|
+
expect(result).to.equal("foo");
|
|
201
|
+
});
|
|
202
|
+
it("cancels an underlying request if it is waiting and has no other subscriptions", async () => {
|
|
203
|
+
const queue = new SubscribableRequestQueue(1);
|
|
204
|
+
const id = queue.addSubscriber();
|
|
205
|
+
// `block` keeps the queue full, to keep `test` from running
|
|
206
|
+
queue.addRequest("block", id, () => delay(TIMEOUT, undefined));
|
|
207
|
+
const promise = queue.addRequest("test", id, () => delay(LONG_TIMEOUT, "foo"));
|
|
208
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
209
|
+
expect(queue.requestRunning("test")).to.be.false;
|
|
210
|
+
expect(queue.isSubscribed(id, "test")).to.be.true;
|
|
211
|
+
queue.cancelRequest("test", id);
|
|
212
|
+
expect(await isRejected(promise)).to.be.true;
|
|
213
|
+
expect(queue.hasRequest("test")).to.be.false;
|
|
214
|
+
expect(queue.isSubscribed(id, "test")).to.be.false;
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
describe("removeSubscriber", () => {
|
|
218
|
+
it("removes a subscriber", async () => {
|
|
219
|
+
const queue = new SubscribableRequestQueue();
|
|
220
|
+
const id = queue.addSubscriber();
|
|
221
|
+
expect(queue.hasSubscriber(id)).to.be.true;
|
|
222
|
+
queue.removeSubscriber(id);
|
|
223
|
+
expect(queue.hasSubscriber(id)).to.be.false;
|
|
224
|
+
});
|
|
225
|
+
it("cancels all subscriptions from a removed subscriber", async () => {
|
|
226
|
+
const queue = new SubscribableRequestQueue();
|
|
227
|
+
const id1 = queue.addSubscriber();
|
|
228
|
+
const id2 = queue.addSubscriber();
|
|
229
|
+
const promise1 = queue.addRequest("test", id1, () => delay(TIMEOUT, "foo"));
|
|
230
|
+
const promise2 = queue.addRequest("test2", id1, () => delay(TIMEOUT, "bar"));
|
|
231
|
+
const promise3 = queue.addRequest("test", id2, () => delay(TIMEOUT, "foo"));
|
|
232
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
233
|
+
expect(queue.isSubscribed(id1, "test")).to.be.true;
|
|
234
|
+
expect(queue.isSubscribed(id2, "test")).to.be.true;
|
|
235
|
+
queue.removeSubscriber(id1);
|
|
236
|
+
expect(queue.hasRequest("test")).to.be.true;
|
|
237
|
+
expect(queue.isSubscribed(id1, "test")).to.be.false;
|
|
238
|
+
expect(queue.isSubscribed(id1, "test2")).to.be.false;
|
|
239
|
+
expect(await isRejected(promise1)).to.be.true;
|
|
240
|
+
expect(await isRejected(promise2)).to.be.true;
|
|
241
|
+
expect(await isRejected(promise3)).to.be.false;
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
});
|