minerender 2.1.0-alpha.142 → 2.1.0-alpha.143

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.
@@ -1,4 +1,5 @@
1
1
  // src/world/WorldStreamer.ts
2
+ var MAX_PREFETCHED_COLUMNS = 4;
2
3
  var WorldStreamer = class {
3
4
  constructor(world, source, options = {}) {
4
5
  this.world = world;
@@ -8,6 +9,7 @@ var WorldStreamer = class {
8
9
  this.missing = /* @__PURE__ */ new Map();
9
10
  this.failed = /* @__PURE__ */ new Map();
10
11
  this.desired = [];
12
+ this.reads = /* @__PURE__ */ new Map();
11
13
  this.disposed = false;
12
14
  this.loadRadius = options.loadRadius ?? 2;
13
15
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -52,6 +54,12 @@ var WorldStreamer = class {
52
54
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
53
55
  this.loading.controller.abort();
54
56
  }
57
+ for (const [key, read] of this.reads) {
58
+ if (!this.within(read.position, this.loadRadius)) {
59
+ read.controller.abort();
60
+ if (read.ready) this.reads.delete(key);
61
+ }
62
+ }
55
63
  }
56
64
  return this.running ?? (this.running = Promise.resolve().then(() => this.drain()));
57
65
  }
@@ -72,8 +80,11 @@ var WorldStreamer = class {
72
80
  this.desired = [];
73
81
  this.center = void 0;
74
82
  this.loading?.controller.abort();
83
+ for (const read of this.reads.values()) read.controller.abort();
75
84
  return this.disposal ?? (this.disposal = Promise.resolve().then(async () => {
76
85
  await this.running?.catch(() => void 0);
86
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
87
+ this.reads.clear();
77
88
  try {
78
89
  await this.unloadMany([...this.owned.values()]);
79
90
  } finally {
@@ -99,6 +110,10 @@ var WorldStreamer = class {
99
110
  await this.unloadMany(obsolete);
100
111
  continue;
101
112
  }
113
+ if (this.world.placeChunks) {
114
+ if (await this.placeReadyChunks()) continue;
115
+ return;
116
+ }
102
117
  const position = this.desired.find((position2) => this.needsLoad(position2));
103
118
  if (!position) return;
104
119
  let chunk;
@@ -121,26 +136,105 @@ var WorldStreamer = class {
121
136
  continue;
122
137
  }
123
138
  this.owned.set(this.key(position), position);
139
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
140
+ }
141
+ } finally {
142
+ for (const [key, read] of this.reads) {
143
+ read.controller.abort();
144
+ if (read.ready) this.reads.delete(key);
145
+ }
146
+ this.running = void 0;
147
+ }
148
+ }
149
+ fillReadSlots() {
150
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
151
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
152
+ if (!position) return;
153
+ const key = this.key(position);
154
+ const read = {
155
+ position,
156
+ controller: new AbortController(),
157
+ promise: Promise.resolve(),
158
+ ready: false,
159
+ failed: false
160
+ };
161
+ this.reads.set(key, read);
162
+ read.promise = (async () => {
124
163
  try {
125
- await this.world.placeChunk(chunk);
126
- this.loaded.set(this.key(position), position);
164
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
165
+ if (read.chunk) this.validateChunk(read.chunk, position);
127
166
  } catch (error) {
128
- try {
129
- await this.unloadMany([position]);
130
- } catch (cleanupError) {
131
- const failure = error instanceof Error ? error : new Error(String(error));
132
- try {
133
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
134
- } catch {
135
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
136
- }
137
- throw failure;
167
+ read.failed = true;
168
+ read.error = error;
169
+ } finally {
170
+ read.ready = true;
171
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
172
+ this.reads.delete(key);
138
173
  }
139
- throw error;
174
+ }
175
+ })();
176
+ }
177
+ }
178
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
179
+ async placeReadyChunks() {
180
+ this.fillReadSlots();
181
+ const position = this.desired.find((position2) => this.needsLoad(position2));
182
+ if (!position) {
183
+ if (!this.reads.size) return false;
184
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
185
+ return true;
186
+ }
187
+ const head = this.reads.get(this.key(position));
188
+ if (!head) {
189
+ for (let index = this.desired.length - 1; index >= 0; index--) {
190
+ const key = this.key(this.desired[index]);
191
+ if (this.reads.get(key)?.ready) {
192
+ this.reads.delete(key);
193
+ return true;
140
194
  }
141
195
  }
142
- } finally {
143
- this.running = void 0;
196
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
197
+ return true;
198
+ }
199
+ if (!head.ready) {
200
+ await head.promise;
201
+ return true;
202
+ }
203
+ const chunks = [], positions = [];
204
+ for (const desired of this.desired) {
205
+ if (!this.needsLoad(desired)) continue;
206
+ const key = this.key(desired), read = this.reads.get(key);
207
+ if (!read?.ready) break;
208
+ this.reads.delete(key);
209
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
210
+ else if (!read.chunk) this.missing.set(key, desired);
211
+ else {
212
+ chunks.push(read.chunk);
213
+ positions.push(desired);
214
+ }
215
+ }
216
+ if (!chunks.length) return true;
217
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
218
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
219
+ return true;
220
+ }
221
+ async placeOwnedColumns(positions, place) {
222
+ try {
223
+ await place();
224
+ for (const position of positions) this.loaded.set(this.key(position), position);
225
+ } catch (error) {
226
+ try {
227
+ await this.unloadMany(positions);
228
+ } catch (cleanupError) {
229
+ const failure = error instanceof Error ? error : new Error(String(error));
230
+ try {
231
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
232
+ } catch {
233
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
234
+ }
235
+ throw failure;
236
+ }
237
+ throw error;
144
238
  }
145
239
  }
146
240
  needsLoad(position) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/world/WorldStreamer.ts"],"sourcesContent":["import type { AnvilChunk } from \"./AnvilParser\";\nimport type { MineRenderWorld } from \"./MineRenderWorld\";\nimport type { WorldChunkSource } from \"./WorldChunkSource\";\n\nexport interface WorldStreamerOptions {\n /** Loads a square of chunk columns around the center. Integer from 0 to 32; defaults to 2. */\n loadRadius?: number;\n /** Retains loaded columns within this radius. Defaults to loadRadius + 1, capped at 32. */\n unloadRadius?: number;\n}\n\ninterface ChunkPosition {\n x: number;\n z: number;\n}\n\n/**\n * Loads nearby chunk columns in distance order and unloads distant columns.\n * Updates coalesce to the latest center and serialize world mutations. Use a dedicated world:\n * the streamer replaces columns it loads, and callers must await disposal before editing that world.\n * The source remains caller-owned. Lighting, biome tint, and LOD are not added by streaming.\n */\nexport class WorldStreamer {\n private readonly loadRadius: number;\n private readonly unloadRadius: number;\n private readonly loaded = new Map<string, ChunkPosition>();\n private readonly owned = new Map<string, ChunkPosition>();\n private readonly missing = new Map<string, ChunkPosition>();\n private readonly failed = new Map<string, ChunkPosition & { error: unknown }>();\n private desired: ChunkPosition[] = [];\n private center?: ChunkPosition;\n private loading?: { position: ChunkPosition; controller: AbortController };\n private running?: Promise<void>;\n private disposal?: Promise<void>;\n private disposed = false;\n\n constructor(\n private readonly world: Pick<MineRenderWorld<boolean>, \"placeChunk\" | \"unloadChunkColumns\">,\n private readonly source: WorldChunkSource,\n options: WorldStreamerOptions = {}\n ) {\n this.loadRadius = options.loadRadius ?? 2;\n this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);\n if (![this.loadRadius, this.unloadRadius].every(value => Number.isInteger(value) && value >= 0 && value <= 32)\n || this.unloadRadius < this.loadRadius) {\n throw new RangeError(\"Chunk radii must be integers from 0 to 32, with unloadRadius at least loadRadius\");\n }\n }\n\n /** Returns absolute coordinates of successfully placed columns, including retained columns. */\n public get loadedChunks(): readonly { x: number; z: number }[] {\n return [...this.loaded.values()].map(position => ({ ...position }));\n }\n\n /** Returns source failures retained within the unload radius. Failed columns do not block other loads. */\n public get failedChunks(): readonly { x: number; z: number; error: unknown }[] {\n return [...this.failed.values()].map(failure => ({ ...failure }));\n }\n\n /** Counts desired columns still waiting for a source read or placement, including the active column. */\n public get pendingChunks(): number {\n return this.desired.filter(position => this.needsLoad(position)).length;\n }\n\n /**\n * Updates the center in absolute chunk coordinates. Repeated calls share the active drain.\n * Source failures are recorded in failedChunks; other columns continue loading. Missing and failed\n * columns are remembered within the retention radius. Call retryFailedChunks to retry source failures.\n * Placement and unload failures reject the drain; call update again to retry those operations.\n * Reads outside the latest load radius receive an abort signal; placement finishes before unloading.\n * Sources should represent an unchanged world during streaming.\n */\n public async update(x: number, z: number): Promise<void> {\n if (this.disposed) throw new Error(\"WorldStreamer is disposed\");\n if (![x, z].every(value => Number.isSafeInteger(value)\n && Number.isSafeInteger(value - this.unloadRadius) && Number.isSafeInteger(value + this.unloadRadius))) {\n throw new RangeError(\"Chunk coordinates must be safe integers within the streaming radius\");\n }\n if (!this.center || this.center.x !== x || this.center.z !== z) {\n this.center = { x, z };\n this.desired = [];\n for (let dz = -this.loadRadius; dz <= this.loadRadius; dz++) {\n for (let dx = -this.loadRadius; dx <= this.loadRadius; dx++) {\n this.desired.push({ x: x + dx, z: z + dz });\n }\n }\n this.desired.sort((a, b) => (a.x - x) ** 2 + (a.z - z) ** 2 - (b.x - x) ** 2 - (b.z - z) ** 2);\n if (this.loading && !this.within(this.loading.position, this.loadRadius)) {\n this.loading.controller.abort();\n }\n }\n return this.running ??= Promise.resolve().then(() => this.drain());\n }\n\n /** Updates from a camera or view-center position in scene units. One chunk spans 256 scene units. */\n public async updatePosition(position: { x: number; z: number }): Promise<void> {\n if (![position.x, position.z].every(Number.isFinite)) throw new RangeError(\"Scene coordinates must be finite\");\n return this.update(Math.floor(position.x / 256), Math.floor(position.z / 256));\n }\n\n /** Forgets recorded source failures and retries desired columns at the latest center. */\n public async retryFailedChunks(): Promise<void> {\n if (this.disposed) throw new Error(\"WorldStreamer is disposed\");\n this.failed.clear();\n if (this.center) await this.update(this.center.x, this.center.z);\n }\n\n /** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */\n public dispose(): Promise<void> {\n this.disposed = true;\n this.desired = [];\n this.center = undefined;\n this.loading?.controller.abort();\n return this.disposal ??= Promise.resolve().then(async () => {\n await this.running?.catch(() => undefined);\n try {\n await this.unloadMany([...this.owned.values()]);\n } finally {\n this.missing.clear();\n this.failed.clear();\n }\n }).catch(error => {\n this.disposal = undefined;\n throw error;\n });\n }\n\n private async drain(): Promise<void> {\n try {\n while (!this.disposed) {\n for (const [key, position] of this.missing) {\n if (!this.within(position, this.unloadRadius)) this.missing.delete(key);\n }\n for (const [key, position] of this.failed) {\n if (!this.within(position, this.unloadRadius)) this.failed.delete(key);\n }\n // Failed placements remain owned until cleanup succeeds, even inside the retained area.\n const obsolete = [...this.owned.values()].filter(position =>\n !this.loaded.has(this.key(position)) || !this.within(position, this.unloadRadius));\n if (obsolete.length) {\n await this.unloadMany(obsolete);\n continue;\n }\n const position = this.desired.find(position => this.needsLoad(position));\n if (!position) return;\n let chunk: AnvilChunk | undefined;\n const controller = new AbortController();\n this.loading = { position, controller };\n try {\n chunk = await this.source.getChunk(position.x, position.z, controller.signal);\n if (chunk) this.validateChunk(chunk, position);\n } catch (error) {\n if (!controller.signal.aborted && !this.disposed && this.within(position, this.loadRadius)) {\n this.failed.set(this.key(position), { ...position, error });\n }\n continue;\n } finally {\n this.loading = undefined;\n }\n if (controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) continue;\n if (!chunk) {\n this.missing.set(this.key(position), position);\n continue;\n }\n this.owned.set(this.key(position), position);\n try {\n await this.world.placeChunk(chunk);\n this.loaded.set(this.key(position), position);\n } catch (error) {\n try {\n await this.unloadMany([position]);\n } catch (cleanupError) {\n const failure = error instanceof Error ? error : new Error(String(error));\n try {\n Object.defineProperty(failure, \"cause\", { value: cleanupError, configurable: true });\n } catch {\n throw Object.assign(new Error(failure.message), { cause: cleanupError });\n }\n throw failure;\n }\n throw error;\n }\n }\n } finally {\n this.running = undefined;\n }\n }\n\n private needsLoad(position: ChunkPosition): boolean {\n const key = this.key(position);\n return !this.loaded.has(key) && !this.missing.has(key) && !this.failed.has(key);\n }\n\n private async unloadMany(positions: readonly ChunkPosition[]): Promise<void> {\n if (!positions.length) return;\n try {\n await this.world.unloadChunkColumns(positions.map(({ x, z }) => ({ x, z })));\n } catch (error) {\n // A rejected batch may have removed any subset; retry every owner before reloading it.\n for (const position of positions) this.loaded.delete(this.key(position));\n throw error;\n }\n for (const position of positions) {\n this.owned.delete(this.key(position));\n this.loaded.delete(this.key(position));\n }\n }\n\n private validateChunk(chunk: AnvilChunk, position: ChunkPosition): void {\n if (chunk.x !== position.x || chunk.z !== position.z) {\n throw new Error(`World source returned chunk ${chunk.x},${chunk.z} for ${position.x},${position.z}`);\n }\n }\n\n private within(position: ChunkPosition, radius: number): boolean {\n return !!this.center && Math.abs(position.x - this.center.x) <= radius && Math.abs(position.z - this.center.z) <= radius;\n }\n\n private key(position: ChunkPosition): string {\n return `${position.x},${position.z}`;\n }\n}\n"],"mappings":"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","names":["position"]}
1
+ {"version":3,"sources":["../../../../src/world/WorldStreamer.ts"],"sourcesContent":["import type { AnvilChunk } from \"./AnvilParser\";\nimport type { MineRenderWorld } from \"./MineRenderWorld\";\nimport type { WorldChunkSource } from \"./WorldChunkSource\";\n\nexport interface WorldStreamerOptions {\n /** Loads a square of chunk columns around the center. Integer from 0 to 32; defaults to 2. */\n loadRadius?: number;\n /** Retains loaded columns within this radius. Defaults to loadRadius + 1, capped at 32. */\n unloadRadius?: number;\n}\n\ninterface ChunkPosition {\n x: number;\n z: number;\n}\n\ninterface ChunkRead {\n position: ChunkPosition;\n controller: AbortController;\n promise: Promise<void>;\n ready: boolean;\n chunk?: AnvilChunk;\n failed: boolean;\n error?: unknown;\n}\n\nconst MAX_PREFETCHED_COLUMNS = 4;\n\n/**\n * Loads nearby chunk columns in distance order and unloads distant columns.\n * Updates coalesce to the latest center and serialize world mutations. Use a dedicated world:\n * the streamer replaces columns it loads, and callers must await disposal before editing that world.\n * The source remains caller-owned. Lighting, biome tint, and LOD are not added by streaming.\n */\nexport class WorldStreamer {\n private readonly loadRadius: number;\n private readonly unloadRadius: number;\n private readonly loaded = new Map<string, ChunkPosition>();\n private readonly owned = new Map<string, ChunkPosition>();\n private readonly missing = new Map<string, ChunkPosition>();\n private readonly failed = new Map<string, ChunkPosition & { error: unknown }>();\n private desired: ChunkPosition[] = [];\n private center?: ChunkPosition;\n private loading?: { position: ChunkPosition; controller: AbortController };\n private readonly reads = new Map<string, ChunkRead>();\n private running?: Promise<void>;\n private disposal?: Promise<void>;\n private disposed = false;\n\n constructor(\n private readonly world: Pick<MineRenderWorld<boolean>, \"placeChunk\" | \"unloadChunkColumns\">\n & Partial<Pick<MineRenderWorld<boolean>, \"placeChunks\">>,\n private readonly source: WorldChunkSource,\n options: WorldStreamerOptions = {}\n ) {\n this.loadRadius = options.loadRadius ?? 2;\n this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);\n if (![this.loadRadius, this.unloadRadius].every(value => Number.isInteger(value) && value >= 0 && value <= 32)\n || this.unloadRadius < this.loadRadius) {\n throw new RangeError(\"Chunk radii must be integers from 0 to 32, with unloadRadius at least loadRadius\");\n }\n }\n\n /** Returns absolute coordinates of successfully placed columns, including retained columns. */\n public get loadedChunks(): readonly { x: number; z: number }[] {\n return [...this.loaded.values()].map(position => ({ ...position }));\n }\n\n /** Returns source failures retained within the unload radius. Failed columns do not block other loads. */\n public get failedChunks(): readonly { x: number; z: number; error: unknown }[] {\n return [...this.failed.values()].map(failure => ({ ...failure }));\n }\n\n /** Counts desired columns still waiting for a source read or placement, including the active column. */\n public get pendingChunks(): number {\n return this.desired.filter(position => this.needsLoad(position)).length;\n }\n\n /**\n * Updates the center in absolute chunk coordinates. Repeated calls share the active drain.\n * Source failures are recorded in failedChunks; other columns continue loading. Missing and failed\n * columns are remembered within the retention radius. Call retryFailedChunks to retry source failures.\n * Placement and unload failures reject the drain; call update again to retry those operations.\n * Reads outside the latest load radius receive an abort signal; placement finishes before unloading.\n * Sources should represent an unchanged world during streaming.\n */\n public async update(x: number, z: number): Promise<void> {\n if (this.disposed) throw new Error(\"WorldStreamer is disposed\");\n if (![x, z].every(value => Number.isSafeInteger(value)\n && Number.isSafeInteger(value - this.unloadRadius) && Number.isSafeInteger(value + this.unloadRadius))) {\n throw new RangeError(\"Chunk coordinates must be safe integers within the streaming radius\");\n }\n if (!this.center || this.center.x !== x || this.center.z !== z) {\n this.center = { x, z };\n this.desired = [];\n for (let dz = -this.loadRadius; dz <= this.loadRadius; dz++) {\n for (let dx = -this.loadRadius; dx <= this.loadRadius; dx++) {\n this.desired.push({ x: x + dx, z: z + dz });\n }\n }\n this.desired.sort((a, b) => (a.x - x) ** 2 + (a.z - z) ** 2 - (b.x - x) ** 2 - (b.z - z) ** 2);\n if (this.loading && !this.within(this.loading.position, this.loadRadius)) {\n this.loading.controller.abort();\n }\n for (const [key, read] of this.reads) {\n if (!this.within(read.position, this.loadRadius)) {\n read.controller.abort();\n if (read.ready) this.reads.delete(key);\n }\n }\n }\n return this.running ??= Promise.resolve().then(() => this.drain());\n }\n\n /** Updates from a camera or view-center position in scene units. One chunk spans 256 scene units. */\n public async updatePosition(position: { x: number; z: number }): Promise<void> {\n if (![position.x, position.z].every(Number.isFinite)) throw new RangeError(\"Scene coordinates must be finite\");\n return this.update(Math.floor(position.x / 256), Math.floor(position.z / 256));\n }\n\n /** Forgets recorded source failures and retries desired columns at the latest center. */\n public async retryFailedChunks(): Promise<void> {\n if (this.disposed) throw new Error(\"WorldStreamer is disposed\");\n this.failed.clear();\n if (this.center) await this.update(this.center.x, this.center.z);\n }\n\n /** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */\n public dispose(): Promise<void> {\n this.disposed = true;\n this.desired = [];\n this.center = undefined;\n this.loading?.controller.abort();\n for (const read of this.reads.values()) read.controller.abort();\n return this.disposal ??= Promise.resolve().then(async () => {\n await this.running?.catch(() => undefined);\n await Promise.all([...this.reads.values()].map(read => read.promise));\n this.reads.clear();\n try {\n await this.unloadMany([...this.owned.values()]);\n } finally {\n this.missing.clear();\n this.failed.clear();\n }\n }).catch(error => {\n this.disposal = undefined;\n throw error;\n });\n }\n\n private async drain(): Promise<void> {\n try {\n while (!this.disposed) {\n for (const [key, position] of this.missing) {\n if (!this.within(position, this.unloadRadius)) this.missing.delete(key);\n }\n for (const [key, position] of this.failed) {\n if (!this.within(position, this.unloadRadius)) this.failed.delete(key);\n }\n // Failed placements remain owned until cleanup succeeds, even inside the retained area.\n const obsolete = [...this.owned.values()].filter(position =>\n !this.loaded.has(this.key(position)) || !this.within(position, this.unloadRadius));\n if (obsolete.length) {\n await this.unloadMany(obsolete);\n continue;\n }\n if (this.world.placeChunks) {\n if (await this.placeReadyChunks()) continue;\n return;\n }\n const position = this.desired.find(position => this.needsLoad(position));\n if (!position) return;\n let chunk: AnvilChunk | undefined;\n const controller = new AbortController();\n this.loading = { position, controller };\n try {\n chunk = await this.source.getChunk(position.x, position.z, controller.signal);\n if (chunk) this.validateChunk(chunk, position);\n } catch (error) {\n if (!controller.signal.aborted && !this.disposed && this.within(position, this.loadRadius)) {\n this.failed.set(this.key(position), { ...position, error });\n }\n continue;\n } finally {\n this.loading = undefined;\n }\n if (controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) continue;\n if (!chunk) {\n this.missing.set(this.key(position), position);\n continue;\n }\n this.owned.set(this.key(position), position);\n await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk!));\n }\n } finally {\n for (const [key, read] of this.reads) {\n read.controller.abort();\n if (read.ready) this.reads.delete(key);\n }\n this.running = undefined;\n }\n }\n\n private fillReadSlots(): void {\n while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {\n const position = this.desired.find(position => this.needsLoad(position) && !this.reads.has(this.key(position)));\n if (!position) return;\n const key = this.key(position);\n const read: ChunkRead = {\n position, controller: new AbortController(), promise: Promise.resolve(), ready: false, failed: false\n };\n this.reads.set(key, read);\n read.promise = (async () => {\n try {\n read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);\n if (read.chunk) this.validateChunk(read.chunk, position);\n } catch (error) {\n read.failed = true;\n read.error = error;\n } finally {\n read.ready = true;\n if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {\n this.reads.delete(key);\n }\n }\n })();\n }\n }\n\n /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */\n private async placeReadyChunks(): Promise<boolean> {\n this.fillReadSlots();\n const position = this.desired.find(position => this.needsLoad(position));\n if (!position) {\n if (!this.reads.size) return false;\n await Promise.all([...this.reads.values()].map(read => read.promise));\n return true;\n }\n const head = this.reads.get(this.key(position));\n if (!head) {\n for (let index = this.desired.length - 1; index >= 0; index--) {\n const key = this.key(this.desired[index]);\n if (this.reads.get(key)?.ready) {\n this.reads.delete(key);\n return true;\n }\n }\n // Aborted reads still occupy slots until their source work settles.\n await Promise.race([...this.reads.values()].map(read => read.promise));\n return true;\n }\n if (!head.ready) {\n await head.promise;\n return true;\n }\n const chunks: AnvilChunk[] = [], positions: ChunkPosition[] = [];\n for (const desired of this.desired) {\n if (!this.needsLoad(desired)) continue;\n const key = this.key(desired), read = this.reads.get(key);\n if (!read?.ready) break;\n this.reads.delete(key);\n if (read.failed) this.failed.set(key, { ...desired, error: read.error });\n else if (!read.chunk) this.missing.set(key, desired);\n else {\n chunks.push(read.chunk);\n positions.push(desired);\n }\n }\n if (!chunks.length) return true;\n for (const position of positions) this.owned.set(this.key(position), position);\n await this.placeOwnedColumns(positions, () => this.world.placeChunks!(chunks));\n return true;\n }\n\n private async placeOwnedColumns(positions: readonly ChunkPosition[], place: () => Promise<void>): Promise<void> {\n try {\n await place();\n for (const position of positions) this.loaded.set(this.key(position), position);\n } catch (error) {\n try {\n await this.unloadMany(positions);\n } catch (cleanupError) {\n const failure = error instanceof Error ? error : new Error(String(error));\n try {\n Object.defineProperty(failure, \"cause\", { value: cleanupError, configurable: true });\n } catch {\n throw Object.assign(new Error(failure.message), { cause: cleanupError });\n }\n throw failure;\n }\n throw error;\n }\n }\n\n private needsLoad(position: ChunkPosition): boolean {\n const key = this.key(position);\n return !this.loaded.has(key) && !this.missing.has(key) && !this.failed.has(key);\n }\n\n private async unloadMany(positions: readonly ChunkPosition[]): Promise<void> {\n if (!positions.length) return;\n try {\n await this.world.unloadChunkColumns(positions.map(({ x, z }) => ({ x, z })));\n } catch (error) {\n // A rejected batch may have removed any subset; retry every owner before reloading it.\n for (const position of positions) this.loaded.delete(this.key(position));\n throw error;\n }\n for (const position of positions) {\n this.owned.delete(this.key(position));\n this.loaded.delete(this.key(position));\n }\n }\n\n private validateChunk(chunk: AnvilChunk, position: ChunkPosition): void {\n if (chunk.x !== position.x || chunk.z !== position.z) {\n throw new Error(`World source returned chunk ${chunk.x},${chunk.z} for ${position.x},${position.z}`);\n }\n }\n\n private within(position: ChunkPosition, radius: number): boolean {\n return !!this.center && Math.abs(position.x - this.center.x) <= radius && Math.abs(position.z - this.center.z) <= radius;\n }\n\n private key(position: ChunkPosition): string {\n return `${position.x},${position.z}`;\n }\n}\n"],"mappings":"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","names":["position"]}
@@ -783,7 +783,10 @@ declare function toRadians(degrees: number): number;
783
783
  declare function toDegrees(radians: number): number;
784
784
  /** Resolves after a delay in milliseconds. */
785
785
  declare function sleep(timeout: number): Promise<void>;
786
- /** Yields to the next macrotask without timer clamping when the platform supports it. */
786
+ /**
787
+ * Yields to the next macrotask without timer clamping when the platform supports it.
788
+ * Not `scheduler.yield()`: Chrome runs its continuations ahead of rendering, so chained yields delay frames.
789
+ */
787
790
  declare function yieldToEventLoop(): Promise<void>;
788
791
  /** Reduces an angle modulo 360, retaining a negative sign for negative inputs. */
789
792
  declare function clampRotationDegrees(deg: number): number;
@@ -4282,6 +4285,12 @@ declare class MineRenderWorld<SectionMeshing extends boolean = false> {
4282
4285
  private placeChunkGroups;
4283
4286
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
4284
4287
  placeChunk(chunk: AnvilChunk, executor?: BatchedExecutor): Promise<void>;
4288
+ /**
4289
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
4290
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
4291
+ */
4292
+ placeChunks(chunks: readonly AnvilChunk[]): Promise<void>;
4293
+ private placeColumn;
4285
4294
  /**
4286
4295
  * Removes blocks and owned entities at chunk-column coordinates, then refreshes neighboring faces and fluids.
4287
4296
  * Coordinates match AnvilChunk.x/z, not block positions. Missing columns are ignored.
@@ -4369,10 +4378,11 @@ declare class WorldStreamer {
4369
4378
  private desired;
4370
4379
  private center?;
4371
4380
  private loading?;
4381
+ private readonly reads;
4372
4382
  private running?;
4373
4383
  private disposal?;
4374
4384
  private disposed;
4375
- constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns">, source: WorldChunkSource, options?: WorldStreamerOptions);
4385
+ constructor(world: Pick<MineRenderWorld<boolean>, "placeChunk" | "unloadChunkColumns"> & Partial<Pick<MineRenderWorld<boolean>, "placeChunks">>, source: WorldChunkSource, options?: WorldStreamerOptions);
4376
4386
  /** Returns absolute coordinates of successfully placed columns, including retained columns. */
4377
4387
  get loadedChunks(): readonly {
4378
4388
  x: number;
@@ -4405,6 +4415,10 @@ declare class WorldStreamer {
4405
4415
  /** Aborts source reads, waits for active work, then unloads owned columns. The world and source remain caller-owned. */
4406
4416
  dispose(): Promise<void>;
4407
4417
  private drain;
4418
+ private fillReadSlots;
4419
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
4420
+ private placeReadyChunks;
4421
+ private placeOwnedColumns;
4408
4422
  private needsLoad;
4409
4423
  private unloadMany;
4410
4424
  private validateChunk;
@@ -264,8 +264,6 @@ async function sleep(timeout) {
264
264
  });
265
265
  }
266
266
  function yieldToEventLoop() {
267
- const scheduler = globalThis.scheduler;
268
- if (scheduler?.yield) return scheduler.yield();
269
267
  return new Promise((resolve) => {
270
268
  if (typeof MessageChannel !== "undefined") {
271
269
  const channel = new MessageChannel();
@@ -18669,6 +18667,7 @@ function skipsBlockFace(data, rule, block, neighbor, face) {
18669
18667
  // src/world/MineRenderWorld.ts
18670
18668
  init_log();
18671
18669
  var log19 = logger("MineRenderWorld");
18670
+ var YIELD_INTERVAL_MS = 8;
18672
18671
  var MineRenderWorld = class {
18673
18672
  constructor(scene, options = {}) {
18674
18673
  this._chunks = /* @__PURE__ */ new Map();
@@ -18782,7 +18781,7 @@ var MineRenderWorld = class {
18782
18781
  failures.set(chunk, error);
18783
18782
  if (this._chunks.get(`${chunk.x}_${chunk.y}_${chunk.z}`) === chunk) this.pendingBiomes.set(chunk, void 0);
18784
18783
  }
18785
- if (performance.now() - sliceStart > 8) {
18784
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18786
18785
  await yieldToEventLoop();
18787
18786
  sliceStart = performance.now();
18788
18787
  }
@@ -18937,7 +18936,7 @@ var MineRenderWorld = class {
18937
18936
  )));
18938
18937
  for (const pos of positions) changes.set(`${pos.x},${pos.y},${pos.z}`, pos);
18939
18938
  }
18940
- if (performance.now() - sliceStart > 8) {
18939
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18941
18940
  await yieldToEventLoop();
18942
18941
  sliceStart = performance.now();
18943
18942
  }
@@ -18945,6 +18944,62 @@ var MineRenderWorld = class {
18945
18944
  }
18946
18945
  /** Replaces a chunk column's blocks, biomes, and saved light and, with `renderEntities`, its supported saved mobs. */
18947
18946
  async placeChunk(chunk, executor) {
18947
+ return this.placeColumn(chunk, executor);
18948
+ }
18949
+ /**
18950
+ * Replaces parsed columns in input order, then refreshes neighboring geometry once.
18951
+ * Duplicate coordinates retain their order. A failed column stops placement; queued refreshes finish before rejection.
18952
+ */
18953
+ async placeChunks(chunks) {
18954
+ const columns = [];
18955
+ for (const chunk of chunks) {
18956
+ const { x, z } = chunk ?? {};
18957
+ if (![x, z].every(Number.isSafeInteger)) {
18958
+ throw new RangeError("Chunk column coordinates must be safe integers");
18959
+ }
18960
+ const sections = Array.from(chunk.sections, (section) => {
18961
+ const { y } = section ?? {};
18962
+ if (!Number.isSafeInteger(y)) throw new RangeError("Chunk section coordinates must be safe integers");
18963
+ return { ...section, y };
18964
+ });
18965
+ columns.push({ ...chunk, x, z, sections });
18966
+ }
18967
+ if (!columns.length) return;
18968
+ await this.culling;
18969
+ let failed = false, failure, sliceStart = performance.now();
18970
+ try {
18971
+ for (const column of columns) {
18972
+ await this.placeColumn(column, void 0, true);
18973
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
18974
+ await yieldToEventLoop();
18975
+ sliceStart = performance.now();
18976
+ }
18977
+ }
18978
+ } catch (error) {
18979
+ failed = true;
18980
+ failure = error;
18981
+ } finally {
18982
+ try {
18983
+ const failures = await this.refreshBiomes(true);
18984
+ if (failures.size) log19.warn("Failed to refresh biome colors", failures.values().next().value);
18985
+ } catch (error) {
18986
+ if (!failed) {
18987
+ failed = true;
18988
+ failure = error;
18989
+ }
18990
+ }
18991
+ try {
18992
+ await this.updateCulling([]);
18993
+ } catch (error) {
18994
+ if (!failed) {
18995
+ failed = true;
18996
+ failure = error;
18997
+ }
18998
+ }
18999
+ }
19000
+ if (failed) throw failure;
19001
+ }
19002
+ async placeColumn(chunk, executor, deferCulling = false) {
18948
19003
  if (![chunk.x, chunk.z].every(Number.isSafeInteger)) {
18949
19004
  throw new RangeError("Chunk column coordinates must be safe integers");
18950
19005
  }
@@ -19006,7 +19061,7 @@ var MineRenderWorld = class {
19006
19061
  } finally {
19007
19062
  this.markSectionChanged(target);
19008
19063
  }
19009
- if (performance.now() - sliceStart > 8) {
19064
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19010
19065
  await yieldToEventLoop();
19011
19066
  sliceStart = performance.now();
19012
19067
  }
@@ -19029,9 +19084,12 @@ var MineRenderWorld = class {
19029
19084
  await placeEntities?.();
19030
19085
  } finally {
19031
19086
  this.markBiomesChanged(chunk.x, chunk.z, void 0, false);
19032
- const failures = await this.refreshBiomes();
19033
- if (failures.size) log19.warn("Failed to refresh biome colors", failures.values().next().value);
19034
- await this.updateCulling([...changes.values()]);
19087
+ if (deferCulling) await this.updateCulling([...changes.values()], true);
19088
+ else {
19089
+ const failures = await this.refreshBiomes();
19090
+ if (failures.size) log19.warn("Failed to refresh biome colors", failures.values().next().value);
19091
+ await this.updateCulling([...changes.values()]);
19092
+ }
19035
19093
  }
19036
19094
  }
19037
19095
  /**
@@ -19061,7 +19119,7 @@ var MineRenderWorld = class {
19061
19119
  for (const { x, z } of columns.values()) {
19062
19120
  this.entities?.clearColumn(x, z);
19063
19121
  await this.clearChunkColumn(x, z);
19064
- if (performance.now() - sliceStart > 8) {
19122
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19065
19123
  await yieldToEventLoop();
19066
19124
  sliceStart = performance.now();
19067
19125
  }
@@ -19200,15 +19258,25 @@ var MineRenderWorld = class {
19200
19258
  for (const [index, mask] of masks) await chunk.setCullMaskIndex(index, mask);
19201
19259
  await chunk.refreshLight(indices === "all" ? void 0 : indices);
19202
19260
  chunk.refreshAmbientOcclusion(this._ambientOcclusion);
19203
- if ((this._ambientOcclusion || this.ambientOcclusionPending) && performance.now() - sliceStart > 8) {
19261
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19204
19262
  await yieldToEventLoop();
19205
19263
  sliceStart = performance.now();
19206
19264
  }
19207
19265
  }
19208
- const builds = await Promise.allSettled([...batch.keys()].map(async (chunk) => {
19209
- await chunk.rebuildSectionMesh();
19210
- batch.delete(chunk);
19211
- }));
19266
+ const pending = [];
19267
+ for (const chunk of [...batch.keys()]) {
19268
+ const build = (async () => {
19269
+ await chunk.rebuildSectionMesh();
19270
+ batch.delete(chunk);
19271
+ })();
19272
+ void build.catch(() => void 0);
19273
+ pending.push(build);
19274
+ if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
19275
+ await yieldToEventLoop();
19276
+ sliceStart = performance.now();
19277
+ }
19278
+ }
19279
+ const builds = await Promise.allSettled(pending);
19212
19280
  const failure = builds.find((result) => result.status === "rejected");
19213
19281
  if (failure?.status === "rejected") throw failure.reason;
19214
19282
  } catch (error) {
@@ -20667,6 +20735,7 @@ var Chunk = class _Chunk {
20667
20735
  };
20668
20736
 
20669
20737
  // src/world/WorldStreamer.ts
20738
+ var MAX_PREFETCHED_COLUMNS = 4;
20670
20739
  var WorldStreamer = class {
20671
20740
  constructor(world, source, options = {}) {
20672
20741
  this.world = world;
@@ -20676,6 +20745,7 @@ var WorldStreamer = class {
20676
20745
  this.missing = /* @__PURE__ */ new Map();
20677
20746
  this.failed = /* @__PURE__ */ new Map();
20678
20747
  this.desired = [];
20748
+ this.reads = /* @__PURE__ */ new Map();
20679
20749
  this.disposed = false;
20680
20750
  this.loadRadius = options.loadRadius ?? 2;
20681
20751
  this.unloadRadius = options.unloadRadius ?? Math.min(32, this.loadRadius + 1);
@@ -20720,6 +20790,12 @@ var WorldStreamer = class {
20720
20790
  if (this.loading && !this.within(this.loading.position, this.loadRadius)) {
20721
20791
  this.loading.controller.abort();
20722
20792
  }
20793
+ for (const [key, read] of this.reads) {
20794
+ if (!this.within(read.position, this.loadRadius)) {
20795
+ read.controller.abort();
20796
+ if (read.ready) this.reads.delete(key);
20797
+ }
20798
+ }
20723
20799
  }
20724
20800
  return this.running ?? (this.running = Promise.resolve().then(() => this.drain()));
20725
20801
  }
@@ -20740,8 +20816,11 @@ var WorldStreamer = class {
20740
20816
  this.desired = [];
20741
20817
  this.center = void 0;
20742
20818
  this.loading?.controller.abort();
20819
+ for (const read of this.reads.values()) read.controller.abort();
20743
20820
  return this.disposal ?? (this.disposal = Promise.resolve().then(async () => {
20744
20821
  await this.running?.catch(() => void 0);
20822
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20823
+ this.reads.clear();
20745
20824
  try {
20746
20825
  await this.unloadMany([...this.owned.values()]);
20747
20826
  } finally {
@@ -20767,6 +20846,10 @@ var WorldStreamer = class {
20767
20846
  await this.unloadMany(obsolete);
20768
20847
  continue;
20769
20848
  }
20849
+ if (this.world.placeChunks) {
20850
+ if (await this.placeReadyChunks()) continue;
20851
+ return;
20852
+ }
20770
20853
  const position = this.desired.find((position2) => this.needsLoad(position2));
20771
20854
  if (!position) return;
20772
20855
  let chunk;
@@ -20789,26 +20872,105 @@ var WorldStreamer = class {
20789
20872
  continue;
20790
20873
  }
20791
20874
  this.owned.set(this.key(position), position);
20875
+ await this.placeOwnedColumns([position], () => this.world.placeChunk(chunk));
20876
+ }
20877
+ } finally {
20878
+ for (const [key, read] of this.reads) {
20879
+ read.controller.abort();
20880
+ if (read.ready) this.reads.delete(key);
20881
+ }
20882
+ this.running = void 0;
20883
+ }
20884
+ }
20885
+ fillReadSlots() {
20886
+ while (!this.disposed && this.reads.size < MAX_PREFETCHED_COLUMNS) {
20887
+ const position = this.desired.find((position2) => this.needsLoad(position2) && !this.reads.has(this.key(position2)));
20888
+ if (!position) return;
20889
+ const key = this.key(position);
20890
+ const read = {
20891
+ position,
20892
+ controller: new AbortController(),
20893
+ promise: Promise.resolve(),
20894
+ ready: false,
20895
+ failed: false
20896
+ };
20897
+ this.reads.set(key, read);
20898
+ read.promise = (async () => {
20792
20899
  try {
20793
- await this.world.placeChunk(chunk);
20794
- this.loaded.set(this.key(position), position);
20900
+ read.chunk = await this.source.getChunk(position.x, position.z, read.controller.signal);
20901
+ if (read.chunk) this.validateChunk(read.chunk, position);
20795
20902
  } catch (error) {
20796
- try {
20797
- await this.unloadMany([position]);
20798
- } catch (cleanupError) {
20799
- const failure = error instanceof Error ? error : new Error(String(error));
20800
- try {
20801
- Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20802
- } catch {
20803
- throw Object.assign(new Error(failure.message), { cause: cleanupError });
20804
- }
20805
- throw failure;
20903
+ read.failed = true;
20904
+ read.error = error;
20905
+ } finally {
20906
+ read.ready = true;
20907
+ if (read.controller.signal.aborted || this.disposed || !this.within(position, this.loadRadius)) {
20908
+ this.reads.delete(key);
20806
20909
  }
20807
- throw error;
20910
+ }
20911
+ })();
20912
+ }
20913
+ }
20914
+ /** Places only the ready prefix, so later source reads cannot delay the nearest ready column. */
20915
+ async placeReadyChunks() {
20916
+ this.fillReadSlots();
20917
+ const position = this.desired.find((position2) => this.needsLoad(position2));
20918
+ if (!position) {
20919
+ if (!this.reads.size) return false;
20920
+ await Promise.all([...this.reads.values()].map((read) => read.promise));
20921
+ return true;
20922
+ }
20923
+ const head = this.reads.get(this.key(position));
20924
+ if (!head) {
20925
+ for (let index = this.desired.length - 1; index >= 0; index--) {
20926
+ const key = this.key(this.desired[index]);
20927
+ if (this.reads.get(key)?.ready) {
20928
+ this.reads.delete(key);
20929
+ return true;
20808
20930
  }
20809
20931
  }
20810
- } finally {
20811
- this.running = void 0;
20932
+ await Promise.race([...this.reads.values()].map((read) => read.promise));
20933
+ return true;
20934
+ }
20935
+ if (!head.ready) {
20936
+ await head.promise;
20937
+ return true;
20938
+ }
20939
+ const chunks = [], positions = [];
20940
+ for (const desired of this.desired) {
20941
+ if (!this.needsLoad(desired)) continue;
20942
+ const key = this.key(desired), read = this.reads.get(key);
20943
+ if (!read?.ready) break;
20944
+ this.reads.delete(key);
20945
+ if (read.failed) this.failed.set(key, { ...desired, error: read.error });
20946
+ else if (!read.chunk) this.missing.set(key, desired);
20947
+ else {
20948
+ chunks.push(read.chunk);
20949
+ positions.push(desired);
20950
+ }
20951
+ }
20952
+ if (!chunks.length) return true;
20953
+ for (const position2 of positions) this.owned.set(this.key(position2), position2);
20954
+ await this.placeOwnedColumns(positions, () => this.world.placeChunks(chunks));
20955
+ return true;
20956
+ }
20957
+ async placeOwnedColumns(positions, place) {
20958
+ try {
20959
+ await place();
20960
+ for (const position of positions) this.loaded.set(this.key(position), position);
20961
+ } catch (error) {
20962
+ try {
20963
+ await this.unloadMany(positions);
20964
+ } catch (cleanupError) {
20965
+ const failure = error instanceof Error ? error : new Error(String(error));
20966
+ try {
20967
+ Object.defineProperty(failure, "cause", { value: cleanupError, configurable: true });
20968
+ } catch {
20969
+ throw Object.assign(new Error(failure.message), { cause: cleanupError });
20970
+ }
20971
+ throw failure;
20972
+ }
20973
+ throw error;
20812
20974
  }
20813
20975
  }
20814
20976
  needsLoad(position) {