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.
- package/dist/browser/esm/index.browser.d.mts +16 -2
- package/dist/browser/esm/util/util.mjs +0 -2
- package/dist/browser/esm/util/util.mjs.map +1 -1
- package/dist/browser/esm/world/MineRenderWorld.mjs +82 -12
- package/dist/browser/esm/world/MineRenderWorld.mjs.map +1 -1
- package/dist/browser/esm/world/WorldStreamer.mjs +109 -15
- package/dist/browser/esm/world/WorldStreamer.mjs.map +1 -1
- package/dist/browser/index.d.ts +16 -2
- package/dist/browser/index.js +191 -29
- package/dist/browser/index.js.map +1 -1
- package/dist/bundle.js +191 -29
- package/dist/bundle.js.map +1 -1
- package/dist/node/index.d.mts +16 -2
- package/dist/node/index.d.ts +16 -2
- package/dist/node/index.js +191 -29
- package/dist/node/index.js.map +1 -1
- package/dist/node/index.mjs +191 -29
- package/dist/node/index.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -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.
|
|
126
|
-
this.
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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"]}
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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;
|
package/dist/browser/index.js
CHANGED
|
@@ -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 >
|
|
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 >
|
|
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 >
|
|
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
|
-
|
|
19033
|
-
|
|
19034
|
-
|
|
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 >
|
|
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 (
|
|
19261
|
+
if (performance.now() - sliceStart > YIELD_INTERVAL_MS) {
|
|
19204
19262
|
await yieldToEventLoop();
|
|
19205
19263
|
sliceStart = performance.now();
|
|
19206
19264
|
}
|
|
19207
19265
|
}
|
|
19208
|
-
const
|
|
19209
|
-
|
|
19210
|
-
|
|
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.
|
|
20794
|
-
this.
|
|
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
|
-
|
|
20797
|
-
|
|
20798
|
-
|
|
20799
|
-
|
|
20800
|
-
|
|
20801
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20811
|
-
|
|
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) {
|