@aics/vole-core 3.12.4 → 3.13.1

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 (50) hide show
  1. package/README.md +21 -13
  2. package/es/View3d.js +21 -5
  3. package/es/Volume.js +1 -1
  4. package/es/VolumeCache.js +10 -3
  5. package/es/VolumeRenderSettings.js +11 -0
  6. package/es/loaders/JsonImageInfoLoader.js +2 -1
  7. package/es/loaders/OmeZarrLoader.js +30 -31
  8. package/es/loaders/TiffLoader.js +3 -1
  9. package/es/loaders/VolumeLoadError.js +1 -1
  10. package/es/loaders/zarr_utils/ChunkPrefetchIterator.js +7 -0
  11. package/es/loaders/zarr_utils/validation.js +18 -7
  12. package/es/loaders/zarr_utils/wrapArray.js +39 -0
  13. package/es/types/NaiveSurfaceNets.d.ts +1 -1
  14. package/es/types/RayMarchedAtlasVolume.d.ts +1 -1
  15. package/es/types/ThreeJsPanel.d.ts +2 -2
  16. package/es/types/TrackballControls.d.ts +1 -1
  17. package/es/types/View3d.d.ts +6 -2
  18. package/es/types/VolumeCache.d.ts +5 -2
  19. package/es/types/VolumeDrawable.d.ts +1 -1
  20. package/es/types/VolumeRenderImpl.d.ts +1 -1
  21. package/es/types/index.d.ts +1 -1
  22. package/es/types/loaders/zarr_utils/types.d.ts +17 -12
  23. package/es/types/loaders/zarr_utils/validation.d.ts +14 -2
  24. package/es/types/loaders/zarr_utils/wrapArray.d.ts +7 -0
  25. package/es/types/workers/VolumeLoaderContext.d.ts +9 -13
  26. package/es/types/workers/types.d.ts +25 -16
  27. package/es/workers/VolumeLoadWorker.js +54 -32
  28. package/es/workers/VolumeLoaderContext.js +52 -51
  29. package/es/workers/types.js +17 -7
  30. package/package.json +14 -14
  31. package/es/loaders/zarr_utils/WrappedStore.js +0 -51
  32. package/es/test/ChunkPrefetchIterator.test.js +0 -208
  33. package/es/test/RequestQueue.test.js +0 -442
  34. package/es/test/SubscribableRequestQueue.test.js +0 -244
  35. package/es/test/VolumeCache.test.js +0 -118
  36. package/es/test/VolumeRenderSettings.test.js +0 -71
  37. package/es/test/lut.test.js +0 -671
  38. package/es/test/num_utils.test.js +0 -140
  39. package/es/test/volume.test.js +0 -98
  40. package/es/test/zarr_utils.test.js +0 -358
  41. package/es/types/loaders/zarr_utils/WrappedStore.d.ts +0 -24
  42. package/es/types/test/ChunkPrefetchIterator.test.d.ts +0 -1
  43. package/es/types/test/RequestQueue.test.d.ts +0 -1
  44. package/es/types/test/SubscribableRequestQueue.test.d.ts +0 -1
  45. package/es/types/test/VolumeCache.test.d.ts +0 -1
  46. package/es/types/test/VolumeRenderSettings.test.d.ts +0 -1
  47. package/es/types/test/lut.test.d.ts +0 -1
  48. package/es/types/test/num_utils.test.d.ts +0 -1
  49. package/es/types/test/volume.test.d.ts +0 -1
  50. package/es/types/test/zarr_utils.test.d.ts +0 -1
@@ -1,5 +1,5 @@
1
- import * as zarr from "@zarrita/core";
2
- import type WrappedStore from "./WrappedStore.js";
1
+ import * as zarr from "zarrita";
2
+ import { AsyncReadable } from "@zarrita/storage";
3
3
  import type SubscribableRequestQueue from "../../utils/SubscribableRequestQueue.js";
4
4
  export type TCZYX<T> = [T, T, T, T, T];
5
5
  export type SubscriberId = ReturnType<SubscribableRequestQueue["addSubscriber"]>;
@@ -53,16 +53,16 @@ export type OMEMultiscale = {
53
53
  };
54
54
  /** https://ngff.openmicroscopy.org/latest/#omero-md */
55
55
  export type OmeroTransitionalMetadata = {
56
- id: number;
57
- name: string;
58
- version: string;
56
+ id?: number;
57
+ name?: string;
58
+ version?: string;
59
59
  channels: {
60
- active: boolean;
61
- coefficient: number;
60
+ active?: boolean;
61
+ coefficient?: number;
62
62
  color: string;
63
- family: string;
64
- inverted: boolean;
65
- label: string;
63
+ family?: string;
64
+ inverted?: boolean;
65
+ label?: string;
66
66
  window: {
67
67
  end: number;
68
68
  max: number;
@@ -73,9 +73,14 @@ export type OmeroTransitionalMetadata = {
73
73
  };
74
74
  export type OMEZarrMetadata = {
75
75
  multiscales: OMEMultiscale[];
76
- omero: OmeroTransitionalMetadata;
76
+ omero?: OmeroTransitionalMetadata;
77
+ };
78
+ export type WrappedArrayOpts = {
79
+ subscriber?: SubscriberId;
80
+ reportChunk?: (coords: number[], subscriber: SubscriberId) => void;
81
+ isPrefetch?: boolean;
77
82
  };
78
- export type NumericZarrArray = zarr.Array<zarr.NumberDataType, WrappedStore<RequestInit>>;
83
+ export type NumericZarrArray = zarr.Array<zarr.NumberDataType, AsyncReadable<RequestInit & WrappedArrayOpts>>;
79
84
  /** A record with everything we need to access and use a single remote source of multiscale OME-Zarr data. */
80
85
  export type ZarrSource = {
81
86
  /** Representations of each scale level in this zarr. We pick one and pass it to zarrita to load data. */
@@ -1,7 +1,19 @@
1
1
  import { OMEZarrMetadata } from "./types.js";
2
2
  /**
3
- * Validates that the `OMEZarrMetadata` record `data` has the minimal amount of data required to open a volume. Since
3
+ * If `meta` is the top-level metadata of a zarr node formatted according to the OME-Zarr spec version 0.5, returns
4
+ * the object formatted according to v0.4 of the spec. For our purposes this just means flattening out the `ome` key.
5
+ *
6
+ * Return type is `unknown` because this does no actual validation; use `validateOMEZarrMetadata` for that.
7
+ */
8
+ export declare const toOMEZarrMetaV4: (meta: unknown) => unknown;
9
+ /** Intermediate stage of validation, before we've picked a single multiscale to validate */
10
+ export type MultiscaleRecord = {
11
+ multiscales: unknown[];
12
+ };
13
+ export declare function assertMetadataHasMultiscales(meta: unknown, name?: string): asserts meta is MultiscaleRecord;
14
+ /**
15
+ * Validates that the `OMEZarrMetadata` record `meta` has the minimal amount of data required to open a volume. Since
4
16
  * we only ever open one multiscale, we only validate the multiscale metadata record at index `multiscaleIdx` here.
5
17
  * `name` is used in error messages to identify the source of the metadata.
6
18
  */
7
- export declare function validateOMEZarrMetadata(data: unknown, multiscaleIdx?: number, name?: string): asserts data is OMEZarrMetadata;
19
+ export declare function validateOMEZarrMetadata(meta: MultiscaleRecord, multiscaleIdx?: number, name?: string): asserts meta is OMEZarrMetadata;
@@ -0,0 +1,7 @@
1
+ import type { Array as ZarrArray, AsyncReadable, DataType } from "zarrita";
2
+ import VolumeCache from "../../VolumeCache.js";
3
+ import type { WrappedArrayOpts } from "./types.js";
4
+ import SubscribableRequestQueue from "../../utils/SubscribableRequestQueue.js";
5
+ type AsyncReadableExt<Opts> = AsyncReadable<Opts & WrappedArrayOpts>;
6
+ export default function wrapArray<T extends DataType, Opts = unknown, Store extends AsyncReadable<Opts> = AsyncReadable<Opts>>(array: ZarrArray<T, Store>, basePath: string, cache?: VolumeCache, queue?: SubscribableRequestQueue): ZarrArray<T, AsyncReadableExt<Opts>>;
7
+ export {};
@@ -4,9 +4,8 @@ import { CreateLoaderOptions, PrefetchDirection } from "../loaders/index.js";
4
4
  import { ThreadableVolumeLoader, LoadSpec, RawChannelDataCallback, LoadedVolumeInfo } from "../loaders/IVolumeLoader.js";
5
5
  import { RawArrayLoader } from "../loaders/RawArrayLoader.js";
6
6
  import { TiffLoader } from "../loaders/TiffLoader.js";
7
- import type { WorkerRequestPayload, WorkerResponsePayload, ChannelLoadEvent, MetadataUpdateEvent } from "./types.js";
7
+ import type { ChannelLoadEvent, MetadataUpdateEvent, WorkerMsgTypeGlobal, WorkerMsgTypeWithLoader, WorkerRequestPayload, WorkerResponsePayload } from "./types.js";
8
8
  import type { ZarrLoaderFetchOptions } from "../loaders/OmeZarrLoader.js";
9
- import { WorkerMsgType } from "./types.js";
10
9
  /**
11
10
  * A handle that holds the worker and manages requests and messages to/from it.
12
11
  *
@@ -22,9 +21,7 @@ declare class SharedLoadWorkerHandle {
22
21
  private worker;
23
22
  private pendingRequests;
24
23
  private workerOpen;
25
- private throttleChannelData;
26
- onChannelData: ((e: ChannelLoadEvent) => void) | undefined;
27
- onUpdateMetadata: ((e: MetadataUpdateEvent) => void) | undefined;
24
+ onEvent: ((e: ChannelLoadEvent | MetadataUpdateEvent) => void) | undefined;
28
25
  constructor();
29
26
  /** Given a handle for settling a promise when a response is received from the worker, store it and return its ID */
30
27
  private registerMessagePromise;
@@ -35,16 +32,16 @@ declare class SharedLoadWorkerHandle {
35
32
  * Send a message of type `T` to the worker.
36
33
  * Returns a `Promise` that resolves with the worker's response, or rejects with an error message.
37
34
  */
38
- sendMessage<T extends WorkerMsgType>(type: T, payload: WorkerRequestPayload<T>): Promise<WorkerResponsePayload<T>>;
35
+ sendMessage<T extends WorkerMsgTypeGlobal>(type: T, payload: WorkerRequestPayload<T>): Promise<WorkerResponsePayload<T>>;
36
+ sendMessage<T extends WorkerMsgTypeWithLoader>(type: T, payload: WorkerRequestPayload<T>, loaderId: number): Promise<WorkerResponsePayload<T>>;
39
37
  /** Receive a message from the worker. If it's an event, call a callback; otherwise, resolve/reject a promise. */
40
38
  private receiveMessage;
41
- setThrottleChannelData(throttle: boolean): void;
42
39
  }
43
40
  /**
44
41
  * A context in which volume loaders can be run, which allows loading to run on a WebWorker (where it won't block
45
42
  * rendering or UI updates) and loaders to share a single `VolumeCache` and `RequestQueue`.
46
43
  *
47
- * ### To use:
44
+ * # To use:
48
45
  * 1. Create a `VolumeLoaderContext` with the desired cache and queue configuration.
49
46
  * 2. Before creating a loader, await `onOpen` to ensure the worker is ready.
50
47
  * 3. Create a loader with `createLoader`. This accepts nearly the same arguments as `createVolumeLoader`, but without
@@ -56,14 +53,15 @@ declare class SharedLoadWorkerHandle {
56
53
  */
57
54
  declare class VolumeLoaderContext {
58
55
  private workerHandle;
56
+ private loaders;
59
57
  private openPromise;
60
- private activeLoader;
61
- private activeLoaderId;
58
+ private throttleChannelData;
62
59
  constructor(maxCacheSize?: number, maxActiveRequests?: number, maxLowPriorityRequests?: number);
63
60
  /** Returns a `Promise` that resolves when the worker is ready. `await` it before trying to create a loader. */
64
61
  onOpen(): Promise<void>;
65
62
  /** Close this context, its worker, and any active loaders. */
66
63
  close(): void;
64
+ private handleEvent;
67
65
  /**
68
66
  * Create a new loader within this context. This loader will share the context's `VolumeCache` and `RequestQueue`.
69
67
  *
@@ -71,7 +69,6 @@ declare class VolumeLoaderContext {
71
69
  */
72
70
  createLoader(path: string | string[], options?: Omit<CreateLoaderOptions, "cache" | "queue">): Promise<WorkerLoader | TiffLoader | RawArrayLoader>;
73
71
  setThrottleChannelData(throttle: boolean): void;
74
- getActiveLoader(): WorkerLoader | undefined;
75
72
  }
76
73
  /**
77
74
  * A handle to an instance of `IVolumeLoader` (technically, a `ThreadableVolumeLoader`) running on a WebWorker.
@@ -81,12 +78,11 @@ declare class VolumeLoaderContext {
81
78
  declare class WorkerLoader extends ThreadableVolumeLoader {
82
79
  private loaderId;
83
80
  private workerHandle;
84
- private isOpen;
85
81
  private currentLoadId;
86
82
  private currentLoadCallback;
87
83
  private currentMetadataUpdateCallback;
88
84
  constructor(loaderId: number, workerHandle: SharedLoadWorkerHandle);
89
- private checkIsOpen;
85
+ private getLoaderId;
90
86
  /** Close and permanently invalidate this loader. */
91
87
  close(): void;
92
88
  /**
@@ -9,13 +9,18 @@ import type { ZarrLoaderFetchOptions } from "../loaders/OmeZarrLoader.js";
9
9
  export declare const enum WorkerMsgType {
10
10
  INIT = 0,
11
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
12
+ CLOSE_LOADER = 2,
13
+ CREATE_VOLUME = 3,
14
+ LOAD_DIMS = 4,
15
+ LOAD_VOLUME_DATA = 5,
16
+ SET_PREFETCH_PRIORITY_DIRECTIONS = 6,
17
+ SYNCHRONIZE_MULTICHANNEL_LOADING = 7,
18
+ UPDATE_FETCH_OPTIONS = 8
18
19
  }
20
+ /** The variants of `WorkerMessageType` which represent "global" actions that don't require a specific loader */
21
+ export type WorkerMsgTypeGlobal = WorkerMsgType.INIT | WorkerMsgType.CREATE_LOADER;
22
+ /** The variants of `WorkerMessageType` which represent actions on a specific loader */
23
+ export type WorkerMsgTypeWithLoader = Exclude<WorkerMsgType, WorkerMsgTypeGlobal>;
19
24
  /** The kind of response a worker can return - `SUCCESS`, `ERROR`, or `EVENT`. */
20
25
  export declare const enum WorkerResponseResult {
21
26
  SUCCESS = 0,
@@ -29,9 +34,13 @@ export declare const enum WorkerEventType {
29
34
  /** Fired when data for a channel (or batch of channels) is loaded */
30
35
  CHANNEL_LOAD = 1
31
36
  }
32
- /** All messages to/from a worker carry a `msgId`, a `type`, and a `payload` (whose type is determined by `type`). */
37
+ /**
38
+ * All messages to/from a worker carry a `msgId`, a `type`, and a `payload` (whose type is determined by `type`).
39
+ * Messages which operate on a specific loader also require a `loaderId`.
40
+ */
33
41
  type WorkerMsgBase<T extends WorkerMsgType, P> = {
34
42
  msgId: number;
43
+ loaderId: T extends WorkerMsgTypeWithLoader ? number : undefined;
35
44
  type: T;
36
45
  payload: P;
37
46
  };
@@ -46,12 +55,12 @@ export type WorkerRequestPayload<T extends WorkerMsgType> = {
46
55
  path: string | string[];
47
56
  options?: CreateLoaderOptions;
48
57
  };
58
+ [WorkerMsgType.CLOSE_LOADER]: void;
49
59
  [WorkerMsgType.CREATE_VOLUME]: LoadSpec;
50
60
  [WorkerMsgType.LOAD_DIMS]: LoadSpec;
51
61
  [WorkerMsgType.LOAD_VOLUME_DATA]: {
52
62
  imageInfo: ImageInfo;
53
63
  loadSpec: LoadSpec;
54
- loaderId: number;
55
64
  loadId: number;
56
65
  };
57
66
  [WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS]: PrefetchDirection[];
@@ -61,7 +70,8 @@ export type WorkerRequestPayload<T extends WorkerMsgType> = {
61
70
  /** Maps each `WorkerMsgType` to the type of the payload of responses of that type. */
62
71
  export type WorkerResponsePayload<T extends WorkerMsgType> = {
63
72
  [WorkerMsgType.INIT]: void;
64
- [WorkerMsgType.CREATE_LOADER]: boolean;
73
+ [WorkerMsgType.CREATE_LOADER]: number | undefined;
74
+ [WorkerMsgType.CLOSE_LOADER]: void;
65
75
  [WorkerMsgType.CREATE_VOLUME]: LoadedVolumeInfo;
66
76
  [WorkerMsgType.LOAD_DIMS]: VolumeDims[];
67
77
  [WorkerMsgType.LOAD_VOLUME_DATA]: void;
@@ -69,11 +79,13 @@ export type WorkerResponsePayload<T extends WorkerMsgType> = {
69
79
  [WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING]: void;
70
80
  [WorkerMsgType.UPDATE_FETCH_OPTIONS]: void;
71
81
  }[T];
72
- /** Event for when a batch of channel data loads. */
73
- export type ChannelLoadEvent = {
74
- eventType: WorkerEventType.CHANNEL_LOAD;
82
+ type WorkerEventBase<T extends WorkerEventType> = {
83
+ eventType: T;
75
84
  loaderId: number;
76
85
  loadId: number;
86
+ };
87
+ /** Event for when a batch of channel data loads. */
88
+ export type ChannelLoadEvent = WorkerEventBase<WorkerEventType.CHANNEL_LOAD> & {
77
89
  channelIndex: number[];
78
90
  dtype: NumberType[];
79
91
  data: TypedArray<NumberType>[];
@@ -81,10 +93,7 @@ export type ChannelLoadEvent = {
81
93
  atlasDims?: [number, number];
82
94
  };
83
95
  /** Event for when metadata updates. */
84
- export type MetadataUpdateEvent = {
85
- eventType: WorkerEventType.METADATA_UPDATE;
86
- loaderId: number;
87
- loadId: number;
96
+ export type MetadataUpdateEvent = WorkerEventBase<WorkerEventType.METADATA_UPDATE> & {
88
97
  imageInfo?: ImageInfo;
89
98
  loadSpec?: LoadSpec;
90
99
  };
@@ -9,9 +9,16 @@ import { rebuildLoadSpec } from "./util.js";
9
9
  let cache = undefined;
10
10
  let queue = undefined;
11
11
  let subscribableQueue = undefined;
12
- let loader = undefined;
12
+ let loaderCount = 0;
13
+ const loaders = new Map();
14
+ const getLoader = loaderId => {
15
+ const loader = loaders.get(loaderId);
16
+ if (loader === undefined) {
17
+ throw new VolumeLoadError(`Loader with ID ${loaderId} does not exist`);
18
+ }
19
+ return loader;
20
+ };
13
21
  let initialized = false;
14
- let copyOnLoad = false;
15
22
  const messageHandlers = {
16
23
  [WorkerMsgType.INIT]: ({
17
24
  maxCacheSize,
@@ -30,37 +37,50 @@ const messageHandlers = {
30
37
  path,
31
38
  options
32
39
  }) => {
33
- const pathString = Array.isArray(path) ? path[0] : path;
34
- const fileType = options?.fileType || pathToFileType(pathString);
35
- copyOnLoad = fileType === VolumeFileFormat.JSON;
36
- loader = await createVolumeLoader(path, {
40
+ const loader = await createVolumeLoader(path, {
37
41
  ...options,
38
42
  cache,
39
43
  queue: subscribableQueue
40
44
  });
41
- return loader !== undefined;
42
- },
43
- [WorkerMsgType.CREATE_VOLUME]: async loadSpec => {
44
45
  if (loader === undefined) {
45
- throw new VolumeLoadError("No loader created");
46
+ return undefined;
46
47
  }
48
+ const pathString = Array.isArray(path) ? path[0] : path;
49
+ const fileType = options?.fileType || pathToFileType(pathString);
50
+ const copyOnLoad = fileType === VolumeFileFormat.JSON;
51
+ const loaderId = loaderCount;
52
+ loaderCount += 1;
53
+ loaders.set(loaderId, {
54
+ loader,
55
+ copyOnLoad
56
+ });
57
+ return loaderId;
58
+ },
59
+ [WorkerMsgType.CLOSE_LOADER]: (_, loaderId) => {
60
+ loaders.delete(loaderId);
61
+ return Promise.resolve();
62
+ },
63
+ [WorkerMsgType.CREATE_VOLUME]: async (loadSpec, loaderId) => {
64
+ const {
65
+ loader
66
+ } = getLoader(loaderId);
47
67
  return await loader.createImageInfo(rebuildLoadSpec(loadSpec));
48
68
  },
49
- [WorkerMsgType.LOAD_DIMS]: async loadSpec => {
50
- if (loader === undefined) {
51
- throw new VolumeLoadError("No loader created");
52
- }
69
+ [WorkerMsgType.LOAD_DIMS]: async (loadSpec, loaderId) => {
70
+ const {
71
+ loader
72
+ } = getLoader(loaderId);
53
73
  return await loader.loadDims(rebuildLoadSpec(loadSpec));
54
74
  },
55
75
  [WorkerMsgType.LOAD_VOLUME_DATA]: ({
56
76
  imageInfo,
57
77
  loadSpec,
58
- loaderId,
59
78
  loadId
60
- }) => {
61
- if (loader === undefined) {
62
- throw new VolumeLoadError("No loader created");
63
- }
79
+ }, loaderId) => {
80
+ const {
81
+ loader,
82
+ copyOnLoad
83
+ } = getLoader(loaderId);
64
84
  return loader.loadRawChannelData(imageInfo, rebuildLoadSpec(loadSpec), (imageInfo, loadSpec) => {
65
85
  const message = {
66
86
  responseResult: WorkerResponseResult.EVENT,
@@ -86,16 +106,25 @@ const messageHandlers = {
86
106
  self.postMessage(message, copyOnLoad ? [] : data.map(d => d.buffer));
87
107
  });
88
108
  },
89
- [WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS]: directions => {
109
+ [WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS]: (directions, loaderId) => {
110
+ const {
111
+ loader
112
+ } = getLoader(loaderId);
90
113
  // Silently does nothing if the loader isn't an `OMEZarrLoader`
91
114
  loader?.setPrefetchPriority(directions);
92
115
  return Promise.resolve();
93
116
  },
94
- [WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING]: syncChannels => {
117
+ [WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING]: (syncChannels, loaderId) => {
118
+ const {
119
+ loader
120
+ } = getLoader(loaderId);
95
121
  loader?.syncMultichannelLoading(syncChannels);
96
122
  return Promise.resolve();
97
123
  },
98
- [WorkerMsgType.UPDATE_FETCH_OPTIONS]: fetchOptions => {
124
+ [WorkerMsgType.UPDATE_FETCH_OPTIONS]: (fetchOptions, loaderId) => {
125
+ const {
126
+ loader
127
+ } = getLoader(loaderId);
99
128
  loader?.updateFetchOptions(fetchOptions);
100
129
  return Promise.resolve();
101
130
  }
@@ -103,25 +132,18 @@ const messageHandlers = {
103
132
  self.onmessage = async ({
104
133
  data
105
134
  }) => {
106
- const {
107
- msgId,
108
- type,
109
- payload
110
- } = data;
111
135
  let message;
112
136
  try {
113
- const response = await messageHandlers[type](payload);
137
+ const response = await messageHandlers[data.type](data.payload, data.loaderId);
114
138
  message = {
139
+ ...data,
115
140
  responseResult: WorkerResponseResult.SUCCESS,
116
- msgId,
117
- type,
118
141
  payload: response
119
142
  };
120
143
  } catch (e) {
121
144
  message = {
145
+ ...data,
122
146
  responseResult: WorkerResponseResult.ERROR,
123
- msgId,
124
- type,
125
147
  payload: serializeError(e)
126
148
  };
127
149
  }
@@ -21,11 +21,11 @@ const throttle = throttledQueue(1, 16);
21
21
  class SharedLoadWorkerHandle {
22
22
  pendingRequests = [];
23
23
  workerOpen = true;
24
- throttleChannelData = false;
25
- onChannelData = undefined;
26
- onUpdateMetadata = undefined;
24
+ onEvent = undefined;
27
25
  constructor() {
28
- this.worker = new Worker(new URL("./VolumeLoadWorker", import.meta.url));
26
+ this.worker = new Worker(new URL("./VolumeLoadWorker", import.meta.url), {
27
+ type: "module"
28
+ });
29
29
  this.worker.onmessage = this.receiveMessage.bind(this);
30
30
  }
31
31
 
@@ -53,7 +53,11 @@ class SharedLoadWorkerHandle {
53
53
  * Send a message of type `T` to the worker.
54
54
  * Returns a `Promise` that resolves with the worker's response, or rejects with an error message.
55
55
  */
56
- sendMessage(type, payload) {
56
+ // overload 1: message is a global action and does not require a loader ID
57
+
58
+ // overload 2: message is a loader-specific action and requires a loader ID
59
+
60
+ sendMessage(type, payload, loaderId) {
57
61
  let msgId = -1;
58
62
  const promise = new Promise((resolve, reject) => {
59
63
  msgId = this.registerMessagePromise({
@@ -65,7 +69,8 @@ class SharedLoadWorkerHandle {
65
69
  const msg = {
66
70
  msgId,
67
71
  type,
68
- payload
72
+ payload,
73
+ loaderId
69
74
  };
70
75
  this.worker.postMessage(msg);
71
76
  return promise;
@@ -76,17 +81,7 @@ class SharedLoadWorkerHandle {
76
81
  data
77
82
  }) {
78
83
  if (data.responseResult === WorkerResponseResult.EVENT) {
79
- if (data.eventType === WorkerEventType.CHANNEL_LOAD) {
80
- if (this.onChannelData) {
81
- if (this.throttleChannelData) {
82
- throttle(() => this.onChannelData ? this.onChannelData(data) : {});
83
- } else {
84
- this.onChannelData ? this.onChannelData(data) : {};
85
- }
86
- }
87
- } else if (data.eventType === WorkerEventType.METADATA_UPDATE) {
88
- this.onUpdateMetadata?.(data);
89
- }
84
+ this.onEvent?.(data);
90
85
  } else {
91
86
  const prom = this.pendingRequests[data.msgId];
92
87
  if (prom === undefined) {
@@ -103,16 +98,13 @@ class SharedLoadWorkerHandle {
103
98
  this.pendingRequests[data.msgId] = undefined;
104
99
  }
105
100
  }
106
- setThrottleChannelData(throttle) {
107
- this.throttleChannelData = throttle;
108
- }
109
101
  }
110
102
 
111
103
  /**
112
104
  * A context in which volume loaders can be run, which allows loading to run on a WebWorker (where it won't block
113
105
  * rendering or UI updates) and loaders to share a single `VolumeCache` and `RequestQueue`.
114
106
  *
115
- * ### To use:
107
+ * # To use:
116
108
  * 1. Create a `VolumeLoaderContext` with the desired cache and queue configuration.
117
109
  * 2. Before creating a loader, await `onOpen` to ensure the worker is ready.
118
110
  * 3. Create a loader with `createLoader`. This accepts nearly the same arguments as `createVolumeLoader`, but without
@@ -123,10 +115,11 @@ class SharedLoadWorkerHandle {
123
115
  * running on the worker.
124
116
  */
125
117
  class VolumeLoaderContext {
126
- activeLoader = undefined;
127
- activeLoaderId = -1;
118
+ throttleChannelData = false;
128
119
  constructor(maxCacheSize, maxActiveRequests, maxLowPriorityRequests) {
129
120
  this.workerHandle = new SharedLoadWorkerHandle();
121
+ this.workerHandle.onEvent = this.handleEvent.bind(this);
122
+ this.loaders = new Map();
130
123
  this.openPromise = this.workerHandle.sendMessage(WorkerMsgType.INIT, {
131
124
  maxCacheSize,
132
125
  maxActiveRequests,
@@ -145,7 +138,20 @@ class VolumeLoaderContext {
145
138
  /** Close this context, its worker, and any active loaders. */
146
139
  close() {
147
140
  this.workerHandle.close();
148
- this.activeLoader?.close();
141
+ }
142
+ handleEvent(e) {
143
+ const loader = this.loaders.get(e.loaderId);
144
+ if (loader) {
145
+ if (e.eventType === WorkerEventType.CHANNEL_LOAD) {
146
+ if (this.throttleChannelData) {
147
+ throttle(() => loader.onChannelData(e));
148
+ } else {
149
+ loader.onChannelData(e);
150
+ }
151
+ } else if (e.eventType === WorkerEventType.METADATA_UPDATE) {
152
+ loader.onUpdateMetadata(e);
153
+ }
154
+ }
149
155
  }
150
156
 
151
157
  /**
@@ -165,23 +171,19 @@ class VolumeLoaderContext {
165
171
  }
166
172
  return new RawArrayLoader(options.rawArrayOptions.data, options.rawArrayOptions.metadata);
167
173
  }
168
- const success = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_LOADER, {
174
+ const loaderId = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_LOADER, {
169
175
  path,
170
176
  options
171
177
  });
172
- if (!success) {
178
+ if (loaderId === undefined) {
173
179
  throw new Error("Failed to create loader");
174
180
  }
175
- this.activeLoader?.close();
176
- this.activeLoaderId += 1;
177
- this.activeLoader = new WorkerLoader(this.activeLoaderId, this.workerHandle);
178
- return this.activeLoader;
181
+ const loader = new WorkerLoader(loaderId, this.workerHandle);
182
+ this.loaders.set(loaderId, loader);
183
+ return loader;
179
184
  }
180
185
  setThrottleChannelData(throttle) {
181
- this.workerHandle.setThrottleChannelData(throttle);
182
- }
183
- getActiveLoader() {
184
- return this.activeLoader;
186
+ this.throttleChannelData = throttle;
185
187
  }
186
188
  }
187
189
 
@@ -191,7 +193,6 @@ class VolumeLoaderContext {
191
193
  * Created with `VolumeLoaderContext.createLoader`. See its documentation for more.
192
194
  */
193
195
  class WorkerLoader extends ThreadableVolumeLoader {
194
- isOpen = true;
195
196
  currentLoadId = -1;
196
197
  currentLoadCallback = undefined;
197
198
  currentMetadataUpdateCallback = undefined;
@@ -199,18 +200,21 @@ class WorkerLoader extends ThreadableVolumeLoader {
199
200
  super();
200
201
  this.loaderId = loaderId;
201
202
  this.workerHandle = workerHandle;
202
- workerHandle.onChannelData = this.onChannelData.bind(this);
203
- workerHandle.onUpdateMetadata = this.onUpdateMetadata.bind(this);
204
203
  }
205
- checkIsOpen() {
206
- if (!this.isOpen || !this.workerHandle.isOpen) {
204
+ getLoaderId() {
205
+ if (this.loaderId === undefined || !this.workerHandle.isOpen) {
207
206
  throw new Error("Tried to use a closed loader");
208
207
  }
208
+ return this.loaderId;
209
209
  }
210
210
 
211
211
  /** Close and permanently invalidate this loader. */
212
212
  close() {
213
- this.isOpen = false;
213
+ if (this.loaderId === undefined) {
214
+ return;
215
+ }
216
+ this.workerHandle.sendMessage(WorkerMsgType.CLOSE_LOADER, undefined, this.loaderId);
217
+ this.loaderId = undefined;
214
218
  }
215
219
 
216
220
  /**
@@ -218,40 +222,37 @@ class WorkerLoader extends ThreadableVolumeLoader {
218
222
  * any chunks are prefetched in any other directions. Has no effect if this loader doesn't support prefetching.
219
223
  */
220
224
  setPrefetchPriority(directions) {
221
- return this.workerHandle.sendMessage(WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS, directions);
225
+ return this.workerHandle.sendMessage(WorkerMsgType.SET_PREFETCH_PRIORITY_DIRECTIONS, directions, this.getLoaderId());
222
226
  }
223
227
  updateFetchOptions(fetchOptions) {
224
- return this.workerHandle.sendMessage(WorkerMsgType.UPDATE_FETCH_OPTIONS, fetchOptions);
228
+ return this.workerHandle.sendMessage(WorkerMsgType.UPDATE_FETCH_OPTIONS, fetchOptions, this.getLoaderId());
225
229
  }
226
230
  syncMultichannelLoading(sync) {
227
- return this.workerHandle.sendMessage(WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING, sync);
231
+ return this.workerHandle.sendMessage(WorkerMsgType.SYNCHRONIZE_MULTICHANNEL_LOADING, sync, this.getLoaderId());
228
232
  }
229
233
  loadDims(loadSpec) {
230
- this.checkIsOpen();
231
- return this.workerHandle.sendMessage(WorkerMsgType.LOAD_DIMS, loadSpec);
234
+ return this.workerHandle.sendMessage(WorkerMsgType.LOAD_DIMS, loadSpec, this.getLoaderId());
232
235
  }
233
236
  async createImageInfo(loadSpec) {
234
- this.checkIsOpen();
235
237
  const {
236
238
  imageInfo,
237
239
  loadSpec: adjustedLoadSpec
238
- } = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_VOLUME, loadSpec);
240
+ } = await this.workerHandle.sendMessage(WorkerMsgType.CREATE_VOLUME, loadSpec, this.getLoaderId());
239
241
  return {
240
242
  imageInfo,
241
243
  loadSpec: rebuildLoadSpec(adjustedLoadSpec)
242
244
  };
243
245
  }
244
246
  loadRawChannelData(imageInfo, loadSpec, onUpdateMetadata, onData) {
245
- this.checkIsOpen();
246
247
  this.currentLoadCallback = onData;
247
248
  this.currentMetadataUpdateCallback = onUpdateMetadata;
248
249
  this.currentLoadId += 1;
249
- return this.workerHandle.sendMessage(WorkerMsgType.LOAD_VOLUME_DATA, {
250
+ const message = {
250
251
  imageInfo,
251
252
  loadSpec,
252
- loaderId: this.loaderId,
253
253
  loadId: this.currentLoadId
254
- });
254
+ };
255
+ return this.workerHandle.sendMessage(WorkerMsgType.LOAD_VOLUME_DATA, message, this.getLoaderId());
255
256
  }
256
257
  onChannelData(e) {
257
258
  if (e.loaderId !== this.loaderId || e.loadId !== this.currentLoadId) {
@@ -2,15 +2,20 @@
2
2
  export let WorkerMsgType = /*#__PURE__*/function (WorkerMsgType) {
3
3
  WorkerMsgType[WorkerMsgType["INIT"] = 0] = "INIT";
4
4
  WorkerMsgType[WorkerMsgType["CREATE_LOADER"] = 1] = "CREATE_LOADER";
5
- WorkerMsgType[WorkerMsgType["CREATE_VOLUME"] = 2] = "CREATE_VOLUME";
6
- WorkerMsgType[WorkerMsgType["LOAD_DIMS"] = 3] = "LOAD_DIMS";
7
- WorkerMsgType[WorkerMsgType["LOAD_VOLUME_DATA"] = 4] = "LOAD_VOLUME_DATA";
8
- WorkerMsgType[WorkerMsgType["SET_PREFETCH_PRIORITY_DIRECTIONS"] = 5] = "SET_PREFETCH_PRIORITY_DIRECTIONS";
9
- WorkerMsgType[WorkerMsgType["SYNCHRONIZE_MULTICHANNEL_LOADING"] = 6] = "SYNCHRONIZE_MULTICHANNEL_LOADING";
10
- WorkerMsgType[WorkerMsgType["UPDATE_FETCH_OPTIONS"] = 7] = "UPDATE_FETCH_OPTIONS";
5
+ WorkerMsgType[WorkerMsgType["CLOSE_LOADER"] = 2] = "CLOSE_LOADER";
6
+ WorkerMsgType[WorkerMsgType["CREATE_VOLUME"] = 3] = "CREATE_VOLUME";
7
+ WorkerMsgType[WorkerMsgType["LOAD_DIMS"] = 4] = "LOAD_DIMS";
8
+ WorkerMsgType[WorkerMsgType["LOAD_VOLUME_DATA"] = 5] = "LOAD_VOLUME_DATA";
9
+ WorkerMsgType[WorkerMsgType["SET_PREFETCH_PRIORITY_DIRECTIONS"] = 6] = "SET_PREFETCH_PRIORITY_DIRECTIONS";
10
+ WorkerMsgType[WorkerMsgType["SYNCHRONIZE_MULTICHANNEL_LOADING"] = 7] = "SYNCHRONIZE_MULTICHANNEL_LOADING";
11
+ WorkerMsgType[WorkerMsgType["UPDATE_FETCH_OPTIONS"] = 8] = "UPDATE_FETCH_OPTIONS";
11
12
  return WorkerMsgType;
12
13
  }({});
13
14
 
15
+ /** The variants of `WorkerMessageType` which represent "global" actions that don't require a specific loader */
16
+
17
+ /** The variants of `WorkerMessageType` which represent actions on a specific loader */
18
+
14
19
  /** The kind of response a worker can return - `SUCCESS`, `ERROR`, or `EVENT`. */
15
20
  export let WorkerResponseResult = /*#__PURE__*/function (WorkerResponseResult) {
16
21
  WorkerResponseResult[WorkerResponseResult["SUCCESS"] = 0] = "SUCCESS";
@@ -21,12 +26,17 @@ export let WorkerResponseResult = /*#__PURE__*/function (WorkerResponseResult) {
21
26
 
22
27
  /** The kind of events that can occur when loading */
23
28
  export let WorkerEventType = /*#__PURE__*/function (WorkerEventType) {
29
+ /** Fired to update a `Volume`'s `imageInfo` and/or `loadSpec` based on loaded data (time, channels, region, etc.) */
24
30
  WorkerEventType[WorkerEventType["METADATA_UPDATE"] = 0] = "METADATA_UPDATE";
31
+ /** Fired when data for a channel (or batch of channels) is loaded */
25
32
  WorkerEventType[WorkerEventType["CHANNEL_LOAD"] = 1] = "CHANNEL_LOAD";
26
33
  return WorkerEventType;
27
34
  }({});
28
35
 
29
- /** All messages to/from a worker carry a `msgId`, a `type`, and a `payload` (whose type is determined by `type`). */
36
+ /**
37
+ * All messages to/from a worker carry a `msgId`, a `type`, and a `payload` (whose type is determined by `type`).
38
+ * Messages which operate on a specific loader also require a `loaderId`.
39
+ */
30
40
 
31
41
  /** Maps each `WorkerMsgType` to the type of the payload of requests of that type. */
32
42