@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.
Files changed (141) hide show
  1. package/LICENSE.txt +26 -0
  2. package/README.md +119 -0
  3. package/es/Atlas2DSlice.js +224 -0
  4. package/es/Channel.js +264 -0
  5. package/es/FileSaver.js +31 -0
  6. package/es/FusedChannelData.js +192 -0
  7. package/es/Histogram.js +250 -0
  8. package/es/ImageInfo.js +127 -0
  9. package/es/Light.js +74 -0
  10. package/es/Lut.js +500 -0
  11. package/es/MarchingCubes.js +507 -0
  12. package/es/MeshVolume.js +334 -0
  13. package/es/NaiveSurfaceNets.js +251 -0
  14. package/es/PathTracedVolume.js +482 -0
  15. package/es/RayMarchedAtlasVolume.js +250 -0
  16. package/es/RenderToBuffer.js +31 -0
  17. package/es/ThreeJsPanel.js +633 -0
  18. package/es/Timing.js +28 -0
  19. package/es/TrackballControls.js +538 -0
  20. package/es/View3d.js +848 -0
  21. package/es/Volume.js +352 -0
  22. package/es/VolumeCache.js +161 -0
  23. package/es/VolumeDims.js +16 -0
  24. package/es/VolumeDrawable.js +702 -0
  25. package/es/VolumeMaker.js +101 -0
  26. package/es/VolumeRenderImpl.js +1 -0
  27. package/es/VolumeRenderSettings.js +203 -0
  28. package/es/constants/basicShaders.js +29 -0
  29. package/es/constants/colors.js +59 -0
  30. package/es/constants/denoiseShader.js +43 -0
  31. package/es/constants/lights.js +42 -0
  32. package/es/constants/materials.js +85 -0
  33. package/es/constants/pathtraceOutputShader.js +13 -0
  34. package/es/constants/scaleBarSVG.js +21 -0
  35. package/es/constants/time.js +34 -0
  36. package/es/constants/volumePTshader.js +153 -0
  37. package/es/constants/volumeRayMarchShader.js +123 -0
  38. package/es/constants/volumeSliceShader.js +115 -0
  39. package/es/index.js +21 -0
  40. package/es/loaders/IVolumeLoader.js +131 -0
  41. package/es/loaders/JsonImageInfoLoader.js +255 -0
  42. package/es/loaders/OmeZarrLoader.js +495 -0
  43. package/es/loaders/OpenCellLoader.js +65 -0
  44. package/es/loaders/RawArrayLoader.js +89 -0
  45. package/es/loaders/TiffLoader.js +219 -0
  46. package/es/loaders/VolumeLoadError.js +44 -0
  47. package/es/loaders/VolumeLoaderUtils.js +221 -0
  48. package/es/loaders/index.js +40 -0
  49. package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +143 -0
  50. package/es/loaders/zarr_utils/WrappedStore.js +51 -0
  51. package/es/loaders/zarr_utils/types.js +24 -0
  52. package/es/loaders/zarr_utils/utils.js +225 -0
  53. package/es/loaders/zarr_utils/validation.js +49 -0
  54. package/es/test/ChunkPrefetchIterator.test.js +208 -0
  55. package/es/test/RequestQueue.test.js +442 -0
  56. package/es/test/SubscribableRequestQueue.test.js +244 -0
  57. package/es/test/VolumeCache.test.js +118 -0
  58. package/es/test/VolumeRenderSettings.test.js +71 -0
  59. package/es/test/lut.test.js +671 -0
  60. package/es/test/num_utils.test.js +140 -0
  61. package/es/test/volume.test.js +98 -0
  62. package/es/test/zarr_utils.test.js +358 -0
  63. package/es/types/Atlas2DSlice.d.ts +41 -0
  64. package/es/types/Channel.d.ts +44 -0
  65. package/es/types/FileSaver.d.ts +6 -0
  66. package/es/types/FusedChannelData.d.ts +26 -0
  67. package/es/types/Histogram.d.ts +57 -0
  68. package/es/types/ImageInfo.d.ts +87 -0
  69. package/es/types/Light.d.ts +27 -0
  70. package/es/types/Lut.d.ts +67 -0
  71. package/es/types/MarchingCubes.d.ts +53 -0
  72. package/es/types/MeshVolume.d.ts +40 -0
  73. package/es/types/NaiveSurfaceNets.d.ts +11 -0
  74. package/es/types/PathTracedVolume.d.ts +65 -0
  75. package/es/types/RayMarchedAtlasVolume.d.ts +41 -0
  76. package/es/types/RenderToBuffer.d.ts +17 -0
  77. package/es/types/ThreeJsPanel.d.ts +107 -0
  78. package/es/types/Timing.d.ts +11 -0
  79. package/es/types/TrackballControls.d.ts +51 -0
  80. package/es/types/View3d.d.ts +357 -0
  81. package/es/types/Volume.d.ts +152 -0
  82. package/es/types/VolumeCache.d.ts +43 -0
  83. package/es/types/VolumeDims.d.ts +28 -0
  84. package/es/types/VolumeDrawable.d.ts +108 -0
  85. package/es/types/VolumeMaker.d.ts +49 -0
  86. package/es/types/VolumeRenderImpl.d.ts +22 -0
  87. package/es/types/VolumeRenderSettings.d.ts +98 -0
  88. package/es/types/constants/basicShaders.d.ts +4 -0
  89. package/es/types/constants/colors.d.ts +2 -0
  90. package/es/types/constants/denoiseShader.d.ts +40 -0
  91. package/es/types/constants/lights.d.ts +38 -0
  92. package/es/types/constants/materials.d.ts +20 -0
  93. package/es/types/constants/pathtraceOutputShader.d.ts +11 -0
  94. package/es/types/constants/scaleBarSVG.d.ts +2 -0
  95. package/es/types/constants/time.d.ts +19 -0
  96. package/es/types/constants/volumePTshader.d.ts +137 -0
  97. package/es/types/constants/volumeRayMarchShader.d.ts +117 -0
  98. package/es/types/constants/volumeSliceShader.d.ts +109 -0
  99. package/es/types/glsl.d.js +0 -0
  100. package/es/types/index.d.ts +28 -0
  101. package/es/types/loaders/IVolumeLoader.d.ts +113 -0
  102. package/es/types/loaders/JsonImageInfoLoader.d.ts +80 -0
  103. package/es/types/loaders/OmeZarrLoader.d.ts +87 -0
  104. package/es/types/loaders/OpenCellLoader.d.ts +9 -0
  105. package/es/types/loaders/RawArrayLoader.d.ts +33 -0
  106. package/es/types/loaders/TiffLoader.d.ts +45 -0
  107. package/es/types/loaders/VolumeLoadError.d.ts +18 -0
  108. package/es/types/loaders/VolumeLoaderUtils.d.ts +38 -0
  109. package/es/types/loaders/index.d.ts +22 -0
  110. package/es/types/loaders/zarr_utils/ChunkPrefetchIterator.d.ts +22 -0
  111. package/es/types/loaders/zarr_utils/WrappedStore.d.ts +24 -0
  112. package/es/types/loaders/zarr_utils/types.d.ts +94 -0
  113. package/es/types/loaders/zarr_utils/utils.d.ts +23 -0
  114. package/es/types/loaders/zarr_utils/validation.d.ts +7 -0
  115. package/es/types/test/ChunkPrefetchIterator.test.d.ts +1 -0
  116. package/es/types/test/RequestQueue.test.d.ts +1 -0
  117. package/es/types/test/SubscribableRequestQueue.test.d.ts +1 -0
  118. package/es/types/test/VolumeCache.test.d.ts +1 -0
  119. package/es/types/test/VolumeRenderSettings.test.d.ts +1 -0
  120. package/es/types/test/lut.test.d.ts +1 -0
  121. package/es/types/test/num_utils.test.d.ts +1 -0
  122. package/es/types/test/volume.test.d.ts +1 -0
  123. package/es/types/test/zarr_utils.test.d.ts +1 -0
  124. package/es/types/types.d.ts +115 -0
  125. package/es/types/utils/RequestQueue.d.ts +112 -0
  126. package/es/types/utils/SubscribableRequestQueue.d.ts +52 -0
  127. package/es/types/utils/num_utils.d.ts +43 -0
  128. package/es/types/workers/VolumeLoaderContext.d.ts +106 -0
  129. package/es/types/workers/types.d.ts +101 -0
  130. package/es/types/workers/util.d.ts +3 -0
  131. package/es/types.js +75 -0
  132. package/es/typings.d.js +0 -0
  133. package/es/utils/RequestQueue.js +267 -0
  134. package/es/utils/SubscribableRequestQueue.js +187 -0
  135. package/es/utils/num_utils.js +231 -0
  136. package/es/workers/FetchTiffWorker.js +153 -0
  137. package/es/workers/VolumeLoadWorker.js +129 -0
  138. package/es/workers/VolumeLoaderContext.js +271 -0
  139. package/es/workers/types.js +41 -0
  140. package/es/workers/util.js +8 -0
  141. 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 {};
@@ -0,0 +1,3 @@
1
+ import { LoadSpec } from "../loaders/IVolumeLoader";
2
+ /** Recreates a `LoadSpec` that has just been sent to/from a worker to restore three.js object prototypes */
3
+ export declare function rebuildLoadSpec(spec: LoadSpec): LoadSpec;
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];
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
+ }