@navaramap/worker 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/helpers/transferConstructedPolygonGeometry.d.ts +6 -0
  4. package/dist/helpers/transferConstructedPolylineGeometry.d.ts +6 -0
  5. package/dist/helpers/transferReturnedConstructedTerrainMesh.d.ts +6 -0
  6. package/dist/index.d.ts +4 -0
  7. package/dist/index.js +3203 -0
  8. package/dist/index.umd.cjs +11 -0
  9. package/dist/manager.d.ts +19 -0
  10. package/dist/pool.d.ts +17 -0
  11. package/dist/recyclingPool.d.ts +86 -0
  12. package/dist/tasks/constructPolygonBatchedFeature.d.ts +2 -0
  13. package/dist/tasks/constructPolylineBatchedFeature.d.ts +2 -0
  14. package/dist/tasks/constructQuantizedMeshTerrainMesh.d.ts +4 -0
  15. package/dist/tasks/constructTerrainMesh.d.ts +4 -0
  16. package/dist/tasks/getImageDataFromBlob.d.ts +12 -0
  17. package/dist/tasks/getImageDataFromImageBitmap.d.ts +1 -0
  18. package/dist/tasks/index.d.ts +26 -0
  19. package/dist/tasks/parseMvtTile.d.ts +15 -0
  20. package/dist/tasks/upsampleQuantizedMeshTerrainMesh.d.ts +2 -0
  21. package/dist/tasks/upsampleTerrainMesh.d.ts +2 -0
  22. package/dist/tasks/waitWasm.d.ts +8 -0
  23. package/dist/utils/index.d.ts +5 -0
  24. package/dist/utils/toExtentRadianF32Like.d.ts +3 -0
  25. package/dist/utils/toPolygonMaterialLike.d.ts +3 -0
  26. package/dist/utils/toPolylineMaterialLike.d.ts +3 -0
  27. package/dist/utils/toTransferableMartiniLike.d.ts +3 -0
  28. package/dist/utils/toTransferablePolygonBatchedFeatureLike.d.ts +3 -0
  29. package/dist/utils/toTransferablePolylineBatchedFeatureLike.d.ts +3 -0
  30. package/dist/utils/toTransferableRasterDEMDataLike.d.ts +3 -0
  31. package/dist/utils/toTransferableTileLike.d.ts +5 -0
  32. package/dist/utils/toUpsamplableTerrainGeometryLike.d.ts +3 -0
  33. package/dist/utils/toVec3Like.d.ts +3 -0
  34. package/dist/worker/index.d.ts +4 -0
  35. package/dist/worker/transfer.d.ts +1 -0
  36. package/package.json +37 -0
  37. package/src/helpers/transferConstructedPolygonGeometry.ts +25 -0
  38. package/src/helpers/transferConstructedPolylineGeometry.ts +28 -0
  39. package/src/helpers/transferReturnedConstructedTerrainMesh.ts +21 -0
  40. package/src/index.ts +4 -0
  41. package/src/manager.test.ts +45 -0
  42. package/src/manager.ts +43 -0
  43. package/src/pool.test.ts +87 -0
  44. package/src/pool.ts +150 -0
  45. package/src/recyclingPool.test.ts +404 -0
  46. package/src/recyclingPool.ts +385 -0
  47. package/src/tasks/constructPolygonBatchedFeature.ts +40 -0
  48. package/src/tasks/constructPolylineBatchedFeature.ts +36 -0
  49. package/src/tasks/constructQuantizedMeshTerrainMesh.ts +36 -0
  50. package/src/tasks/constructTerrainMesh.ts +44 -0
  51. package/src/tasks/getImageDataFromBlob.ts +30 -0
  52. package/src/tasks/getImageDataFromImageBitmap.ts +19 -0
  53. package/src/tasks/index.ts +31 -0
  54. package/src/tasks/parseMvtTile.ts +67 -0
  55. package/src/tasks/upsampleQuantizedMeshTerrainMesh.ts +38 -0
  56. package/src/tasks/upsampleTerrainMesh.ts +40 -0
  57. package/src/tasks/waitWasm.ts +19 -0
  58. package/src/utils/index.ts +5 -0
  59. package/src/utils/toExtentRadianF32Like.ts +6 -0
  60. package/src/utils/toPolygonMaterialLike.ts +20 -0
  61. package/src/utils/toPolylineMaterialLike.ts +17 -0
  62. package/src/utils/toTransferableMartiniLike.ts +6 -0
  63. package/src/utils/toTransferablePolygonBatchedFeatureLike.ts +41 -0
  64. package/src/utils/toTransferablePolylineBatchedFeatureLike.ts +21 -0
  65. package/src/utils/toTransferableRasterDEMDataLike.ts +22 -0
  66. package/src/utils/toTransferableTileLike.ts +34 -0
  67. package/src/utils/toUpsamplableTerrainGeometryLike.ts +14 -0
  68. package/src/utils/toVec3Like.ts +6 -0
  69. package/src/vite-env.d.ts +1 -0
  70. package/src/worker/index.ts +44 -0
  71. package/src/worker/transfer.ts +6 -0
@@ -0,0 +1,385 @@
1
+ import invariant from "tiny-invariant";
2
+ import workerpool from "workerpool";
3
+ import type { Promise as WorkerpoolPromise } from "workerpool";
4
+ import type Pool from "workerpool/types/Pool";
5
+ import type { ExecOptions } from "workerpool/types/types";
6
+
7
+ // WASM linear memory only grows, so each worker's heap ratchets up to its
8
+ // peak decode working set and never returns memory to the OS. The only way
9
+ // to release it is to terminate the worker thread (the wasm-bindgen glue
10
+ // caches its instance and skips re-initialization within a worker), so this
11
+ // pool recycles worker threads.
12
+ //
13
+ // Each worker gets its own single-worker workerpool (a "slot") so it can be
14
+ // recycled independently at its own task boundaries: after each settled task
15
+ // the slot's actual heap size is probed ("getWasmMemoryUsage"); once it
16
+ // exceeds the budget the slot stops receiving new tasks, and as soon as its
17
+ // in-flight tasks drain it is terminated and replaced. This keeps memory
18
+ // bounded even under sustained load (no pool-wide idle moment is required)
19
+ // and never discards more than one warm WASM instance at a time. The
20
+ // trade-off versus a single shared queue is possible head-of-line blocking
21
+ // within a slot, but upstream backpressure (ConcurrencyManager) keeps
22
+ // per-slot queues shallow.
23
+ //
24
+ // A burst that stays under the heap budget would otherwise keep its heaps
25
+ // forever, so an idle sweep additionally recycles every used slot once the
26
+ // pool has been quiet for RECYCLE_IDLE_MS.
27
+ //
28
+ // Recycled slots are pre-warmed immediately with a fire-and-forget "warmUp"
29
+ // task so the next real task doesn't pay WASM initialization latency.
30
+
31
+ /**
32
+ * WASM linear memory budget per worker; a worker measured over this budget
33
+ * is recycled. Must be comfortably above the post-init baseline heap, or
34
+ * recycling would thrash.
35
+ */
36
+ export const MAX_WORKER_HEAP_BYTES_DEFAULT = 256 * 1024 * 1024;
37
+ /**
38
+ * Backstop for when heap probes cannot run (a failed probe already recycles
39
+ * conservatively, so this only matters if probing silently stalls).
40
+ */
41
+ export const RECYCLE_AFTER_TASKS_BACKSTOP = 128;
42
+ export const RECYCLE_IDLE_MS = 5_000;
43
+ /**
44
+ * How long workerpool waits for a worker to acknowledge a cleanup message
45
+ * (sent on cancel-while-running) before force-terminating it. A worker busy
46
+ * in a synchronous WASM task cannot respond until the task finishes, so the
47
+ * default (1s) would terminate exactly the long-running tasks the worker-side
48
+ * abort listener is meant to keep alive (see `keepWorkerAliveOnAbort`). Must
49
+ * comfortably exceed the longest expected task; a genuinely wedged worker
50
+ * still gets terminated once this elapses.
51
+ */
52
+ export const WORKER_TERMINATE_TIMEOUT_MS = 30_000;
53
+
54
+ export type RecyclingWorkerPoolOptions = {
55
+ /** Overrides {@link MAX_WORKER_HEAP_BYTES_DEFAULT}. */
56
+ maxWorkerHeapBytes?: number;
57
+ };
58
+
59
+ type Slot = {
60
+ pool: Pool;
61
+ /** Settled tasks since this slot's pool was (re)created. */
62
+ tasksSinceSpawn: number;
63
+ /** Dispatched but not yet settled tasks, including warm-ups and probes. */
64
+ inflight: number;
65
+ /** Set once the slot exceeds its heap budget; drained slots recycle. */
66
+ draining: boolean;
67
+ /** Whether a heap probe is already in flight (probes are coalesced). */
68
+ probing: boolean;
69
+ /** WASM heap size from the most recent probe; undefined until the first
70
+ * probe settles and after a recycle. Point-in-time — see {@link heapStats}. */
71
+ lastHeapBytes: number | undefined;
72
+ };
73
+
74
+ export type WorkerPoolStats = ReturnType<Pool["stats"]> & {
75
+ tasksSinceSpawn: number;
76
+ };
77
+
78
+ export type WorkerPoolHeapStats = {
79
+ /** Last probed WASM heap per slot (undefined = not probed yet). */
80
+ perSlot: (number | undefined)[];
81
+ /** Sum of the probed heaps. */
82
+ totalBytes: number;
83
+ /** The per-worker budget slots are recycled against. */
84
+ maxWorkerHeapBytes: number;
85
+ };
86
+
87
+ const createPool = (url: string) =>
88
+ workerpool.pool(url, {
89
+ // One worker per pool so each worker can be recycled independently.
90
+ maxWorkers: 1,
91
+ // Avoid oversubscribing CPU when combined with other systems (e.g., DRACO loader).
92
+ minWorkers: 0,
93
+ workerTerminateTimeout: WORKER_TERMINATE_TIMEOUT_MS,
94
+ workerOpts: {
95
+ type: import.meta.env.PROD ? undefined : "module",
96
+ },
97
+ });
98
+
99
+ export class RecyclingWorkerPool {
100
+ private readonly url: string;
101
+ private readonly maxWorkerHeapBytes: number;
102
+ private readonly slots: Slot[];
103
+ private idleSweepTimer: ReturnType<typeof setTimeout> | undefined;
104
+ private terminated = false;
105
+
106
+ constructor(
107
+ url: string,
108
+ maxWorkers: number,
109
+ options?: RecyclingWorkerPoolOptions,
110
+ ) {
111
+ invariant(maxWorkers > 0, "maxWorkers must be a positive number.");
112
+ this.url = url;
113
+ this.maxWorkerHeapBytes =
114
+ options?.maxWorkerHeapBytes ?? MAX_WORKER_HEAP_BYTES_DEFAULT;
115
+ this.slots = Array.from({ length: maxWorkers }, () => ({
116
+ pool: createPool(url),
117
+ tasksSinceSpawn: 0,
118
+ inflight: 0,
119
+ draining: false,
120
+ probing: false,
121
+ lastHeapBytes: undefined,
122
+ }));
123
+ }
124
+
125
+ exec<T extends (...args: any[]) => any>(
126
+ method: string | T,
127
+ params?: Parameters<T> | null,
128
+ options?: ExecOptions,
129
+ ): WorkerpoolPromise<ReturnType<T>> {
130
+ const slot = this.pickSlot();
131
+ slot.inflight++;
132
+ try {
133
+ return (
134
+ slot.pool
135
+ .exec(method, params, options)
136
+ // `finally` doesn't work, so use `then()` and `catch()` to ensure that `onSettled()` is invoked.
137
+ .then((value) => {
138
+ this.onSettled(slot);
139
+ return value;
140
+ })
141
+ .catch((e) => {
142
+ this.onSettled(slot);
143
+ throw e;
144
+ })
145
+ );
146
+ } catch (e) {
147
+ this.onSettled(slot);
148
+ throw e;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Pre-initializes WASM on the workers so that upcoming tasks don't pay
154
+ * the initialization latency. Warms a single worker and resolves once it
155
+ * is ready; the rest warm in the background. Correctness doesn't depend
156
+ * on warm workers (every task awaits WASM internally), and a slot whose
157
+ * warm-up fails surfaces the error on its next real task — the same
158
+ * policy as a recycled slot. Rejects only when every worker fails to
159
+ * warm, which signals a broken worker setup.
160
+ */
161
+ warmUp(): Promise<void> {
162
+ // Warming every slot at once would fetch and compile the worker WASM N
163
+ // times concurrently, competing over the same HTTP/2 connection with
164
+ // the main-thread WASM and other startup fetches. The first warm-up
165
+ // populates the HTTP cache, so the background ones load from cache.
166
+ const [first, ...rest] = this.slots;
167
+ return this.warmSlot(first).then(
168
+ () => {
169
+ for (const slot of rest) {
170
+ // Fire-and-forget; a failure surfaces on the slot's next real task.
171
+ this.warmSlot(slot).catch(() => undefined);
172
+ }
173
+ },
174
+ (e) => {
175
+ // A single failed spawn doesn't prove the setup is broken; fall
176
+ // back to the remaining workers and reject only when all fail.
177
+ if (rest.length === 0) throw e;
178
+ return Promise.any(rest.map((slot) => this.warmSlot(slot))).then(
179
+ () => undefined,
180
+ );
181
+ },
182
+ );
183
+ }
184
+
185
+ /**
186
+ * Whether at least one worker is still under its heap budget. When false,
187
+ * every slot is draining and upstream intake should pause until a recycled
188
+ * worker comes back (tasks dispatched anyway still run, on the
189
+ * least-loaded draining slot).
190
+ */
191
+ canAcceptTasks(): boolean {
192
+ return this.slots.some((slot) => !slot.draining);
193
+ }
194
+
195
+ stats(): WorkerPoolStats {
196
+ const aggregated = {
197
+ totalWorkers: 0,
198
+ busyWorkers: 0,
199
+ idleWorkers: 0,
200
+ pendingTasks: 0,
201
+ activeTasks: 0,
202
+ tasksSinceSpawn: 0,
203
+ };
204
+ for (const slot of this.slots) {
205
+ const stats = slot.pool.stats();
206
+ aggregated.totalWorkers += stats.totalWorkers;
207
+ aggregated.busyWorkers += stats.busyWorkers;
208
+ aggregated.idleWorkers += stats.idleWorkers;
209
+ aggregated.pendingTasks += stats.pendingTasks;
210
+ aggregated.activeTasks += stats.activeTasks;
211
+ aggregated.tasksSinceSpawn += slot.tasksSinceSpawn;
212
+ }
213
+ return aggregated;
214
+ }
215
+
216
+ /**
217
+ * Aggregated WASM heap sizes from the most recent probes. Values are
218
+ * point-in-time samples taken after task settlements (or {@link probeHeap});
219
+ * under load they lag by the tasks queued ahead of the probe.
220
+ */
221
+ heapStats(): WorkerPoolHeapStats {
222
+ const perSlot = this.slots.map((slot) => slot.lastHeapBytes);
223
+ return {
224
+ perSlot,
225
+ totalBytes: perSlot.reduce<number>((sum, b) => sum + (b ?? 0), 0),
226
+ maxWorkerHeapBytes: this.maxWorkerHeapBytes,
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Requests a fresh heap probe on every non-draining slot (draining slots
232
+ * are about to be recycled anyway). Probes coalesce per slot, so calling
233
+ * this at display frequency is safe.
234
+ */
235
+ probeHeap(): void {
236
+ if (this.terminated) return;
237
+ for (const slot of this.slots) {
238
+ if (!slot.draining) {
239
+ this.probeSlot(slot);
240
+ }
241
+ }
242
+ }
243
+
244
+ terminate(): void {
245
+ if (this.idleSweepTimer !== undefined) {
246
+ clearTimeout(this.idleSweepTimer);
247
+ this.idleSweepTimer = undefined;
248
+ }
249
+ this.terminated = true;
250
+ for (const slot of this.slots) {
251
+ slot.pool.terminate();
252
+ }
253
+ }
254
+
255
+ private pickSlot(): Slot {
256
+ // Prefer slots still under their heap budget; fall back to a draining
257
+ // slot only when every slot is draining at once (it recycles once it
258
+ // finally drains, so this cannot starve the recycling).
259
+ const candidates = this.slots.filter((slot) => !slot.draining);
260
+ const eligible = candidates.length > 0 ? candidates : this.slots;
261
+ return eligible.reduce((least, slot) =>
262
+ slot.inflight < least.inflight ? slot : least,
263
+ );
264
+ }
265
+
266
+ private onSettled(slot: Slot): void {
267
+ slot.inflight--;
268
+ if (this.terminated) return;
269
+ slot.tasksSinceSpawn++;
270
+ if (slot.tasksSinceSpawn >= RECYCLE_AFTER_TASKS_BACKSTOP) {
271
+ slot.draining = true;
272
+ }
273
+ // Terminating the worker must not kill in-flight tasks, so a slot over
274
+ // its budget stops receiving tasks (pickSlot skips it) and recycles at
275
+ // its next task boundary instead of immediately.
276
+ if (slot.draining && slot.inflight === 0) {
277
+ this.recycleSlot(slot);
278
+ } else if (!slot.draining) {
279
+ this.probeSlot(slot);
280
+ }
281
+ this.armIdleSweep();
282
+ }
283
+
284
+ private recycleSlot(slot: Slot): void {
285
+ slot.pool.terminate();
286
+ slot.pool = createPool(this.url);
287
+ slot.tasksSinceSpawn = 0;
288
+ slot.draining = false;
289
+ slot.lastHeapBytes = undefined;
290
+ // Fire-and-forget; a warm-up failure surfaces on the next real task.
291
+ this.warmSlot(slot).catch(() => undefined);
292
+ }
293
+
294
+ // Warm-ups and probes are internal traffic: they count into `inflight` (to
295
+ // steer dispatch away from a busy worker) but not into `tasksSinceSpawn`,
296
+ // and their settlement doesn't re-arm the idle sweep, so a sweep can't
297
+ // perpetuate itself through the work it triggers.
298
+
299
+ private warmSlot(slot: Slot): Promise<void> {
300
+ slot.inflight++;
301
+ const settle = () => {
302
+ slot.inflight--;
303
+ // Same as the probe: if the slot drained while this warm-up was in
304
+ // flight, its settlement is the last task boundary, so recycle here.
305
+ if (!this.terminated && slot.draining && slot.inflight === 0) {
306
+ this.recycleSlot(slot);
307
+ }
308
+ };
309
+ // Wrapped in a native promise so that a synchronous exec failure rejects
310
+ // instead of throwing through the recycle path.
311
+ return new Promise((resolve, reject) => {
312
+ try {
313
+ slot.pool
314
+ .exec("warmUp")
315
+ .then(() => {
316
+ settle();
317
+ resolve();
318
+ })
319
+ .catch((e) => {
320
+ settle();
321
+ reject(e);
322
+ });
323
+ } catch (e) {
324
+ settle();
325
+ reject(e);
326
+ }
327
+ });
328
+ }
329
+
330
+ // The heap probe is the recycle trigger: after each settled task the
331
+ // worker reports its actual linear memory size, which task-count or
332
+ // input-size heuristics could only approximate. A probe rides the slot's
333
+ // queue, so under load the measurement lags by the tasks queued ahead of
334
+ // it — acceptable since upstream backpressure keeps queues shallow.
335
+ private probeSlot(slot: Slot): void {
336
+ if (slot.probing) return;
337
+ slot.probing = true;
338
+ slot.inflight++;
339
+ const settle = (overBudget: boolean) => {
340
+ slot.probing = false;
341
+ slot.inflight--;
342
+ if (this.terminated) return;
343
+ if (overBudget) {
344
+ slot.draining = true;
345
+ }
346
+ // Also covers a slot drained by the backstop while this probe was in
347
+ // flight: the probe is then the last thing keeping the slot alive, so
348
+ // its settlement must trigger the recycle even when under budget.
349
+ if (slot.draining && slot.inflight === 0) {
350
+ this.recycleSlot(slot);
351
+ }
352
+ };
353
+ try {
354
+ slot.pool
355
+ .exec("getWasmMemoryUsage")
356
+ .then((bytes) => {
357
+ if (typeof bytes === "number") {
358
+ slot.lastHeapBytes = bytes;
359
+ }
360
+ settle(typeof bytes === "number" && bytes >= this.maxWorkerHeapBytes);
361
+ })
362
+ .catch(() => {
363
+ // A worker that cannot even report its heap is recycled conservatively.
364
+ settle(true);
365
+ });
366
+ } catch {
367
+ settle(true);
368
+ }
369
+ }
370
+
371
+ private armIdleSweep(): void {
372
+ // Re-armed on every settled task, so it fires RECYCLE_IDLE_MS after the
373
+ // last one settles; slots that are still busy at that point are swept
374
+ // when their remaining work settles and re-arms it.
375
+ if (this.idleSweepTimer !== undefined) clearTimeout(this.idleSweepTimer);
376
+ this.idleSweepTimer = setTimeout(() => {
377
+ this.idleSweepTimer = undefined;
378
+ for (const slot of this.slots) {
379
+ if (slot.tasksSinceSpawn > 0 && slot.inflight === 0) {
380
+ this.recycleSlot(slot);
381
+ }
382
+ }
383
+ }, RECYCLE_IDLE_MS);
384
+ }
385
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ PolygonMaterialLike,
3
+ TransferablePolygonBatchedFeatureLike,
4
+ ConstructedPolygonGeometryLike,
5
+ ExtentRadianF32Like,
6
+ } from "@navaramap/core";
7
+ import { constructPolygonBatchedFeature as constructPolygonBatchedFeatureImpl } from "@navaramap/engine-worker";
8
+
9
+ import { transfer } from "..";
10
+ import { transferConstructedPolygonGeometry } from "../helpers/transferConstructedPolygonGeometry";
11
+ import { toExtentRadianF32Like } from "../utils";
12
+ import { toPolygonMaterialLike } from "../utils/toPolygonMaterialLike";
13
+ import { toTransferablePolygonBatchedFeatureLike } from "../utils/toTransferablePolygonBatchedFeatureLike";
14
+
15
+ import { waitWasm } from "./waitWasm";
16
+
17
+ export async function constructPolygonBatchedFeature(
18
+ transferableBatchedFeatureLike: TransferablePolygonBatchedFeatureLike,
19
+ materialLike: PolygonMaterialLike,
20
+ flat: boolean,
21
+ tile_extent: ExtentRadianF32Like | undefined,
22
+ ): Promise<ConstructedPolygonGeometryLike | undefined> {
23
+ await waitWasm();
24
+
25
+ const geometry = constructPolygonBatchedFeatureImpl(
26
+ toTransferablePolygonBatchedFeatureLike(transferableBatchedFeatureLike),
27
+ toPolygonMaterialLike(materialLike),
28
+ flat,
29
+ tile_extent ? toExtentRadianF32Like(tile_extent) : undefined,
30
+ );
31
+
32
+ if (!geometry) {
33
+ return;
34
+ }
35
+
36
+ const { result, transfers } = transferConstructedPolygonGeometry(geometry);
37
+ geometry.free();
38
+
39
+ return transfer(result, transfers);
40
+ }
@@ -0,0 +1,36 @@
1
+ import {
2
+ TransferablePolylineBatchedFeatureLike,
3
+ PolylineMaterialLike,
4
+ ConstructedPolylineGeometryLike,
5
+ } from "@navaramap/core";
6
+ import { constructPolylineBatchedFeature as constructPolylineBatchedFeatureImpl } from "@navaramap/engine-worker";
7
+
8
+ import { transfer } from "..";
9
+ import { transferConstructedPolylineGeometry } from "../helpers/transferConstructedPolylineGeometry";
10
+ import { toPolylineMaterialLike } from "../utils/toPolylineMaterialLike";
11
+ import { toTransferablePolylineBatchedFeatureLike } from "../utils/toTransferablePolylineBatchedFeatureLike";
12
+
13
+ import { waitWasm } from "./waitWasm";
14
+
15
+ export async function constructPolylineBatchedFeature(
16
+ transferableBatchedFeatureLike: TransferablePolylineBatchedFeatureLike,
17
+ materialLike: PolylineMaterialLike,
18
+ flat: boolean,
19
+ ): Promise<ConstructedPolylineGeometryLike | undefined> {
20
+ await waitWasm();
21
+
22
+ const geometry = constructPolylineBatchedFeatureImpl(
23
+ toTransferablePolylineBatchedFeatureLike(transferableBatchedFeatureLike),
24
+ toPolylineMaterialLike(materialLike),
25
+ flat,
26
+ );
27
+
28
+ if (!geometry) {
29
+ return;
30
+ }
31
+
32
+ const { result, transfers } = transferConstructedPolylineGeometry(geometry);
33
+ geometry.free();
34
+
35
+ return transfer(result, transfers);
36
+ }
@@ -0,0 +1,36 @@
1
+ import type {
2
+ ReturnedConstructedTerrainMeshLike,
3
+ TransferableTileLike,
4
+ } from "@navaramap/core";
5
+ import { constructQuantizedMeshTerrainMesh as constructQuantizedMeshTerrainMeshImpl } from "@navaramap/engine-worker";
6
+
7
+ import { transfer } from "..";
8
+ import { transferReturnedConstructedTerrainMesh } from "../helpers/transferReturnedConstructedTerrainMesh";
9
+ import { toTransferableTile } from "../utils";
10
+
11
+ import { waitWasm } from "./waitWasm";
12
+
13
+ export async function constructQuantizedMeshTerrainMesh(
14
+ bytes: Uint8Array,
15
+ tile: TransferableTileLike,
16
+ skirt: boolean,
17
+ skirtExaggeration: number,
18
+ geographic: boolean,
19
+ tms: boolean,
20
+ ): Promise<{
21
+ result: ReturnedConstructedTerrainMeshLike;
22
+ }> {
23
+ await waitWasm();
24
+
25
+ const mesh = constructQuantizedMeshTerrainMeshImpl(
26
+ bytes,
27
+ toTransferableTile(tile),
28
+ skirt,
29
+ skirtExaggeration,
30
+ geographic,
31
+ tms,
32
+ );
33
+ const { result, transfers } = transferReturnedConstructedTerrainMesh(mesh);
34
+ mesh.free();
35
+ return transfer({ result }, [...transfers]);
36
+ }
@@ -0,0 +1,44 @@
1
+ import type {
2
+ TransferableTileLike,
3
+ TransferableRasterDEMDataLike,
4
+ ReturnedConstructedTerrainMeshLike,
5
+ } from "@navaramap/core";
6
+ import {
7
+ constructTerrainMesh as constructTerrainMeshImpl,
8
+ TransferableMartini,
9
+ } from "@navaramap/engine-worker";
10
+
11
+ import { transfer } from "..";
12
+ import { transferReturnedConstructedTerrainMesh } from "../helpers/transferReturnedConstructedTerrainMesh";
13
+ import { toTransferableTile } from "../utils";
14
+ import { toTransferableRasterDEMDataLike } from "../utils/toTransferableRasterDEMDataLike";
15
+
16
+ import { waitWasm } from "./waitWasm";
17
+
18
+ export async function constructTerrainMesh(
19
+ bytes: Uint8Array,
20
+ tile: TransferableTileLike,
21
+ rasterDEMData: TransferableRasterDEMDataLike,
22
+ size: number,
23
+ skirt: boolean,
24
+ skirtExaggeration: number,
25
+ ): Promise<{
26
+ result: ReturnedConstructedTerrainMeshLike;
27
+ }> {
28
+ await waitWasm();
29
+
30
+ const tileSizeWithBoundary = size + 1;
31
+ const martini = TransferableMartini.fromSize(tileSizeWithBoundary);
32
+
33
+ const mesh = constructTerrainMeshImpl(
34
+ bytes,
35
+ toTransferableTile(tile),
36
+ toTransferableRasterDEMDataLike(rasterDEMData),
37
+ martini,
38
+ skirt,
39
+ skirtExaggeration,
40
+ );
41
+ const { result, transfers } = transferReturnedConstructedTerrainMesh(mesh);
42
+ mesh.free();
43
+ return transfer({ result }, [...transfers]);
44
+ }
@@ -0,0 +1,30 @@
1
+ import { transfer } from "..";
2
+
3
+ /**
4
+ * Decode a compressed image Blob into raw RGBA pixels entirely off the main
5
+ * thread. Running `createImageBitmap` inside the worker keeps the (potentially
6
+ * expensive) image decode off the main thread, which matters for DEM /
7
+ * elevation tiles that stream in large numbers while panning and zooming.
8
+ *
9
+ * The pixel buffer is transferred back to the caller (not structured-cloned),
10
+ * so the worker→main hand-off moves ownership instead of copying width*height*4
11
+ * bytes. The Blob argument is already cheap to pass in: its structured clone
12
+ * shares the immutable backing bytes by reference, and Blob is not transferable.
13
+ */
14
+ export async function getImageDataFromBlob(
15
+ blob: Blob,
16
+ ): Promise<Uint8ClampedArray> {
17
+ const bitmap = await createImageBitmap(blob);
18
+ try {
19
+ const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
20
+ const context = canvas.getContext("2d");
21
+ if (context === null) {
22
+ throw new Error("failed to get context of canvas");
23
+ }
24
+ context.drawImage(bitmap, 0, 0);
25
+ const data = context.getImageData(0, 0, bitmap.width, bitmap.height).data;
26
+ return transfer(data, [data.buffer]);
27
+ } finally {
28
+ bitmap.close();
29
+ }
30
+ }
@@ -0,0 +1,19 @@
1
+ export async function getImageDataFromImageBitmap(
2
+ img: ImageBitmap,
3
+ canvas: OffscreenCanvas,
4
+ ): Promise<Uint8ClampedArray> {
5
+ try {
6
+ const context = canvas.getContext("2d");
7
+ if (context === null) {
8
+ throw new Error("failed to get context of canvas");
9
+ } else {
10
+ context.drawImage(img, 0, 0);
11
+ }
12
+ const data = context.getImageData(0, 0, img.width, img.height).data;
13
+ return data;
14
+ } finally {
15
+ // The bitmap is transferred into this worker; close it so its backing
16
+ // store is released immediately instead of waiting for GC.
17
+ img.close();
18
+ }
19
+ }
@@ -0,0 +1,31 @@
1
+ import { constructPolygonBatchedFeature } from "./constructPolygonBatchedFeature";
2
+ import { constructPolylineBatchedFeature } from "./constructPolylineBatchedFeature";
3
+ import { constructQuantizedMeshTerrainMesh } from "./constructQuantizedMeshTerrainMesh";
4
+ import { constructTerrainMesh } from "./constructTerrainMesh";
5
+ import { getImageDataFromBlob } from "./getImageDataFromBlob";
6
+ import { getImageDataFromImageBitmap } from "./getImageDataFromImageBitmap";
7
+ import { parseMvtTile } from "./parseMvtTile";
8
+ import { upsampleQuantizedMeshTerrainMesh } from "./upsampleQuantizedMeshTerrainMesh";
9
+ import { upsampleTerrainMesh } from "./upsampleTerrainMesh";
10
+ import { getWasmMemoryUsage, waitWasm } from "./waitWasm";
11
+
12
+ /** Pre-warm worker by initializing WASM module */
13
+ export async function warmUp(): Promise<void> {
14
+ await waitWasm();
15
+ }
16
+
17
+ export const commonTasks = {
18
+ constructTerrainMesh,
19
+ constructQuantizedMeshTerrainMesh,
20
+ upsampleTerrainMesh,
21
+ upsampleQuantizedMeshTerrainMesh,
22
+ getImageDataFromImageBitmap,
23
+ getImageDataFromBlob,
24
+ constructPolygonBatchedFeature,
25
+ constructPolylineBatchedFeature,
26
+ parseMvtTile,
27
+ warmUp,
28
+ getWasmMemoryUsage,
29
+ };
30
+
31
+ export type CommonTasks = typeof commonTasks;
@@ -0,0 +1,67 @@
1
+ import { ExtentRadianF32Like } from "@navaramap/core";
2
+ import { parseMvtTile as parseMvtTileImpl } from "@navaramap/engine-worker";
3
+
4
+ import { transfer } from "..";
5
+ import { toExtentRadianF32Like } from "../utils";
6
+
7
+ import { waitWasm } from "./waitWasm";
8
+
9
+ /**
10
+ * A whole tile's parsed groups packed into four per-type streams (see
11
+ * `navara_parser::mvt::pack_parsed_mvt_groups` for the segment order). The
12
+ * tile meta (per-group headers + per-layer property tables) rides along as a
13
+ * structured clone; only the four stream buffers are transferred.
14
+ */
15
+ export type ParsedMvtTileResultLike = {
16
+ f64_stream: Float64Array;
17
+ f32_stream: Float32Array;
18
+ u32_stream: Uint32Array;
19
+ u8_stream: Uint8Array;
20
+ meta: unknown;
21
+ };
22
+
23
+ export async function parseMvtTile(
24
+ bytes: Uint8Array,
25
+ x: number,
26
+ y: number,
27
+ z: number,
28
+ tileExtent: ExtentRadianF32Like | undefined,
29
+ compression: number,
30
+ configsJson: string,
31
+ ): Promise<ParsedMvtTileResultLike> {
32
+ await waitWasm();
33
+
34
+ const packed = parseMvtTileImpl(
35
+ bytes,
36
+ x,
37
+ y,
38
+ z,
39
+ tileExtent ? toExtentRadianF32Like(tileExtent) : undefined,
40
+ compression,
41
+ configsJson,
42
+ );
43
+
44
+ // Free the wasm-bindgen result even when a stream getter throws (e.g. an
45
+ // allocation failure), or its Rust-side Vecs would leak in worker WASM memory.
46
+ let f64_stream: Float64Array;
47
+ let f32_stream: Float32Array;
48
+ let u32_stream: Uint32Array;
49
+ let u8_stream: Uint8Array;
50
+ let meta: unknown;
51
+ try {
52
+ f64_stream = packed.f64_stream();
53
+ f32_stream = packed.f32_stream();
54
+ u32_stream = packed.u32_stream();
55
+ u8_stream = packed.u8_stream();
56
+ meta = packed.meta();
57
+ } finally {
58
+ packed.free();
59
+ }
60
+
61
+ return transfer({ f64_stream, f32_stream, u32_stream, u8_stream, meta }, [
62
+ f64_stream.buffer,
63
+ f32_stream.buffer,
64
+ u32_stream.buffer,
65
+ u8_stream.buffer,
66
+ ]);
67
+ }