las-laz-js 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.1 (2026-10-21)
4
+
5
+ Worker fixes.
6
+
7
+ - Node: a script that stopped part way through a parallel LAZ read (one `chunks()` window, then
8
+ done) never exited, because an idle pool kept Node running. Workers now hold Node only while
9
+ they have work.
10
+ - Node: `node --input-type=module -e` scripts can decode in parallel. Workers used to inherit
11
+ `--input-type` and fail to start.
12
+ - A worker that cannot start (blocked or failing script) now means one-thread decoding, as
13
+ documented, rather than a failed read. The pool waits for every worker to report ready.
14
+
3
15
  ## 0.3.0 (2026-10-21)
4
16
 
5
17
  Faster LAZ.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "las-laz-js",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Zero-dependency JavaScript reader and writer for ASPRS LAS point clouds, and reader for LAZ (LASzip), for the browser and Node, built for files larger than memory.",
5
5
  "type": "module",
6
6
  "exports": {
package/src/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  //==========================================================
7
7
 
8
8
  /** Library version, matches package.json. */
9
- export var VERSION = "0.3.0";
9
+ export var VERSION = "0.3.1";
10
10
 
11
11
  export { LasError, ERROR_CODES } from "./errors.js";
12
12
  export { openLas, LasReader } from "./LasReader.js";
@@ -5,7 +5,8 @@
5
5
  // Runs as a browser module Worker or a Node worker_threads Worker.
6
6
  // In: { id, vlr, recordLength, packed: ArrayBuffer, chunks: [{ s, e, count }], base }
7
7
  // (s / e index into packed; base is packed's file offset)
8
- // Out: { id, records: ArrayBuffer } or { id, error: { code, message, offset } }
8
+ // Out: { ready: true } once listening, then per task
9
+ // { id, records: ArrayBuffer } or { id, error: { code, message, offset } }
9
10
  //
10
11
  // Uses the chunk decoder translated from laz-rs (https://github.com/laz-rs/laz-rs),
11
12
  // itself a port of LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
@@ -28,6 +29,7 @@ if (typeof self !== "undefined" && typeof self.postMessage === "function") {
28
29
  port = { post: function (m, t) { wt.parentPort.postMessage(m, t); } };
29
30
  wt.parentPort.on("message", handle);
30
31
  }
32
+ port.post({ ready: true }); // the pool waits for this before sending work
31
33
 
32
34
  var cached = { key: null, decode: null };
33
35
 
package/src/laz/pool.js CHANGED
@@ -6,8 +6,8 @@
6
6
  // queue and go to the next free worker; results come back as transferred
7
7
  // buffers. close() terminates every worker and fails what is still
8
8
  // queued, so nothing outlives a read. When workers cannot start (no
9
- // Worker, a blocked script), createLazPool returns null and the reader
10
- // decodes on its own thread.
9
+ // Worker, a blocked or failing script), createLazPool returns null and
10
+ // the reader decodes on its own thread.
11
11
  //
12
12
  // Uses the chunk decoder translated from laz-rs (https://github.com/laz-rs/laz-rs),
13
13
  // itself a port of LASzip by rapidlasso. (c) 2007-2015 martin isenburg,
@@ -38,28 +38,52 @@ export async function defaultThreads() {
38
38
  return Math.max(1, Math.min(MAX_THREADS, cores - 1));
39
39
  }
40
40
 
41
- // One worker, browser or Node, behind the same small interface
42
- async function spawn(onMessage, onFail) {
41
+ // Node worker options: the parent's flags are inherited, except --input-type
42
+ // (node --input-type=module -e passes it on, and a worker has no string input
43
+ // for it). Only then is execArgv given: an explicit one is checked flag by
44
+ // flag, and some parents (the test runner) hold flags a worker may not take.
45
+ function nodeWorkerOptions(argv) {
46
+ var isInputType = function (a, i) { return /^--input-type(=|$)/.test(a) || argv[i - 1] === "--input-type"; };
47
+ if (!argv.some(isInputType)) return undefined;
48
+ return { execArgv: argv.filter(function (a, i) { return !isInputType(a, i); }) };
49
+ }
50
+
51
+ // One worker, browser or Node, behind the same small interface. Resolves once
52
+ // the worker says it is ready; rejects if it fails or exits first (a blocked
53
+ // or broken script), so a pool that cannot start is known before any task.
54
+ async function spawn(onMessage, onFail, nodeUrl) {
55
+ var started = false, ready, failed;
56
+ var up = new Promise(function (res, rej) { ready = res; failed = rej; });
57
+ function message(m) { if (m && m.ready) { started = true; ready(); } else onMessage(m); }
58
+ function error(e) { if (started) onFail(e); else failed(e); }
59
+ var handle;
43
60
  if (typeof Worker === "function") {
44
61
  // written inline so bundlers (Vite, webpack 5) see the worker and emit it
45
62
  var w = new Worker(new URL("./chunkWorker.js", import.meta.url), { type: "module" });
46
- w.onmessage = function (e) { onMessage(e.data); };
47
- w.onerror = function (e) { if (e && e.preventDefault) e.preventDefault(); onFail(new Error(e && e.message ? e.message : "LAZ worker failed")); };
48
- return { post: function (m, t) { w.postMessage(m, t); }, close: function () { w.terminate(); } };
63
+ w.onmessage = function (e) { message(e.data); };
64
+ w.onerror = function (e) { if (e && e.preventDefault) e.preventDefault(); error(new Error(e && e.message ? e.message : "LAZ worker failed")); };
65
+ handle = { post: function (m, t) { w.postMessage(m, t); }, close: function () { w.terminate(); } };
66
+ } else {
67
+ var wt = await import(/* webpackIgnore: true */ /* @vite-ignore */ NODE_WORKER_THREADS);
68
+ var nw = new wt.Worker(nodeUrl || new URL("./chunkWorker.js", import.meta.url), nodeWorkerOptions(process.execArgv));
69
+ nw.on("message", message);
70
+ nw.on("error", error);
71
+ nw.on("exit", function (code) { if (!started) failed(new Error("LAZ worker exited (" + code + ") before it started")); });
72
+ // Node runs while a worker has a task and not while it is idle, so a forgotten
73
+ // pool never keeps it running (unref after the "message" listener, which refs again)
74
+ handle = { post: function (m, t) { nw.postMessage(m, t); }, close: function () { return nw.terminate(); }, hold: function () { nw.ref(); }, release: function () { nw.unref(); } };
49
75
  }
50
- var wt = await import(/* webpackIgnore: true */ /* @vite-ignore */ NODE_WORKER_THREADS);
51
- var nw = new wt.Worker(new URL("./chunkWorker.js", import.meta.url));
52
- nw.unref(); // a forgotten pool never keeps Node running
53
- nw.on("message", onMessage);
54
- nw.on("error", onFail);
55
- return { post: function (m, t) { nw.postMessage(m, t); }, close: function () { return nw.terminate(); } };
76
+ try { await up; } catch (e) { handle.close(); throw e; }
77
+ if (handle.release) handle.release();
78
+ return handle;
56
79
  }
57
80
 
58
81
  /**
59
82
  * @param {number} n - workers to start (2 or more)
83
+ * @param {URL} [nodeUrl] - a Node worker entry in place of chunkWorker.js (tests)
60
84
  * @returns {Promise<{size:number, decode:function(object): Promise<Uint8Array>, close:function(): void}|null>}
61
85
  */
62
- export async function createLazPool(n) {
86
+ export async function createLazPool(n, nodeUrl) {
63
87
  var workers = [], idle = [], queue = [], waiting = new Map(), nextId = 1, closed = false;
64
88
 
65
89
  function fail(err) {
@@ -76,6 +100,7 @@ export async function createLazPool(n) {
76
100
  var w = idle.pop(), job = queue.shift();
77
101
  job.worker = w;
78
102
  waiting.set(job.msg.id, job);
103
+ if (w.hold) w.hold();
79
104
  w.post(job.msg, [job.msg.packed]);
80
105
  }
81
106
  }
@@ -84,15 +109,19 @@ export async function createLazPool(n) {
84
109
  var job = waiting.get(m.id);
85
110
  if (!job) return;
86
111
  waiting.delete(m.id);
112
+ if (job.worker.release) job.worker.release();
87
113
  idle.push(job.worker);
88
114
  if (m.error) job.reject(new LasError(m.error.code || ERROR_CODES.UNSUPPORTED, m.error.message, m.error.offset));
89
115
  else job.resolve(new Uint8Array(m.records));
90
116
  pump();
91
117
  }
92
118
 
93
- try {
94
- for (var i = 0; i < n; i++) workers.push(await spawn(onMessage, fail));
95
- } catch (e) {
119
+ var starts = [];
120
+ for (var i = 0; i < n; i++) starts.push(spawn(onMessage, fail, nodeUrl));
121
+ var settled = await Promise.allSettled(starts);
122
+ workers = settled.filter(function (s) { return s.status === "fulfilled"; }).map(function (s) { return s.value; });
123
+ if (workers.length < n) {
124
+ // any worker that cannot start means the reader decodes on its own thread
96
125
  workers.forEach(function (w) { w.close(); });
97
126
  return null;
98
127
  }