@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,101 @@
|
|
|
1
|
+
import type { ErrorObject } from "serialize-error";
|
|
2
|
+
import type { ImageInfo } from "../ImageInfo.js";
|
|
3
|
+
import type { VolumeDims } from "../VolumeDims.js";
|
|
4
|
+
import type { CreateLoaderOptions, PrefetchDirection } from "../loaders/index.js";
|
|
5
|
+
import type { LoadSpec, LoadedVolumeInfo } from "../loaders/IVolumeLoader.js";
|
|
6
|
+
import type { TypedArray, NumberType } from "../types.js";
|
|
7
|
+
import type { ZarrLoaderFetchOptions } from "../loaders/OmeZarrLoader.js";
|
|
8
|
+
/** The types of requests that can be made to the worker. Mostly corresponds to methods on `IVolumeLoader`. */
|
|
9
|
+
export declare const enum WorkerMsgType {
|
|
10
|
+
INIT = 0,
|
|
11
|
+
CREATE_LOADER = 1,
|
|
12
|
+
CREATE_VOLUME = 2,
|
|
13
|
+
LOAD_DIMS = 3,
|
|
14
|
+
LOAD_VOLUME_DATA = 4,
|
|
15
|
+
SET_PREFETCH_PRIORITY_DIRECTIONS = 5,
|
|
16
|
+
SYNCHRONIZE_MULTICHANNEL_LOADING = 6,
|
|
17
|
+
UPDATE_FETCH_OPTIONS = 7
|
|
18
|
+
}
|
|
19
|
+
/** The kind of response a worker can return - `SUCCESS`, `ERROR`, or `EVENT`. */
|
|
20
|
+
export declare const enum WorkerResponseResult {
|
|
21
|
+
SUCCESS = 0,
|
|
22
|
+
ERROR = 1,
|
|
23
|
+
EVENT = 2
|
|
24
|
+
}
|
|
25
|
+
/** The kind of events that can occur when loading */
|
|
26
|
+
export declare const enum WorkerEventType {
|
|
27
|
+
/** Fired to update a `Volume`'s `imageInfo` and/or `loadSpec` based on loaded data (time, channels, region, etc.) */
|
|
28
|
+
METADATA_UPDATE = 0,
|
|
29
|
+
/** Fired when data for a channel (or batch of channels) is loaded */
|
|
30
|
+
CHANNEL_LOAD = 1
|
|
31
|
+
}
|
|
32
|
+
/** All messages to/from a worker carry a `msgId`, a `type`, and a `payload` (whose type is determined by `type`). */
|
|
33
|
+
type WorkerMsgBase<T extends WorkerMsgType, P> = {
|
|
34
|
+
msgId: number;
|
|
35
|
+
type: T;
|
|
36
|
+
payload: P;
|
|
37
|
+
};
|
|
38
|
+
/** Maps each `WorkerMsgType` to the type of the payload of requests of that type. */
|
|
39
|
+
export type WorkerRequestPayload<T extends WorkerMsgType> = {
|
|
40
|
+
[WorkerMsgType.INIT]: {
|
|
41
|
+
maxCacheSize?: number;
|
|
42
|
+
maxActiveRequests?: number;
|
|
43
|
+
maxLowPriorityRequests?: number;
|
|
44
|
+
};
|
|
45
|
+
[WorkerMsgType.CREATE_LOADER]: {
|
|
46
|
+
path: string | string[];
|
|
47
|
+
options?: CreateLoaderOptions;
|
|
48
|
+
};
|
|
49
|
+
[WorkerMsgType.CREATE_VOLUME]: LoadSpec;
|
|
50
|
+
[WorkerMsgType.LOAD_DIMS]: LoadSpec;
|
|
51
|
+
[WorkerMsgType.LOAD_VOLUME_DATA]: {
|
|
52
|
+
imageInfo: ImageInfo;
|
|
53
|
+
loadSpec: LoadSpec;
|
|
54
|
+
loaderId: number;
|
|
55
|
+
loadId: number;
|
|
56
|
+
};
|
|
57
|
+
[WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS]: PrefetchDirection[];
|
|
58
|
+
[WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING]: boolean;
|
|
59
|
+
[WorkerMsgType.UPDATE_FETCH_OPTIONS]: Partial<ZarrLoaderFetchOptions>;
|
|
60
|
+
}[T];
|
|
61
|
+
/** Maps each `WorkerMsgType` to the type of the payload of responses of that type. */
|
|
62
|
+
export type WorkerResponsePayload<T extends WorkerMsgType> = {
|
|
63
|
+
[WorkerMsgType.INIT]: void;
|
|
64
|
+
[WorkerMsgType.CREATE_LOADER]: boolean;
|
|
65
|
+
[WorkerMsgType.CREATE_VOLUME]: LoadedVolumeInfo;
|
|
66
|
+
[WorkerMsgType.LOAD_DIMS]: VolumeDims[];
|
|
67
|
+
[WorkerMsgType.LOAD_VOLUME_DATA]: void;
|
|
68
|
+
[WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS]: void;
|
|
69
|
+
[WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING]: void;
|
|
70
|
+
[WorkerMsgType.UPDATE_FETCH_OPTIONS]: void;
|
|
71
|
+
}[T];
|
|
72
|
+
/** Event for when a batch of channel data loads. */
|
|
73
|
+
export type ChannelLoadEvent = {
|
|
74
|
+
eventType: WorkerEventType.CHANNEL_LOAD;
|
|
75
|
+
loaderId: number;
|
|
76
|
+
loadId: number;
|
|
77
|
+
channelIndex: number[];
|
|
78
|
+
dtype: NumberType[];
|
|
79
|
+
data: TypedArray<NumberType>[];
|
|
80
|
+
ranges: [number, number][];
|
|
81
|
+
atlasDims?: [number, number];
|
|
82
|
+
};
|
|
83
|
+
/** Event for when metadata updates. */
|
|
84
|
+
export type MetadataUpdateEvent = {
|
|
85
|
+
eventType: WorkerEventType.METADATA_UPDATE;
|
|
86
|
+
loaderId: number;
|
|
87
|
+
loadId: number;
|
|
88
|
+
imageInfo?: ImageInfo;
|
|
89
|
+
loadSpec?: LoadSpec;
|
|
90
|
+
};
|
|
91
|
+
/** All valid types of worker requests, with some `WorkerMsgType` and a matching payload type. */
|
|
92
|
+
export type WorkerRequest<T extends WorkerMsgType> = WorkerMsgBase<T, WorkerRequestPayload<T>>;
|
|
93
|
+
/** All valid types of worker responses: `SUCCESS` with a matching payload, `ERROR` with a message, or an `EVENT`. */
|
|
94
|
+
export type WorkerResponse<T extends WorkerMsgType> = ({
|
|
95
|
+
responseResult: WorkerResponseResult.SUCCESS;
|
|
96
|
+
} & WorkerMsgBase<T, WorkerResponsePayload<T>>) | ({
|
|
97
|
+
responseResult: WorkerResponseResult.ERROR;
|
|
98
|
+
} & WorkerMsgBase<T, ErrorObject>) | ({
|
|
99
|
+
responseResult: WorkerResponseResult.EVENT;
|
|
100
|
+
} & (ChannelLoadEvent | MetadataUpdateEvent));
|
|
101
|
+
export {};
|
package/es/types.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// numeric types compatible with zarrita.js.
|
|
2
|
+
// see https://github.com/manzt/zarrita.js/blob/main/packages/core/src/metadata.ts
|
|
3
|
+
|
|
4
|
+
export const ARRAY_CONSTRUCTORS = {
|
|
5
|
+
int8: Int8Array,
|
|
6
|
+
int16: Int16Array,
|
|
7
|
+
int32: Int32Array,
|
|
8
|
+
int64: globalThis.BigInt64Array,
|
|
9
|
+
uint8: Uint8Array,
|
|
10
|
+
uint16: Uint16Array,
|
|
11
|
+
uint32: Uint32Array,
|
|
12
|
+
uint64: globalThis.BigUint64Array,
|
|
13
|
+
float32: Float32Array,
|
|
14
|
+
float64: Float64Array
|
|
15
|
+
};
|
|
16
|
+
/** If `FuseChannel.rgbColor` is this value, it is disabled from fusion. */
|
|
17
|
+
export const FUSE_DISABLED_RGB_COLOR = 0;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Provide options to control the visual appearance of a Volume
|
|
21
|
+
* @typedef {Object} VolumeChannelDisplayOptions
|
|
22
|
+
* @property {boolean} enabled array of boolean per channel
|
|
23
|
+
* @property {Array.<number>} color array of rgb per channel
|
|
24
|
+
* @property {Array.<number>} specularColor array of rgb per channel
|
|
25
|
+
* @property {Array.<number>} emissiveColor array of rgb per channel
|
|
26
|
+
* @property {number} glossiness array of float per channel
|
|
27
|
+
* @property {boolean} isosurfaceEnabled array of boolean per channel
|
|
28
|
+
* @property {number} isovalue array of number per channel
|
|
29
|
+
* @property {number} isosurfaceOpacity array of number per channel
|
|
30
|
+
* @example let options = {
|
|
31
|
+
};
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export let RenderMode = /*#__PURE__*/function (RenderMode) {
|
|
35
|
+
RenderMode[RenderMode["RAYMARCH"] = 0] = "RAYMARCH";
|
|
36
|
+
RenderMode[RenderMode["PATHTRACE"] = 1] = "PATHTRACE";
|
|
37
|
+
RenderMode[RenderMode["SLICE"] = 2] = "SLICE";
|
|
38
|
+
return RenderMode;
|
|
39
|
+
}({});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Provide options to control the visual appearance of a Volume
|
|
43
|
+
* @typedef {Object} VolumeDisplayOptions
|
|
44
|
+
* @property {Array.<VolumeChannelDisplayOptions>} channels array of channel display options
|
|
45
|
+
* @property {number} density
|
|
46
|
+
* @property {Array.<number>} translation xyz
|
|
47
|
+
* @property {Array.<number>} rotation xyz angles in radians
|
|
48
|
+
* @property {number} maskChannelIndex
|
|
49
|
+
* @property {number} maskAlpha
|
|
50
|
+
* @property {Array.<number>} clipBounds [xmin, xmax, ymin, ymax, zmin, zmax] all range from 0 to 1 as a percentage of the volume on that axis
|
|
51
|
+
* @property {Array.<number>} scale xyz voxel size scaling
|
|
52
|
+
* @property {boolean} maxProjection true or false (ray marching)
|
|
53
|
+
* @property {number} renderMode 0 for raymarch, 1 for pathtrace
|
|
54
|
+
* @property {number} shadingMethod 0 for phase, 1 for brdf, 2 for hybrid (path tracer)
|
|
55
|
+
* @property {Array.<number>} gamma [min, max, scale]
|
|
56
|
+
* @property {number} primaryRayStepSize in voxels
|
|
57
|
+
* @property {number} secondaryRayStepSize in voxels
|
|
58
|
+
* @property {boolean} showBoundingBox true or false
|
|
59
|
+
* @property {Array.<number>} boundingBoxColor r,g,b for bounding box lines
|
|
60
|
+
* @example let options = {
|
|
61
|
+
};
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
export const isOrthographicCamera = def => def && def.isOrthographicCamera;
|
|
65
|
+
export const isPerspectiveCamera = def => def && def.isPerspectiveCamera;
|
|
66
|
+
export let ViewportCorner = /*#__PURE__*/function (ViewportCorner) {
|
|
67
|
+
ViewportCorner["TOP_LEFT"] = "top_left";
|
|
68
|
+
ViewportCorner["TOP_RIGHT"] = "top_right";
|
|
69
|
+
ViewportCorner["BOTTOM_LEFT"] = "bottom_left";
|
|
70
|
+
ViewportCorner["BOTTOM_RIGHT"] = "bottom_right";
|
|
71
|
+
return ViewportCorner;
|
|
72
|
+
}({});
|
|
73
|
+
export const isTop = corner => corner === ViewportCorner.TOP_LEFT || corner === ViewportCorner.TOP_RIGHT;
|
|
74
|
+
export const isRight = corner => corner === ViewportCorner.TOP_RIGHT || corner === ViewportCorner.BOTTOM_RIGHT;
|
|
75
|
+
export const DATARANGE_UINT8 = [0, 255];
|
package/es/typings.d.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/** Object format used when passing multiple requests to RequestQueue at once. */
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_REQUEST_CANCEL_REASON = "request cancelled";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal object interface used by RequestQueue to store request metadata and callbacks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Manages a queue of asynchronous requests with unique string keys, which can be added to or cancelled.
|
|
11
|
+
* If redundant requests with the same key are issued, the request action will only be run once per key
|
|
12
|
+
* while the original request is still in the queue.
|
|
13
|
+
*/
|
|
14
|
+
export default class RequestQueue {
|
|
15
|
+
/**
|
|
16
|
+
* The maximum number of requests that can be handled concurrently.
|
|
17
|
+
* Once reached, additional requests will be queued up to run once a running request completes.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The maximum number of requests that can be handled concurrently if only low-priority requests are waiting. Set
|
|
22
|
+
* lower than `concurrencyLimit` to always leave space for high-priority requests. Cannot be set higher than
|
|
23
|
+
* `concurrencyLimit`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** A queue of requests that are ready to be executed, in order of request time. */
|
|
27
|
+
|
|
28
|
+
/** A queue of low-priority tasks that are ready to be executed. `queue` must be empty before any of these tasks run. */
|
|
29
|
+
|
|
30
|
+
/** Stores all requests, even those that are currently active. */
|
|
31
|
+
|
|
32
|
+
/** Stores requests whose actions are currently being run. */
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new RequestQueue.
|
|
36
|
+
* @param maxActiveRequests The maximum number of requests that will be handled concurrently. This is 10 by default.
|
|
37
|
+
* @param maxLowPriorityRequests The maximum number of low-priority requests that will be handled concurrently. Equal
|
|
38
|
+
* to `maxActiveRequests` by default, but may be set lower to always leave space for new high-priority requests.
|
|
39
|
+
*/
|
|
40
|
+
constructor(maxActiveRequests = 10, maxLowPriorityRequests = 5) {
|
|
41
|
+
this.allRequests = new Map();
|
|
42
|
+
this.activeRequests = new Set();
|
|
43
|
+
this.queue = [];
|
|
44
|
+
this.queueLowPriority = [];
|
|
45
|
+
this.maxActiveRequests = maxActiveRequests;
|
|
46
|
+
this.maxLowPriorityRequests = Math.min(maxActiveRequests, maxLowPriorityRequests);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stores request metadata to the internal map of all pending requests.
|
|
51
|
+
* @param key string identifier of the request.
|
|
52
|
+
* @param requestAction callable function action of the request.
|
|
53
|
+
* @returns a reference to the new, registered RequestItem.
|
|
54
|
+
*/
|
|
55
|
+
registerRequest(key, requestAction) {
|
|
56
|
+
// Create a new promise and store the resolve and reject callbacks for later.
|
|
57
|
+
// This lets us perform the actual action at a later point, when the request is at the
|
|
58
|
+
// front of the processing queue.
|
|
59
|
+
let promiseResolve, promiseReject;
|
|
60
|
+
const promise = new Promise((resolve, reject) => {
|
|
61
|
+
promiseResolve = resolve;
|
|
62
|
+
promiseReject = reject;
|
|
63
|
+
});
|
|
64
|
+
// Store the request data.
|
|
65
|
+
const requestItem = {
|
|
66
|
+
key: key,
|
|
67
|
+
action: requestAction,
|
|
68
|
+
resolve: promiseResolve,
|
|
69
|
+
reject: promiseReject,
|
|
70
|
+
promise
|
|
71
|
+
};
|
|
72
|
+
this.allRequests.set(key, requestItem);
|
|
73
|
+
return requestItem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Moves a registered request into the processing queue, clearing any timeouts on the request.
|
|
78
|
+
* @param key string identifier of the request.
|
|
79
|
+
* @param lowPriority Whether this request should be added with low priority. False by default.
|
|
80
|
+
*/
|
|
81
|
+
addRequestToQueue(key, lowPriority) {
|
|
82
|
+
// Check that this request is not cancelled.
|
|
83
|
+
if (this.allRequests.has(key)) {
|
|
84
|
+
// Clear the request timeout, if it has one, since it is being added to the queue.
|
|
85
|
+
const requestItem = this.allRequests.get(key);
|
|
86
|
+
if (requestItem && requestItem.timeoutId) {
|
|
87
|
+
clearTimeout(requestItem.timeoutId);
|
|
88
|
+
requestItem.timeoutId = undefined;
|
|
89
|
+
}
|
|
90
|
+
if (!this.queue.includes(key) && !this.queueLowPriority.includes(key)) {
|
|
91
|
+
// Add to queue and check if the request can be processed right away.
|
|
92
|
+
if (lowPriority) {
|
|
93
|
+
this.queueLowPriority.push(key);
|
|
94
|
+
} else {
|
|
95
|
+
this.queue.push(key);
|
|
96
|
+
}
|
|
97
|
+
this.dequeue();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Adds a request with a unique key to the queue, if it doesn't already exist.
|
|
104
|
+
* @param key The key used to track the request.
|
|
105
|
+
* @param requestAction Function that will be called to complete the request. The function
|
|
106
|
+
* will be run only once per unique key while the request exists, and may be deferred by the
|
|
107
|
+
* queue at any time.
|
|
108
|
+
* @param lowPriority Whether this request should be added with low priority. False by default.
|
|
109
|
+
* @param delayMs Minimum delay, in milliseconds, before this request should be executed.
|
|
110
|
+
*
|
|
111
|
+
* NOTE: Cancelling a request while the action is running WILL NOT stop the action. If this behavior is desired,
|
|
112
|
+
* actions must be responsible for checking the RequestQueue, determining if the request is still valid (e.g.
|
|
113
|
+
* using `.hasRequest()`), and stopping or returning early.
|
|
114
|
+
*
|
|
115
|
+
* @returns A promise that will resolve on completion of the request, or reject if the request is cancelled.
|
|
116
|
+
* If multiple requests are issued with the same key, a promise for the first request will be returned
|
|
117
|
+
* until the request is resolved or cancelled.
|
|
118
|
+
* Note that the return type of the promise will match that of the first request's instance.
|
|
119
|
+
*/
|
|
120
|
+
addRequest(key, requestAction, lowPriority = false, delayMs = 0) {
|
|
121
|
+
if (!this.allRequests.has(key)) {
|
|
122
|
+
// New request!
|
|
123
|
+
const requestItem = this.registerRequest(key, requestAction);
|
|
124
|
+
// If a delay is set, wait to add this to the queue.
|
|
125
|
+
if (delayMs > 0) {
|
|
126
|
+
const timeoutId = setTimeout(() => this.addRequestToQueue(key, lowPriority), delayMs);
|
|
127
|
+
// Save timeout information to request metadata
|
|
128
|
+
requestItem.timeoutId = timeoutId;
|
|
129
|
+
} else {
|
|
130
|
+
// No delay, add immediately
|
|
131
|
+
this.addRequestToQueue(key, lowPriority);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
const lowPriorityIndex = this.queueLowPriority.indexOf(key);
|
|
135
|
+
if (lowPriorityIndex > -1 && !lowPriority) {
|
|
136
|
+
// This request is registered and queued, but is now being requested with high priority.
|
|
137
|
+
// Promote it to high priority.
|
|
138
|
+
this.queueLowPriority.splice(lowPriorityIndex, 1);
|
|
139
|
+
this.addRequestToQueue(key);
|
|
140
|
+
} else if (delayMs <= 0) {
|
|
141
|
+
// This request is registered, but is now being requested without a delay.
|
|
142
|
+
// Move into queue immediately if it's not already added, and clear any timeouts it may have.
|
|
143
|
+
this.addRequestToQueue(key, lowPriority);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const promise = this.allRequests.get(key)?.promise;
|
|
147
|
+
if (!promise) {
|
|
148
|
+
throw new Error("Found no promise to return when getting stored request data.");
|
|
149
|
+
}
|
|
150
|
+
return promise;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Adds multiple requests to the queue, with an optional delay between each.
|
|
155
|
+
* @param requests An array of RequestItems, which include a key and a request action.
|
|
156
|
+
* @param lowPriority Whether these requests should be added with low priority. False by default.
|
|
157
|
+
* @param delayMs An optional minimum delay in milliseconds to be added between each request.
|
|
158
|
+
* For example, a delay of 10 ms will cause the second request to be added to the processing queue
|
|
159
|
+
* after 10 ms, the third to added after 20 ms, and so on. Set to 10 ms by default.
|
|
160
|
+
* @returns An array of promises corresponding to the provided requests. (i.e., the `i`th value
|
|
161
|
+
* of the returned array will be a Promise for the resolution of `requests[i]`). If a request
|
|
162
|
+
* with a matching key is already pending, returns the promise for the initial request.
|
|
163
|
+
*/
|
|
164
|
+
addRequests(requests, lowPriority = false, delayMs = 10) {
|
|
165
|
+
const promises = [];
|
|
166
|
+
for (let i = 0; i < requests.length; i++) {
|
|
167
|
+
const item = requests[i];
|
|
168
|
+
const promise = this.addRequest(item.key, item.requestAction, lowPriority, delayMs * i);
|
|
169
|
+
promises.push(promise);
|
|
170
|
+
}
|
|
171
|
+
return promises;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Attempts to remove and run the next queued request item, if resources are available.
|
|
176
|
+
* @returns true if a request was started, or false if there are too many
|
|
177
|
+
* requests already active.
|
|
178
|
+
*/
|
|
179
|
+
async dequeue() {
|
|
180
|
+
const numRequests = this.activeRequests.size;
|
|
181
|
+
if (numRequests >= this.maxActiveRequests || this.queue.length === 0 && (numRequests >= this.maxLowPriorityRequests || this.queueLowPriority.length === 0)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const requestKey = this.queue.shift() ?? this.queueLowPriority.shift();
|
|
185
|
+
if (!requestKey) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (this.activeRequests.has(requestKey)) {
|
|
189
|
+
// This request is already active, try the next one instead. (this shouldn't happen)
|
|
190
|
+
this.dequeue();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const requestItem = this.allRequests.get(requestKey);
|
|
194
|
+
if (!requestItem) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const key = requestItem.key;
|
|
198
|
+
// Mark that this request is active
|
|
199
|
+
this.activeRequests.add(key);
|
|
200
|
+
await requestItem.action().then(requestItem.resolve, requestItem.reject);
|
|
201
|
+
this.activeRequests.delete(key);
|
|
202
|
+
this.allRequests.delete(key);
|
|
203
|
+
this.dequeue();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Removes any request matching the provided key from the queue and rejects its promise.
|
|
208
|
+
* @param key The key that should be matched against.
|
|
209
|
+
* @param cancelReason A message or object that will be used as the promise rejection.
|
|
210
|
+
*/
|
|
211
|
+
cancelRequest(key, cancelReason = DEFAULT_REQUEST_CANCEL_REASON) {
|
|
212
|
+
if (!this.allRequests.has(key)) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const requestItem = this.allRequests.get(key);
|
|
216
|
+
if (requestItem) {
|
|
217
|
+
if (requestItem.timeoutId) {
|
|
218
|
+
// Cancel requests that have not been queued yet.
|
|
219
|
+
clearTimeout(requestItem.timeoutId);
|
|
220
|
+
}
|
|
221
|
+
// Reject the request, then clear from the queue and known requests.
|
|
222
|
+
requestItem.reject(cancelReason);
|
|
223
|
+
}
|
|
224
|
+
const queueIndex = this.queue.indexOf(key);
|
|
225
|
+
if (queueIndex > -1) {
|
|
226
|
+
this.queue.splice(queueIndex, 1);
|
|
227
|
+
} else {
|
|
228
|
+
const lowPriorityIndex = this.queueLowPriority.indexOf(key);
|
|
229
|
+
if (lowPriorityIndex > -1) {
|
|
230
|
+
this.queueLowPriority.splice(lowPriorityIndex, 1);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
this.allRequests.delete(key);
|
|
234
|
+
this.activeRequests.delete(key);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Rejects all request promises and clears the queue.
|
|
239
|
+
* @param cancelReason A message or object that will be used as the promise rejection.
|
|
240
|
+
*/
|
|
241
|
+
cancelAllRequests(cancelReason = DEFAULT_REQUEST_CANCEL_REASON) {
|
|
242
|
+
// Clear the queue so we don't do extra work while filtering it
|
|
243
|
+
this.queue = [];
|
|
244
|
+
this.queueLowPriority = [];
|
|
245
|
+
for (const key of this.allRequests.keys()) {
|
|
246
|
+
this.cancelRequest(key, cancelReason);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Returns whether a request with the given key exists in the RequestQueue and is not cancelled.
|
|
252
|
+
* @param key the key to search for.
|
|
253
|
+
* @returns true if the request is in the RequestQueue.
|
|
254
|
+
*/
|
|
255
|
+
hasRequest(key) {
|
|
256
|
+
return this.allRequests.has(key);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Returns whether the request with the given key is currently running (not waiting in the queue).
|
|
261
|
+
* @param key the key to search for.
|
|
262
|
+
* @returns true if the request is actively running.
|
|
263
|
+
*/
|
|
264
|
+
requestRunning(key) {
|
|
265
|
+
return this.activeRequests.has(key);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import RequestQueue from "./RequestQueue.js";
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An extension of `RequestQueue` that adds a concept of "subscribers," which may share references to a single request
|
|
7
|
+
* or cancel their subscription without disrupting the request for other subscribers.
|
|
8
|
+
*/
|
|
9
|
+
export default class SubscribableRequestQueue {
|
|
10
|
+
/** The next unused subscriber ID. Increments whenever a subscriber is added. */
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Map of subscribers keyed by ID. Subscribers store a map to all their subscriptions by request key.
|
|
14
|
+
* Subscribers are only useful as handles to cancel subscriptions early, so we only need to store rejecters here.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Map from "inner" request (managed by `queue`) to "outer" promises generated per-subscriber. */
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Since `SubscribableRequestQueue` wraps `RequestQueue`, its constructor may either take the same arguments as the
|
|
21
|
+
* `RequestQueue` constructor and create a new `RequestQueue`, or it may take an existing `RequestQueue` to wrap.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
constructor(maxActiveRequests, maxLowPriorityRequests) {
|
|
25
|
+
if (typeof maxActiveRequests === "number" || maxActiveRequests === undefined) {
|
|
26
|
+
this.queue = new RequestQueue(maxActiveRequests, maxLowPriorityRequests);
|
|
27
|
+
} else {
|
|
28
|
+
this.queue = maxActiveRequests;
|
|
29
|
+
}
|
|
30
|
+
this.nextSubscriberId = 0;
|
|
31
|
+
this.subscribers = new Map();
|
|
32
|
+
this.requests = new Map();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Resolves all subscriptions to request `key` with `value` */
|
|
36
|
+
resolveAll(key, value) {
|
|
37
|
+
const requests = this.requests.get(key);
|
|
38
|
+
if (requests) {
|
|
39
|
+
for (const {
|
|
40
|
+
resolve,
|
|
41
|
+
subscriberId
|
|
42
|
+
} of requests) {
|
|
43
|
+
resolve(value);
|
|
44
|
+
this.subscribers.get(subscriberId)?.delete(key);
|
|
45
|
+
}
|
|
46
|
+
this.requests.delete(key);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Rejects all subscriptions to request `key` with `reason` */
|
|
51
|
+
rejectAll(key, reason) {
|
|
52
|
+
const requests = this.requests.get(key);
|
|
53
|
+
if (requests) {
|
|
54
|
+
for (const {
|
|
55
|
+
reject,
|
|
56
|
+
subscriberId
|
|
57
|
+
} of requests) {
|
|
58
|
+
reject(reason);
|
|
59
|
+
this.subscribers.get(subscriberId)?.delete(key);
|
|
60
|
+
}
|
|
61
|
+
this.requests.delete(key);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Adds a new request subscriber. Returns a unique ID to identify this subscriber. */
|
|
66
|
+
addSubscriber() {
|
|
67
|
+
const subscriberId = this.nextSubscriberId;
|
|
68
|
+
this.nextSubscriberId++;
|
|
69
|
+
this.subscribers.set(subscriberId, new Map());
|
|
70
|
+
return subscriberId;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Queues a new request, or adds a subscription if the request is already queued/running.
|
|
75
|
+
*
|
|
76
|
+
* If `subscriberId` is already subscribed to the request, this rejects the existing promise and returns a new one.
|
|
77
|
+
*/
|
|
78
|
+
addRequest(key, subscriberId, requestAction, lowPriority, delayMs) {
|
|
79
|
+
// Create single underlying request if it does not yet exist
|
|
80
|
+
this.queue.addRequest(key, requestAction, lowPriority, delayMs).then(value => this.resolveAll(key, value)).catch(reason => this.rejectAll(key, reason));
|
|
81
|
+
if (!this.requests.has(key)) {
|
|
82
|
+
this.requests.set(key, []);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Validate subscriber
|
|
86
|
+
if (subscriberId >= this.nextSubscriberId || subscriberId < 0) {
|
|
87
|
+
throw new Error(`SubscribableRequestQueue: subscriber id ${subscriberId} has not been registered`);
|
|
88
|
+
}
|
|
89
|
+
const subscriber = this.subscribers.get(subscriberId);
|
|
90
|
+
if (!subscriber) {
|
|
91
|
+
throw new Error(`SubscribableRequestQueue: subscriber id ${subscriberId} has been removed`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create promise and add to list of requests
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
this.requests.get(key)?.push({
|
|
97
|
+
resolve,
|
|
98
|
+
reject,
|
|
99
|
+
subscriberId
|
|
100
|
+
});
|
|
101
|
+
const subscriber = this.subscribers.get(subscriberId);
|
|
102
|
+
const existingRequest = subscriber?.get(key);
|
|
103
|
+
if (existingRequest) {
|
|
104
|
+
existingRequest.push(reject);
|
|
105
|
+
} else {
|
|
106
|
+
subscriber?.set(key, [reject]);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Rejects a subscription and removes it from the list of subscriptions for a request, then cancels the underlying
|
|
113
|
+
* request if it is no longer subscribed and is not running already.
|
|
114
|
+
*/
|
|
115
|
+
rejectSubscription(key, reject, cancelReason) {
|
|
116
|
+
// Reject the outer "subscription" promise
|
|
117
|
+
reject(cancelReason);
|
|
118
|
+
|
|
119
|
+
// Get the list of subscriptions for this request
|
|
120
|
+
const subscriptions = this.requests.get(key);
|
|
121
|
+
if (!subscriptions) {
|
|
122
|
+
// This should never happen
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Remove this request subscription by ref equality to `reject`
|
|
126
|
+
const idx = subscriptions.findIndex(sub => sub.reject === reject);
|
|
127
|
+
if (idx >= 0) {
|
|
128
|
+
subscriptions.splice(idx, 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Remove the underlying request if there are no more subscribers and the request is not already running
|
|
132
|
+
if (subscriptions.length < 1 && !this.queue.requestRunning(key)) {
|
|
133
|
+
this.queue.cancelRequest(key, cancelReason);
|
|
134
|
+
this.requests.delete(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Cancels a request subscription, and cancels the underlying request if it is no longer subscribed or running. */
|
|
139
|
+
cancelRequest(key, subscriberId, cancelReason) {
|
|
140
|
+
const subscriber = this.subscribers.get(subscriberId);
|
|
141
|
+
if (!subscriber) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const rejecters = subscriber.get(key);
|
|
145
|
+
if (!rejecters || !rejecters.length) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
for (const reject of rejecters) {
|
|
149
|
+
this.rejectSubscription(key, reject, cancelReason);
|
|
150
|
+
}
|
|
151
|
+
subscriber.delete(key);
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Removes a subscriber and cancels its remaining subscriptions. */
|
|
156
|
+
removeSubscriber(subscriberId, cancelReason) {
|
|
157
|
+
const subscriptions = this.subscribers.get(subscriberId);
|
|
158
|
+
if (subscriptions) {
|
|
159
|
+
for (const [key, rejecters] of subscriptions.entries()) {
|
|
160
|
+
for (const reject of rejecters) {
|
|
161
|
+
this.rejectSubscription(key, reject, cancelReason);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.subscribers.delete(subscriberId);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Returns whether a request with the given `key` is running or waiting in the queue */
|
|
169
|
+
hasRequest(key) {
|
|
170
|
+
return this.queue.hasRequest(key);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Returns whether a request with the given `key` is running */
|
|
174
|
+
requestRunning(key) {
|
|
175
|
+
return this.queue.requestRunning(key);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Returns whether a subscriber with the given `subscriberId` exists */
|
|
179
|
+
hasSubscriber(subscriberId) {
|
|
180
|
+
return this.subscribers.has(subscriberId);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Returns whether a subscriber with the given `subscriberId` is subscribed to the request with the given `key` */
|
|
184
|
+
isSubscribed(subscriberId, key) {
|
|
185
|
+
return this.subscribers.get(subscriberId)?.has(key) ?? false;
|
|
186
|
+
}
|
|
187
|
+
}
|