@loradb/lora-node 0.15.0 → 0.17.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.
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
31
31
  return (mod && mod.__esModule) ? mod : { "default": mod };
32
32
  };
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.Transaction = void 0;
34
35
  exports.createDatabase = createDatabase;
35
36
  exports.openWalDatabase = openWalDatabase;
36
37
  const node_buffer_1 = require("node:buffer");
@@ -46,10 +47,20 @@ __exportStar(require("./types.js"), exports);
46
47
  class NativeRowStream {
47
48
  #inner;
48
49
  #streamId;
50
+ #signal;
51
+ #release;
49
52
  #closed = false;
50
- constructor(inner, streamId) {
53
+ constructor(inner, streamId, signal, release) {
51
54
  this.#inner = inner;
52
55
  this.#streamId = streamId;
56
+ this.#signal = signal;
57
+ this.#release = release;
58
+ }
59
+ #finish() {
60
+ this.#closed = true;
61
+ const release = this.#release;
62
+ this.#release = undefined;
63
+ release?.();
53
64
  }
54
65
  [Symbol.asyncIterator]() {
55
66
  return this;
@@ -69,13 +80,15 @@ class NativeRowStream {
69
80
  try {
70
81
  const row = this.#inner.streamNext(this.#streamId);
71
82
  if (row === null) {
72
- this.#closed = true;
83
+ this.#finish();
73
84
  return { done: true, value: undefined };
74
85
  }
75
86
  return { done: false, value: row };
76
87
  }
77
88
  catch (err) {
78
- this.#closed = true;
89
+ this.#finish();
90
+ if (this.#signal?.aborted)
91
+ throw this.#signal.reason;
79
92
  throw (0, types_js_1.wrapError)(err);
80
93
  }
81
94
  }
@@ -96,7 +109,7 @@ class NativeRowStream {
96
109
  close() {
97
110
  if (this.#closed)
98
111
  return;
99
- this.#closed = true;
112
+ this.#finish();
100
113
  try {
101
114
  this.#inner.streamClose(this.#streamId);
102
115
  }
@@ -105,6 +118,189 @@ class NativeRowStream {
105
118
  }
106
119
  }
107
120
  }
121
+ const CHANGES_POLL_MAX = 256;
122
+ class NativeChangeFeed {
123
+ #inner;
124
+ #signal;
125
+ #onDone;
126
+ #opened;
127
+ ready;
128
+ #id;
129
+ #buffer = [];
130
+ #done = false;
131
+ #error = undefined;
132
+ #lastLsn;
133
+ #wakeSeq = 0;
134
+ #wake;
135
+ #queue = Promise.resolve();
136
+ #onAbort;
137
+ constructor(inner, options, onDone) {
138
+ this.#inner = inner;
139
+ this.#signal = options?.signal;
140
+ this.#onDone = onDone;
141
+ const fromLsn = normalizeLsn(options?.fromLsn);
142
+ this.#lastLsn = fromLsn ?? undefined;
143
+ const bufferSize = normalizeBufferSize(options?.bufferSize);
144
+ if (this.#signal?.aborted) {
145
+ this.#done = true;
146
+ this.#error = this.#signal.reason;
147
+ this.#opened = Promise.reject(this.#signal.reason);
148
+ }
149
+ else {
150
+ // Subscribes synchronously when capture is already on, so writes
151
+ // issued after `changes()` returns are never missed.
152
+ let opening;
153
+ try {
154
+ opening = inner.openChanges(fromLsn, bufferSize, () => this.#onWake());
155
+ }
156
+ catch (err) {
157
+ opening = Promise.reject(err);
158
+ }
159
+ this.#opened = opening.then((id) => {
160
+ this.#id = id;
161
+ if (this.#done)
162
+ this.#closeNative();
163
+ return id;
164
+ });
165
+ if (this.#signal) {
166
+ const signal = this.#signal;
167
+ this.#onAbort = () => {
168
+ this.#buffer = [];
169
+ this.#end(signal.reason);
170
+ };
171
+ signal.addEventListener("abort", this.#onAbort, { once: true });
172
+ }
173
+ }
174
+ this.ready = this.#opened.then(() => undefined, (err) => {
175
+ throw (0, types_js_1.wrapError)(err);
176
+ });
177
+ // `ready` is optional to await; its rejection also reaches `next()`.
178
+ this.ready.catch(() => undefined);
179
+ }
180
+ get lastLsn() {
181
+ return this.#lastLsn;
182
+ }
183
+ [Symbol.asyncIterator]() {
184
+ return this;
185
+ }
186
+ next() {
187
+ // Serialize concurrent `next()` calls so batches stay in order.
188
+ const result = this.#queue.then(() => this.#next());
189
+ this.#queue = result.catch(() => undefined);
190
+ return result;
191
+ }
192
+ async return() {
193
+ this.close();
194
+ return { done: true, value: undefined };
195
+ }
196
+ close() {
197
+ this.#buffer = [];
198
+ this.#end(undefined);
199
+ }
200
+ /** Called by `dispose()`: the native side already dropped the feed. */
201
+ disposed() {
202
+ this.#id = undefined;
203
+ this.#end(undefined);
204
+ }
205
+ async #next() {
206
+ for (;;) {
207
+ const buffered = this.#buffer.shift();
208
+ if (buffered) {
209
+ this.#lastLsn = buffered.lsn;
210
+ return { done: false, value: buffered };
211
+ }
212
+ if (this.#done) {
213
+ const err = this.#error;
214
+ this.#error = undefined;
215
+ if (err !== undefined)
216
+ throw err;
217
+ return { done: true, value: undefined };
218
+ }
219
+ let id;
220
+ try {
221
+ id = await this.#opened;
222
+ }
223
+ catch (err) {
224
+ this.#end((0, types_js_1.wrapError)(err));
225
+ continue;
226
+ }
227
+ if (this.#done)
228
+ continue;
229
+ const seq = this.#wakeSeq;
230
+ let polled;
231
+ try {
232
+ polled = await this.#inner.changesPoll(id, CHANGES_POLL_MAX);
233
+ }
234
+ catch (err) {
235
+ this.#end(this.#done ? undefined : (0, types_js_1.wrapError)(err));
236
+ continue;
237
+ }
238
+ this.#buffer.push(...polled.batches);
239
+ if (polled.closed) {
240
+ this.#end(undefined);
241
+ continue;
242
+ }
243
+ if (polled.batches.length === 0 && seq === this.#wakeSeq && !this.#done) {
244
+ await new Promise((resolve) => {
245
+ this.#wake = resolve;
246
+ });
247
+ }
248
+ }
249
+ }
250
+ #onWake() {
251
+ this.#wakeSeq += 1;
252
+ const wake = this.#wake;
253
+ this.#wake = undefined;
254
+ wake?.();
255
+ }
256
+ #end(error) {
257
+ if (this.#done) {
258
+ if (error !== undefined && this.#error === undefined)
259
+ this.#error = error;
260
+ return;
261
+ }
262
+ this.#done = true;
263
+ this.#error = error;
264
+ if (this.#onAbort && this.#signal) {
265
+ this.#signal.removeEventListener("abort", this.#onAbort);
266
+ this.#onAbort = undefined;
267
+ }
268
+ this.#closeNative();
269
+ this.#onDone(this);
270
+ this.#onWake();
271
+ }
272
+ #closeNative() {
273
+ const id = this.#id;
274
+ if (id === undefined)
275
+ return;
276
+ this.#id = undefined;
277
+ try {
278
+ this.#inner.changesClose(id);
279
+ }
280
+ catch {
281
+ // Already gone (database disposed).
282
+ }
283
+ }
284
+ }
285
+ function normalizeLsn(lsn) {
286
+ if (lsn === undefined)
287
+ return null;
288
+ const value = typeof lsn === "bigint" ? Number(lsn) : lsn;
289
+ if (!Number.isSafeInteger(value) ||
290
+ value < 0 ||
291
+ (typeof lsn === "bigint" && BigInt(value) !== lsn)) {
292
+ throw new types_js_1.LoraError(`\`fromLsn\` must be a non-negative safe integer, got ${String(lsn)}`, "LORA_INVALID_PARAMS");
293
+ }
294
+ return value;
295
+ }
296
+ function normalizeBufferSize(size) {
297
+ if (size === undefined)
298
+ return null;
299
+ if (!Number.isInteger(size) || size < 1 || size > 0xffff_ffff) {
300
+ throw new types_js_1.LoraError(`\`bufferSize\` must be a positive integer, got ${String(size)}`, "LORA_INVALID_PARAMS");
301
+ }
302
+ return size;
303
+ }
108
304
  function isFetchUrl(url) {
109
305
  return (url.protocol === "http:" ||
110
306
  url.protocol === "https:" ||
@@ -253,6 +449,7 @@ function normalizeSnapshotLoadOptions(options) {
253
449
  */
254
450
  class DatabaseImpl {
255
451
  #inner;
452
+ #feeds = new Set();
256
453
  constructor(inner) {
257
454
  this.#inner = inner;
258
455
  }
@@ -262,15 +459,25 @@ class DatabaseImpl {
262
459
  * with a narrowed `code` from the `LoraErrorCode` union (e.g.
263
460
  * `LORA_PARSE`, `LORA_INVALID_PARAMS`, `LORA_INTERNAL`).
264
461
  */
265
- async execute(query, params) {
462
+ async execute(query, params, options) {
266
463
  try {
267
- const buf = await this.#inner.execute(query, params ?? null);
464
+ const encoded = encodeParams(params);
465
+ const buf = await this.#limited(options, (timeoutMs, token) => this.#inner.execute(query, encoded, timeoutMs, token));
268
466
  return (0, decode_js_1.decodeResult)(buf);
269
467
  }
270
468
  catch (err) {
271
469
  throw (0, types_js_1.wrapError)(err);
272
470
  }
273
471
  }
472
+ /**
473
+ * Run `start` under `options`. Without a signal the timeout goes to the
474
+ * native call directly. With one, a native cancellation token carries
475
+ * the timeout, `abort` cancels the query server-side, and a cancelled
476
+ * call rejects with the signal's reason.
477
+ */
478
+ #limited(options, start) {
479
+ return runLimited(this.#inner, options, start);
480
+ }
274
481
  /**
275
482
  * Compile a query and return its execution plan without running it.
276
483
  *
@@ -281,7 +488,7 @@ class DatabaseImpl {
281
488
  */
282
489
  async explain(query, params) {
283
490
  try {
284
- return (await this.#inner.explain(query, params ?? null));
491
+ return (await this.#inner.explain(query, encodeParams(params)));
285
492
  }
286
493
  catch (err) {
287
494
  throw (0, types_js_1.wrapError)(err);
@@ -298,7 +505,7 @@ class DatabaseImpl {
298
505
  */
299
506
  async profile(query, params) {
300
507
  try {
301
- return (await this.#inner.profile(query, params ?? null));
508
+ return (await this.#inner.profile(query, encodeParams(params)));
302
509
  }
303
510
  catch (err) {
304
511
  throw (0, types_js_1.wrapError)(err);
@@ -311,18 +518,39 @@ class DatabaseImpl {
311
518
  * Rows are materialized by the native `execute()` promise today, then
312
519
  * yielded one at a time to JS consumers.
313
520
  */
314
- stream(query, params) {
521
+ stream(query, params, options) {
315
522
  try {
316
- const streamId = this.#inner.openStream(query, params ?? null);
317
- return new NativeRowStream(this.#inner, streamId);
523
+ const encoded = encodeParams(params);
524
+ const timeoutMs = normalizeTimeout(options?.timeoutMs);
525
+ const signal = options?.signal;
526
+ if (signal == null) {
527
+ const streamId = this.#inner.openStream(query, encoded, timeoutMs, null);
528
+ return new NativeRowStream(this.#inner, streamId);
529
+ }
530
+ signal.throwIfAborted();
531
+ const token = this.#inner.createCancelToken(timeoutMs);
532
+ const onAbort = () => this.#inner.cancelQuery(token);
533
+ signal.addEventListener("abort", onAbort, { once: true });
534
+ const release = () => {
535
+ signal.removeEventListener("abort", onAbort);
536
+ this.#inner.releaseCancelToken(token);
537
+ };
538
+ try {
539
+ const streamId = this.#inner.openStream(query, encoded, null, token);
540
+ return new NativeRowStream(this.#inner, streamId, signal, release);
541
+ }
542
+ catch (err) {
543
+ release();
544
+ throw err;
545
+ }
318
546
  }
319
547
  catch (err) {
320
548
  throw (0, types_js_1.wrapError)(err);
321
549
  }
322
550
  }
323
551
  /** Alias for `stream()`, useful when naming the row-level API explicitly. */
324
- rows(query, params) {
325
- return this.stream(query, params);
552
+ rows(query, params, options) {
553
+ return this.stream(query, params, options);
326
554
  }
327
555
  /**
328
556
  * Execute a statement batch inside one native transaction.
@@ -331,9 +559,32 @@ class DatabaseImpl {
331
559
  * native transaction is dropped before commit and all prior writes in the
332
560
  * batch are rolled back.
333
561
  */
334
- async transaction(statements, mode = "read_write") {
562
+ /**
563
+ * Begin an interactive transaction: run statements, decide in JS, then
564
+ * `commit()` or `rollback()`.
565
+ *
566
+ * A `read_write` transaction holds the database's writer lock from
567
+ * `begin()` until it finishes, so no other write can interleave with it
568
+ * (other writers wait). Reads outside the transaction keep seeing the
569
+ * last committed state. A statement that fails rolls the transaction
570
+ * back. An unfinished transaction rolls back when the handle is disposed
571
+ * (`await using tx = await db.begin()`), when the database is disposed,
572
+ * or when the process exits. Keep transactions short: while one is open,
573
+ * writers queue behind it.
574
+ */
575
+ async begin(mode = "read_write") {
335
576
  try {
336
- const buffers = await this.#inner.transaction(statements, mode);
577
+ const id = await this.#inner.beginTransaction(mode);
578
+ return new Transaction(this.#inner, id);
579
+ }
580
+ catch (err) {
581
+ throw (0, types_js_1.wrapError)(err);
582
+ }
583
+ }
584
+ async transaction(statements, mode = "read_write", options) {
585
+ try {
586
+ const encoded = statements.map((st) => st.params == null ? st : { ...st, params: encodeParams(st.params) });
587
+ const buffers = await this.#limited(options, (timeoutMs, token) => this.#inner.transaction(encoded, mode, timeoutMs, token));
337
588
  return buffers.map((buf) => (0, decode_js_1.decodeResult)(buf));
338
589
  }
339
590
  catch (err) {
@@ -396,6 +647,34 @@ class DatabaseImpl {
396
647
  catch (err) {
397
648
  throw (0, types_js_1.wrapError)(err);
398
649
  }
650
+ finally {
651
+ for (const feed of [...this.#feeds])
652
+ feed.disposed();
653
+ this.#feeds.clear();
654
+ }
655
+ }
656
+ /**
657
+ * Subscribe to committed changes: one `LoraChangeBatch` per committed
658
+ * write (auto-commit query, transaction, streamed write, `clear()`,
659
+ * snapshot restore), in commit order. Rolled-back work never appears.
660
+ *
661
+ * ```ts
662
+ * for await (const batch of db.changes({ fromLsn: saved })) {
663
+ * for (const change of batch.changes) handle(change);
664
+ * saved = batch.lsn;
665
+ * }
666
+ * ```
667
+ *
668
+ * Writers never wait for the feed. A consumer that falls more than
669
+ * `bufferSize` batches behind gets `LORA_CHANGES_LAGGED` after the
670
+ * buffered batches and should resume with `fromLsn: feed.lastLsn`.
671
+ * `dispose()` ends every feed. An open feed does not keep the process
672
+ * alive on its own.
673
+ */
674
+ changes(options) {
675
+ const feed = new NativeChangeFeed(this.#inner, options, (done) => this.#feeds.delete(done));
676
+ this.#feeds.add(feed);
677
+ return feed;
399
678
  }
400
679
  /**
401
680
  * Save the graph as a snapshot.
@@ -460,6 +739,127 @@ class DatabaseImpl {
460
739
  }
461
740
  }
462
741
  }
742
+ /**
743
+ * An open interactive transaction from `Database.begin()`.
744
+ */
745
+ class Transaction {
746
+ #inner;
747
+ #id;
748
+ #open = true;
749
+ /** @internal */
750
+ constructor(inner, id) {
751
+ this.#inner = inner;
752
+ this.#id = id;
753
+ }
754
+ /** Whether the transaction can still run statements. */
755
+ get isOpen() {
756
+ return this.#open;
757
+ }
758
+ /**
759
+ * Run one statement inside the transaction. It sees the transaction's
760
+ * own uncommitted writes. If it fails, the transaction is rolled back.
761
+ */
762
+ async execute(query, params, options) {
763
+ this.#assertOpen();
764
+ try {
765
+ const encoded = encodeParams(params);
766
+ const buf = await runLimited(this.#inner, options, (timeoutMs, token) => this.#inner.txExecute(this.#id, query, encoded, timeoutMs, token));
767
+ return (0, decode_js_1.decodeResult)(buf);
768
+ }
769
+ catch (err) {
770
+ this.#open = false;
771
+ throw (0, types_js_1.wrapError)(err);
772
+ }
773
+ }
774
+ /**
775
+ * Run several statements inside the transaction in one native call and
776
+ * return their results in order. Each statement sees the writes of the
777
+ * ones before it. This saves the per-call round trip of calling
778
+ * `execute()` once per statement.
779
+ *
780
+ * The batch stops at the first failing statement: the transaction is
781
+ * rolled back (as with a failed `execute()`) and the promise rejects
782
+ * with that statement's error, whose message ends with
783
+ * `(statement i of n)`. `options.timeoutMs` and `options.signal` bound
784
+ * the whole batch.
785
+ */
786
+ async executeMany(statements, options) {
787
+ this.#assertOpen();
788
+ let encoded;
789
+ try {
790
+ encoded = statements.map((st) => st.params == null ? st : { ...st, params: encodeParams(st.params) });
791
+ }
792
+ catch (err) {
793
+ // Invalid params: nothing ran, the transaction stays open.
794
+ throw (0, types_js_1.wrapError)(err);
795
+ }
796
+ try {
797
+ const buffers = await runLimited(this.#inner, options, (timeoutMs, token) => this.#inner.txExecuteMany(this.#id, encoded, timeoutMs, token));
798
+ return buffers.map((buf) => (0, decode_js_1.decodeResult)(buf));
799
+ }
800
+ catch (err) {
801
+ this.#open = false;
802
+ throw (0, types_js_1.wrapError)(err);
803
+ }
804
+ }
805
+ /** Commit every write made in the transaction and release the writer lock. */
806
+ async commit() {
807
+ await this.#finish(true);
808
+ }
809
+ /** Discard every write made in the transaction and release the writer lock. */
810
+ async rollback() {
811
+ await this.#finish(false);
812
+ }
813
+ /** Roll back if still open. Lets `await using tx = await db.begin()` clean up. */
814
+ async [Symbol.asyncDispose]() {
815
+ if (this.#open)
816
+ await this.rollback();
817
+ }
818
+ async #finish(commit) {
819
+ this.#assertOpen();
820
+ this.#open = false;
821
+ try {
822
+ await this.#inner.txFinish(this.#id, commit);
823
+ }
824
+ catch (err) {
825
+ throw (0, types_js_1.wrapError)(err);
826
+ }
827
+ }
828
+ #assertOpen() {
829
+ if (!this.#open) {
830
+ throw new types_js_1.LoraError("transaction is no longer open (already committed, rolled back, or failed)", "LORA_TRANSACTION");
831
+ }
832
+ }
833
+ }
834
+ exports.Transaction = Transaction;
835
+ /**
836
+ * Run `start` under `options`. Without a signal the timeout goes to the
837
+ * native call directly. With one, a native cancellation token carries the
838
+ * timeout, `abort` cancels the query server-side, and a cancelled call
839
+ * rejects with the signal's reason.
840
+ */
841
+ async function runLimited(inner, options, start) {
842
+ const timeoutMs = normalizeTimeout(options?.timeoutMs);
843
+ const signal = options?.signal;
844
+ if (signal == null)
845
+ return start(timeoutMs, null);
846
+ signal.throwIfAborted();
847
+ const token = inner.createCancelToken(timeoutMs);
848
+ const onAbort = () => inner.cancelQuery(token);
849
+ signal.addEventListener("abort", onAbort, { once: true });
850
+ try {
851
+ return await start(null, token);
852
+ }
853
+ catch (err) {
854
+ if (signal.aborted)
855
+ throw signal.reason;
856
+ throw err;
857
+ }
858
+ finally {
859
+ signal.removeEventListener("abort", onAbort);
860
+ inner.releaseCancelToken(token);
861
+ }
862
+ }
463
863
  /**
464
864
  * Create and initialize a new LoraDB instance.
465
865
  *
@@ -522,8 +922,8 @@ async function createDatabase(databaseName, options = {}) {
522
922
  const syncMode = options.syncMode ?? null;
523
923
  const groupSyncIntervalMs = options.groupSyncIntervalMs ?? null;
524
924
  return new DatabaseImpl(databaseName == null
525
- ? new NativeDatabase()
526
- : new NativeDatabase(databaseName, options.databaseDir ?? null, syncMode, groupSyncIntervalMs, null, null, null, null, null));
925
+ ? new NativeDatabase(null, null, null, null, null, null, null, null, null, options.queryTimeoutMs ?? null)
926
+ : new NativeDatabase(databaseName, options.databaseDir ?? null, syncMode, groupSyncIntervalMs, null, null, null, null, null, options.queryTimeoutMs ?? null));
527
927
  }
528
928
  catch (err) {
529
929
  throw (0, types_js_1.wrapError)(err);
@@ -550,4 +950,73 @@ async function openWalDatabase(options) {
550
950
  throw (0, types_js_1.wrapError)(err);
551
951
  }
552
952
  }
953
+ /**
954
+ * Prepare params for the native boundary. Params cross as JSON-shaped
955
+ * values, which cannot carry a `bigint`, so every bigint is rewritten to
956
+ * `{ kind: "integer", value: "<decimal>" }` and decoded exactly on the
957
+ * Rust side. Returns the input unchanged (no copy) when it holds no
958
+ * bigint, which is the common case.
959
+ */
960
+ function encodeParams(params) {
961
+ if (params == null)
962
+ return null;
963
+ return needsEncoding(params) ? encodeBigInts(params) : params;
964
+ }
965
+ /**
966
+ * An integer-valued `number` past 2^53 has usually already been rounded
967
+ * by JavaScript (an id or timestamp parsed from JSON, say). Storing it
968
+ * would make that corruption permanent, so refuse it and point at bigint.
969
+ * Magnitudes of 2^63 and up cannot be 64-bit integers and stay floats.
970
+ */
971
+ function checkNumber(value) {
972
+ if (Number.isInteger(value) &&
973
+ !Number.isSafeInteger(value) &&
974
+ Math.abs(value) < 2 ** 63) {
975
+ throw new types_js_1.LoraError(`integer ${value} is outside the JavaScript safe-integer range and may already be rounded; pass it as a bigint`, "LORA_INVALID_PARAMS");
976
+ }
977
+ }
978
+ function needsEncoding(value) {
979
+ if (typeof value === "bigint")
980
+ return true;
981
+ if (typeof value === "number") {
982
+ checkNumber(value);
983
+ return false;
984
+ }
985
+ if (value === null || typeof value !== "object")
986
+ return false;
987
+ if (ArrayBuffer.isView(value))
988
+ return false;
989
+ if (Array.isArray(value)) {
990
+ let found = false;
991
+ for (const item of value)
992
+ found = needsEncoding(item) || found;
993
+ return found;
994
+ }
995
+ let found = false;
996
+ for (const key in value) {
997
+ found = needsEncoding(value[key]) || found;
998
+ }
999
+ return found;
1000
+ }
1001
+ function encodeBigInts(value) {
1002
+ if (typeof value === "bigint")
1003
+ return { kind: "integer", value: value.toString() };
1004
+ if (value === null || typeof value !== "object" || ArrayBuffer.isView(value))
1005
+ return value;
1006
+ if (Array.isArray(value))
1007
+ return value.map(encodeBigInts);
1008
+ const out = {};
1009
+ for (const key in value)
1010
+ out[key] = encodeBigInts(value[key]);
1011
+ return out;
1012
+ }
1013
+ function normalizeTimeout(timeoutMs) {
1014
+ if (timeoutMs === undefined)
1015
+ return null;
1016
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
1017
+ throw new types_js_1.LoraError(`timeoutMs must be a non-negative finite number, got ${timeoutMs}`, "LORA_INVALID_PARAMS");
1018
+ }
1019
+ // The native side takes whole milliseconds as u32; 0 disables.
1020
+ return Math.min(Math.ceil(timeoutMs), 0xffff_ffff);
1021
+ }
553
1022
  //# sourceMappingURL=index.js.map