Skip to content

Commit a1f26d0

Browse files
committed
Make the data path synchronous: sync layer lifecycle, sync ChunkManager, source eagerly holds its loader (no open())
1 parent 0a97e76 commit a1f26d0

8 files changed

Lines changed: 64 additions & 90 deletions

File tree

src/core/layer.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,11 @@ export abstract class Layer {
6161

6262
public onEvent(_: EventContext): void {}
6363

64-
// TODO: Consider making this an abstract method once chunk manager
65-
// integration is finalized. Most layers will likely need access to the chunk
66-
// manager, but for now, we allow optional overrides to avoid requiring
67-
// placeholder implementations.
68-
public async onAttached(_context: IdetikContext) {}
64+
// Synchronous attach/detach. The source must be opened before a layer is attached
65+
// (see ChunkManager.viewFor) so creating the layer's view needs no async step.
66+
public attach(_context: IdetikContext): void {}
6967

70-
public onDetached(_context: IdetikContext): void {}
68+
public detach(): void {}
7169

7270
public get objects() {
7371
return this.objects_;

src/core/viewport.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export class Viewport {
2929
public readonly events: EventDispatcher;
3030
public cameraControls?: CameraControls;
3131

32-
// Carried only to relay to `layer.onAttached` / `layer.onDetached`.
32+
// Carried only to relay to `layer.attach` / `layer.detach`.
3333
// To be removed when the chunk-infrastructure refactor folds chunk management
3434
// into the source and the attach lifecycle goes away.
3535
private readonly context_: IdetikContext;
@@ -72,7 +72,7 @@ export class Viewport {
7272

7373
public addLayer(layer: Layer): void {
7474
this.layers_.push(layer);
75-
layer.onAttached(this.context_);
75+
layer.attach(this.context_);
7676
}
7777

7878
public removeLayer(layer: Layer): void {
@@ -81,12 +81,12 @@ export class Viewport {
8181
throw new Error(`Layer to remove not found: ${layer}`);
8282
}
8383
this.layers_.splice(index, 1);
84-
layer.onDetached(this.context_);
84+
layer.detach();
8585
}
8686

8787
public removeAllLayers(): void {
8888
for (const layer of this.layers_) {
89-
layer.onDetached(this.context_);
89+
layer.detach();
9090
}
9191
this.layers_ = [];
9292
}

src/data/chunk.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,8 @@ export type SliceCoordinates = {
9999
};
100100

101101
export type ChunkSource = {
102-
open(): Promise<ChunkLoader>;
102+
/** The source's loader. Sources are opened by their factory, so this is synchronous. */
103+
getLoader(): ChunkLoader;
103104
};
104105

105106
export type ChunkLoader = {

src/data/chunk_manager.ts

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@ export type QueueStats = {
99
import { ChunkStore } from "./chunk_store";
1010
import { ChunkStoreView } from "./chunk_store_view";
1111
import { ImageSourcePolicy } from "../core/image_source_policy";
12-
import { Logger } from "../utilities/logger";
1312

1413
export class ChunkManager {
1514
private readonly stores_ = new Map<ChunkSource, ChunkStore>();
16-
private readonly pendingStores_ = new Map<ChunkSource, Promise<ChunkStore>>();
1715
private readonly queue_ = new ChunkQueue();
1816

1917
public get queueStats(): QueueStats {
@@ -27,47 +25,20 @@ export class ChunkManager {
2725
return chunkMemoryStats();
2826
}
2927

30-
public async addView(
28+
/**
29+
* Creates a view for a source. The source is opened by its factory
30+
* (`OmeZarrImageSource.fromHttp(...)` / `fromFileSystem(...)`), so this is synchronous.
31+
*/
32+
public viewFor(
3133
source: ChunkSource,
3234
policy: ImageSourcePolicy
33-
): Promise<ChunkStoreView> {
34-
const store = await this.getOrCreateStore(source);
35-
const view = store.createView(policy);
36-
return view;
37-
}
38-
39-
private async getOrCreateStore(source: ChunkSource): Promise<ChunkStore> {
40-
const existing = this.stores_.get(source);
41-
if (existing) {
42-
return existing;
43-
}
44-
45-
const pending = this.pendingStores_.get(source);
46-
if (pending) {
47-
return pending;
48-
}
49-
50-
const initializeStore = async () => {
51-
const loader = await source.open();
52-
return new ChunkStore(loader);
53-
};
54-
55-
const newPending = initializeStore();
56-
this.pendingStores_.set(source, newPending);
57-
58-
try {
59-
const store = await newPending;
35+
): ChunkStoreView {
36+
let store = this.stores_.get(source);
37+
if (!store) {
38+
store = new ChunkStore(source.getLoader());
6039
this.stores_.set(source, store);
61-
return store;
62-
} catch (error) {
63-
Logger.error(
64-
"ChunkManager",
65-
`Failed to open chunk source: ${String(error)}`
66-
);
67-
throw error;
68-
} finally {
69-
this.pendingStores_.delete(source);
7040
}
41+
return store.createView(policy);
7142
}
7243

7344
public update() {

src/data/ome_zarr/image_source.ts

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { SourceDimensionMap } from "../chunk";
1616
type OmeZarrImageSourceProps = {
1717
location: Location<Readable>;
1818
version?: OmeZarrVersion;
19+
loader: OmeZarrImageLoader;
1920
};
2021

2122
type HttpOmeZarrImageSourceProps = {
@@ -34,18 +35,28 @@ export class OmeZarrImageSource {
3435
readonly location: Location<Readable>;
3536
readonly version?: OmeZarrVersion;
3637

37-
private loader_?: OmeZarrImageLoader;
38+
private readonly loader_: OmeZarrImageLoader;
3839

40+
/**
41+
* Construct only via a static factory (`fromHttp`/`fromFileSystem`). A factory must
42+
* fully open the data up front — e.g. via {@link openLoader}, which resolves to a
43+
* ready loader or throws — and pass that loader in. The constructor therefore always
44+
* receives an opened loader, so a source instance is never in a half-open state
45+
* (`getLoader`/`getDimensions` cannot fail). To add a new factory, open the loader
46+
* first, then `new OmeZarrImageSource({ location, version, loader })`.
47+
*/
3948
private constructor(props: OmeZarrImageSourceProps) {
4049
this.location = props.location;
4150
this.version = props.version;
51+
this.loader_ = props.loader;
4252
}
4353

44-
public async open(): Promise<OmeZarrImageLoader> {
45-
if (this.loader_) return this.loader_;
46-
47-
let zarrVersion = omeZarrToZarrVersion(this.version);
48-
const root = await openGroup(this.location, zarrVersion);
54+
private static async openLoader(
55+
location: Location<Readable>,
56+
version?: OmeZarrVersion
57+
): Promise<OmeZarrImageLoader> {
58+
let zarrVersion = omeZarrToZarrVersion(version);
59+
const root = await openGroup(location, zarrVersion);
4960
const adaptedOmeImage = parseOmeZarrImage(root.attrs);
5061
const images = adaptedOmeImage.multiscales;
5162
if (images.length !== 1) {
@@ -61,7 +72,7 @@ export class OmeZarrImageSource {
6172
zarrVersion = omeZarrToZarrVersion(adaptedOmeImage.originalVersion);
6273
}
6374
const arrayParams = metadata.datasets.map((d) =>
64-
createZarrArrayParams(this.location, d.path, zarrVersion)
75+
createZarrArrayParams(location, d.path, zarrVersion)
6576
);
6677
const arrays = await Promise.all(
6778
arrayParams.map((params) => openArrayFromParams(params))
@@ -74,24 +85,22 @@ export class OmeZarrImageSource {
7485
`Mismatch between number of axes (${axes.length}) and array shape (${shape.length})`
7586
);
7687
}
77-
this.loader_ = new OmeZarrImageLoader({ metadata, arrays, arrayParams });
78-
return this.loader_;
88+
return new OmeZarrImageLoader({ metadata, arrays, arrayParams });
7989
}
8090

8191
public getDimensions(): SourceDimensionMap {
82-
if (!this.loader_) {
83-
throw new Error(
84-
"OmeZarrImageSource.getDimensions() requires the source to be opened first; " +
85-
"use `await OmeZarrImageSource.fromHttp({ url })` or `await source.open()`."
86-
);
87-
}
8892
return this.loader_.getSourceDimensionMap();
8993
}
9094

9195
public getChannelCount(): number {
9296
return this.getDimensions().c?.lods[0].size ?? 1;
9397
}
9498

99+
/** The source's loader, for synchronous view creation by the chunk manager. */
100+
public getLoader(): OmeZarrImageLoader {
101+
return this.loader_;
102+
}
103+
95104
/**
96105
* Creates and opens an OmeZarrImageSource from an HTTP(S) URL.
97106
*
@@ -101,13 +110,9 @@ export class OmeZarrImageSource {
101110
public static async fromHttp(
102111
props: HttpOmeZarrImageSourceProps
103112
): Promise<OmeZarrImageSource> {
104-
const store = new FetchStore(props.url);
105-
const source = new OmeZarrImageSource({
106-
location: new Location(store),
107-
version: props.version,
108-
});
109-
await source.open();
110-
return source;
113+
const location = new Location(new FetchStore(props.url));
114+
const loader = await OmeZarrImageSource.openLoader(location, props.version);
115+
return new OmeZarrImageSource({ location, version: props.version, loader });
111116
}
112117

113118
/**
@@ -122,12 +127,11 @@ export class OmeZarrImageSource {
122127
public static async fromFileSystem(
123128
props: FileSystemOmeZarrImageSourceProps
124129
): Promise<OmeZarrImageSource> {
125-
const store = new WebFileSystemStore(props.directory);
126-
const source = new OmeZarrImageSource({
127-
location: new Location(store, props.path),
128-
version: props.version,
129-
});
130-
await source.open();
131-
return source;
130+
const location = new Location(
131+
new WebFileSystemStore(props.directory),
132+
props.path
133+
);
134+
const loader = await OmeZarrImageSource.openLoader(location, props.version);
135+
return new OmeZarrImageSource({ location, version: props.version, loader });
132136
}
133137
}

src/layers/image_layer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,13 @@ export class ImageLayer extends Layer implements ChannelsEnabled {
7373
this.onPickValue_ = onPickValue;
7474
}
7575

76-
public async onAttached(context: IdetikContext) {
76+
public attach(context: IdetikContext): void {
7777
if (this.chunkStoreView_) {
7878
throw new Error(
79-
"ImageLayer cannot be attached to multiple contexts simultaneously."
79+
"ImageLayer cannot be attached to multiple viewports simultaneously."
8080
);
8181
}
82-
this.chunkStoreView_ = await context.chunkManager.addView(
82+
this.chunkStoreView_ = context.chunkManager.viewFor(
8383
this.source_,
8484
this.policy_
8585
);
@@ -100,7 +100,7 @@ export class ImageLayer extends Layer implements ChannelsEnabled {
100100
}
101101
}
102102

103-
public onDetached(_context: IdetikContext): void {
103+
public detach(): void {
104104
if (!this.chunkStoreView_) return;
105105
this.releaseAndRemoveChunks(this.visibleChunks_.keys());
106106
this.clearObjects();

src/layers/label_layer.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ export class LabelLayer extends Layer {
6666
this.outlineSelected_ = outlineSelected;
6767
}
6868

69-
public async onAttached(context: IdetikContext) {
69+
public attach(context: IdetikContext): void {
7070
if (this.chunkStoreView_) {
7171
throw new Error(
72-
"LabelLayer cannot be attached to multiple contexts simultaneously."
72+
"LabelLayer cannot be attached to multiple viewports simultaneously."
7373
);
7474
}
75-
this.chunkStoreView_ = await context.chunkManager.addView(
75+
this.chunkStoreView_ = context.chunkManager.viewFor(
7676
this.source_,
7777
this.policy_
7878
);
@@ -86,7 +86,7 @@ export class LabelLayer extends Layer {
8686
}
8787
}
8888

89-
public onDetached(_context: IdetikContext): void {
89+
public detach(): void {
9090
if (!this.chunkStoreView_) return;
9191
this.releaseAndRemoveChunks(this.visibleChunks_.keys());
9292
this.clearObjects();

src/layers/volume_layer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ export class VolumeLayer extends Layer implements ChannelsEnabled {
130130
return volume;
131131
}
132132

133-
public async onAttached(context: IdetikContext) {
134-
this.chunkStoreView_ = await context.chunkManager.addView(
133+
public attach(context: IdetikContext): void {
134+
this.chunkStoreView_ = context.chunkManager.viewFor(
135135
this.source_,
136136
this.sourcePolicy_
137137
);
@@ -142,7 +142,7 @@ export class VolumeLayer extends Layer implements ChannelsEnabled {
142142
);
143143
}
144144

145-
public onDetached(_context: IdetikContext): void {
145+
public detach(): void {
146146
if (!this.chunkStoreView_) return;
147147
for (const volume of this.currentVolumes_.values()) {
148148
this.releaseAndRemoveVolume(volume);

0 commit comments

Comments
 (0)