plasmite 0.3.0 → 0.5.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/index.js CHANGED
@@ -1,15 +1,18 @@
1
1
  /*
2
2
  Purpose: JavaScript entry point for the Plasmite Node binding.
3
- Key Exports: Client, Pool, Stream, Durability, ErrorKind, replay.
3
+ Key Exports: Client, Pool, Message, Stream, Durability, ErrorKind, replay.
4
4
  Role: Thin wrapper around the native N-API addon.
5
5
  Invariants: Exports align with native symbols and v0 API semantics.
6
6
  Notes: Requires libplasmite to be discoverable at runtime.
7
7
  */
8
8
 
9
- const { RemoteClient, RemoteError, RemotePool, RemoteTail } = require("./remote");
9
+ const { RemoteClient, RemoteError, RemotePool } = require("./remote");
10
+ const { Message, parseMessage } = require("./message");
11
+ const { ERROR_KIND_VALUES, mapErrorKind } = require("./mappings");
10
12
 
11
13
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
12
14
  const path = require("node:path");
15
+ const os = require("node:os");
13
16
 
14
17
  const PLATFORM_DIRS = Object.freeze({
15
18
  linux: Object.freeze({ x64: "linux-x64", arm64: "linux-arm64" }),
@@ -33,6 +36,9 @@ function resolveNativeAddonPath() {
33
36
  return path.join(__dirname, "native", platformDir, "index.node");
34
37
  }
35
38
 
39
+ const DEFAULT_POOL_DIR = path.join(os.homedir(), ".plasmite", "pools");
40
+ const DEFAULT_POOL_SIZE = 1024 * 1024;
41
+ const DEFAULT_POOL_SIZE_BYTES = DEFAULT_POOL_SIZE;
36
42
  let native = null;
37
43
  let nativeLoadError = null;
38
44
  let nativeAddonPath = null;
@@ -48,6 +54,9 @@ try {
48
54
  nativeLoadError = err;
49
55
  }
50
56
 
57
+ const Durability = native ? native.Durability : Object.freeze({ Fast: 0, Flush: 1 });
58
+ const ErrorKind = native ? native.ErrorKind : ERROR_KIND_VALUES;
59
+
51
60
  function makeNativeUnavailableError() {
52
61
  const reason = nativeLoadError instanceof Error ? nativeLoadError.message : "unknown load error";
53
62
  return new Error(
@@ -97,8 +106,15 @@ function parseNativeError(err) {
97
106
  details[key] = Number.isFinite(parsed) ? parsed : undefined;
98
107
  continue;
99
108
  }
109
+ if (key === "kind") {
110
+ details.kind = mapErrorKind(value);
111
+ continue;
112
+ }
100
113
  details[key] = value;
101
114
  }
115
+ if (details.kind === undefined) {
116
+ details.kind = ErrorKind.Io;
117
+ }
102
118
  return new PlasmiteNativeError(err.message, details, err);
103
119
  }
104
120
 
@@ -106,15 +122,33 @@ function wrapNativeError(err) {
106
122
  return parseNativeError(err) ?? err;
107
123
  }
108
124
 
125
+ function isNativeNotFoundError(err) {
126
+ if (!(err instanceof PlasmiteNativeError)) {
127
+ return false;
128
+ }
129
+ return err.kind === ErrorKind.NotFound;
130
+ }
131
+
109
132
  class Client {
110
- constructor(poolDir) {
133
+ /**
134
+ * Create a local client bound to a pool directory.
135
+ * @param {string} [poolDir]
136
+ * @returns {Client}
137
+ */
138
+ constructor(poolDir = DEFAULT_POOL_DIR) {
111
139
  if (!native) {
112
140
  throw makeNativeUnavailableError();
113
141
  }
114
142
  this._inner = new native.Client(poolDir);
115
143
  }
116
144
 
117
- createPool(poolRef, sizeBytes) {
145
+ /**
146
+ * Create a new pool.
147
+ * @param {string} poolRef
148
+ * @param {number|bigint} [sizeBytes]
149
+ * @returns {Pool}
150
+ */
151
+ createPool(poolRef, sizeBytes = DEFAULT_POOL_SIZE_BYTES) {
118
152
  try {
119
153
  return new Pool(this._inner.createPool(poolRef, sizeBytes));
120
154
  } catch (err) {
@@ -122,6 +156,11 @@ class Client {
122
156
  }
123
157
  }
124
158
 
159
+ /**
160
+ * Open an existing pool.
161
+ * @param {string} poolRef
162
+ * @returns {Pool}
163
+ */
125
164
  openPool(poolRef) {
126
165
  try {
127
166
  return new Pool(this._inner.openPool(poolRef));
@@ -130,12 +169,41 @@ class Client {
130
169
  }
131
170
  }
132
171
 
172
+ /**
173
+ * Open a pool if it exists, otherwise create it.
174
+ * @param {string} poolRef
175
+ * @param {number|bigint} [sizeBytes]
176
+ * @returns {Pool}
177
+ */
178
+ pool(poolRef, sizeBytes = DEFAULT_POOL_SIZE_BYTES) {
179
+ try {
180
+ return this.openPool(poolRef);
181
+ } catch (err) {
182
+ if (isNativeNotFoundError(err)) {
183
+ return this.createPool(poolRef, sizeBytes);
184
+ }
185
+ throw err;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Close the client handle.
191
+ * @returns {void}
192
+ */
133
193
  close() {
134
194
  this._inner.close();
135
195
  }
196
+
197
+ [Symbol.dispose]() {
198
+ this.close();
199
+ }
136
200
  }
137
201
 
138
202
  class Pool {
203
+ /**
204
+ * @param {object} inner
205
+ * @returns {Pool}
206
+ */
139
207
  constructor(inner) {
140
208
  if (!native) {
141
209
  throw makeNativeUnavailableError();
@@ -143,22 +211,54 @@ class Pool {
143
211
  this._inner = inner;
144
212
  }
145
213
 
214
+ /**
215
+ * Append JSON bytes and return raw message bytes.
216
+ * @param {unknown} payload
217
+ * @param {string[]} [tags]
218
+ * @param {number} [durability]
219
+ * @returns {Buffer}
220
+ */
146
221
  appendJson(payload, tags, durability) {
222
+ const input = Buffer.isBuffer(payload)
223
+ ? payload
224
+ : Buffer.from(JSON.stringify(payload));
147
225
  try {
148
- return this._inner.appendJson(payload, tags, durability);
226
+ return this._inner.appendJson(input, tags ?? [], durability ?? Durability.Fast);
149
227
  } catch (err) {
150
228
  throw wrapNativeError(err);
151
229
  }
152
230
  }
153
231
 
232
+ /**
233
+ * Append payload and return parsed Message.
234
+ * @param {unknown} payload
235
+ * @param {string[]} [tags]
236
+ * @param {number} [durability]
237
+ * @returns {Message}
238
+ */
239
+ append(payload, tags, durability) {
240
+ return parseMessage(this.appendJson(payload, tags, durability));
241
+ }
242
+
243
+ /**
244
+ * Append a Lite3 frame payload.
245
+ * @param {Buffer} payload
246
+ * @param {number} [durability]
247
+ * @returns {bigint}
248
+ */
154
249
  appendLite3(payload, durability) {
155
250
  try {
156
- return this._inner.appendLite3(payload, durability);
251
+ return this._inner.appendLite3(payload, durability ?? Durability.Fast);
157
252
  } catch (err) {
158
253
  throw wrapNativeError(err);
159
254
  }
160
255
  }
161
256
 
257
+ /**
258
+ * Get raw message bytes by sequence.
259
+ * @param {number|bigint} seq
260
+ * @returns {Buffer}
261
+ */
162
262
  getJson(seq) {
163
263
  try {
164
264
  return this._inner.getJson(seq);
@@ -167,14 +267,35 @@ class Pool {
167
267
  }
168
268
  }
169
269
 
270
+ /**
271
+ * Get parsed Message by sequence.
272
+ * @param {number|bigint} seq
273
+ * @returns {Message}
274
+ */
275
+ get(seq) {
276
+ return parseMessage(this.getJson(seq));
277
+ }
278
+
279
+ /**
280
+ * Get Lite3 frame by sequence.
281
+ * @param {number|bigint} seq
282
+ * @returns {import("./types").Lite3Frame}
283
+ */
170
284
  getLite3(seq) {
171
285
  try {
172
- return this._inner.getLite3(seq);
286
+ return decorateLite3Frame(this._inner.getLite3(seq));
173
287
  } catch (err) {
174
288
  throw wrapNativeError(err);
175
289
  }
176
290
  }
177
291
 
292
+ /**
293
+ * Open a raw JSON stream.
294
+ * @param {number|bigint|null} sinceSeq
295
+ * @param {number|bigint|null} maxMessages
296
+ * @param {number|bigint|null} timeoutMs
297
+ * @returns {Stream}
298
+ */
178
299
  openStream(sinceSeq, maxMessages, timeoutMs) {
179
300
  try {
180
301
  return new Stream(this._inner.openStream(sinceSeq, maxMessages, timeoutMs));
@@ -183,6 +304,13 @@ class Pool {
183
304
  }
184
305
  }
185
306
 
307
+ /**
308
+ * Open a raw Lite3 stream.
309
+ * @param {number|bigint|null} sinceSeq
310
+ * @param {number|bigint|null} maxMessages
311
+ * @param {number|bigint|null} timeoutMs
312
+ * @returns {Lite3Stream}
313
+ */
186
314
  openLite3Stream(sinceSeq, maxMessages, timeoutMs) {
187
315
  try {
188
316
  return new Lite3Stream(
@@ -193,12 +321,217 @@ class Pool {
193
321
  }
194
322
  }
195
323
 
324
+ /**
325
+ * Tail parsed messages with optional filtering.
326
+ * @param {{sinceSeq?: number|bigint, maxMessages?: number|bigint, timeoutMs?: number|bigint, tags?: string[]}} [options]
327
+ * @returns {AsyncGenerator<Message, void, unknown>}
328
+ */
329
+ async *tail(options = {}) {
330
+ const { sinceSeq, maxMessages, timeoutMs, tags } = options;
331
+ const requiredTags = normalizeTagFilter(tags);
332
+ const limit = normalizeOptionalCount(maxMessages, "maxMessages");
333
+ const pollTimeoutMs = normalizePollingTimeout(timeoutMs);
334
+
335
+ let delivered = 0;
336
+ let cursor = sinceSeq ?? null;
337
+ while (true) {
338
+ if (limit !== null && delivered >= limit) {
339
+ return;
340
+ }
341
+
342
+ const remaining = limit === null ? null : limit - delivered;
343
+ const streamLimit = requiredTags.length && remaining !== null ? null : remaining;
344
+ const stream = this.openStream(cursor, streamLimit, pollTimeoutMs);
345
+ let sawRawMessage = false;
346
+ try {
347
+ for (const message of stream) {
348
+ sawRawMessage = true;
349
+ const parsed = parseMessage(message);
350
+ cursor = nextSinceSeq(parsed, cursor);
351
+ if (!messageHasTags(parsed, requiredTags)) {
352
+ continue;
353
+ }
354
+ delivered += 1;
355
+ yield parsed;
356
+ if (limit !== null && delivered >= limit) {
357
+ return;
358
+ }
359
+ }
360
+ } finally {
361
+ stream.close();
362
+ }
363
+ if (limit !== null && !sawRawMessage) {
364
+ return;
365
+ }
366
+ await sleep(0);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Replay parsed messages with original timing.
372
+ * @param {{speed?: number, sinceSeq?: number|bigint, maxMessages?: number|bigint, timeoutMs?: number|bigint, tags?: string[]}} [options]
373
+ * @returns {AsyncGenerator<Message, void, unknown>}
374
+ */
375
+ async *replay(options = {}) {
376
+ const { speed = 1.0, sinceSeq, maxMessages, timeoutMs, tags } = options;
377
+ if (speed <= 0) {
378
+ throw new Error("speed must be positive");
379
+ }
380
+
381
+ const requiredTags = normalizeTagFilter(tags);
382
+ const limit = normalizeOptionalCount(maxMessages, "maxMessages");
383
+ const pollTimeoutMs = normalizePollingTimeout(timeoutMs);
384
+ const streamLimit = requiredTags.length && limit !== null ? null : limit;
385
+ const stream = this.openStream(
386
+ sinceSeq ?? null,
387
+ streamLimit,
388
+ pollTimeoutMs,
389
+ );
390
+
391
+ const messages = [];
392
+ try {
393
+ for (const message of stream) {
394
+ const parsed = parseMessage(message);
395
+ if (!messageHasTags(parsed, requiredTags)) {
396
+ continue;
397
+ }
398
+ messages.push({
399
+ message: parsed,
400
+ timeMs: messageTimeMs(parsed),
401
+ });
402
+ if (limit !== null && messages.length >= limit) {
403
+ break;
404
+ }
405
+ }
406
+ } finally {
407
+ stream.close();
408
+ }
409
+
410
+ let prevMs = null;
411
+ for (const entry of messages) {
412
+ if (prevMs !== null && entry.timeMs !== null && speed > 0) {
413
+ const delay = (entry.timeMs - prevMs) / speed;
414
+ if (delay > 0) {
415
+ await sleep(delay);
416
+ }
417
+ }
418
+ if (entry.timeMs !== null) {
419
+ prevMs = entry.timeMs;
420
+ }
421
+ yield entry.message;
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Close the pool handle.
427
+ * @returns {void}
428
+ */
196
429
  close() {
197
430
  this._inner.close();
198
431
  }
432
+
433
+ [Symbol.dispose]() {
434
+ this.close();
435
+ }
436
+ }
437
+
438
+ function normalizeTagFilter(tags) {
439
+ if (tags === undefined || tags === null) {
440
+ return [];
441
+ }
442
+ return Array.isArray(tags) ? tags : [tags];
443
+ }
444
+
445
+ function messageHasTags(message, requiredTags) {
446
+ if (!requiredTags.length) {
447
+ return true;
448
+ }
449
+ const messageTags = message && message.meta && Array.isArray(message.meta.tags)
450
+ ? message.meta.tags
451
+ : null;
452
+ if (!messageTags) {
453
+ return false;
454
+ }
455
+ return requiredTags.every((tag) => messageTags.includes(tag));
456
+ }
457
+
458
+ function messageTimeMs(message) {
459
+ if (!message || !(message.time instanceof Date)) {
460
+ return null;
461
+ }
462
+ const value = message.time.getTime();
463
+ return Number.isFinite(value) ? value : null;
464
+ }
465
+
466
+ function nextSinceSeq(message, fallback) {
467
+ if (!message) {
468
+ return fallback;
469
+ }
470
+ if (typeof message.seq === "bigint") {
471
+ return message.seq + 1n;
472
+ }
473
+ if (typeof message.seq === "number" && Number.isFinite(message.seq)) {
474
+ return message.seq + 1;
475
+ }
476
+ return fallback;
477
+ }
478
+
479
+ function decorateLite3Frame(frame) {
480
+ if (!frame || typeof frame !== "object") {
481
+ return frame;
482
+ }
483
+ if (!Object.prototype.hasOwnProperty.call(frame, "time")) {
484
+ Object.defineProperty(frame, "time", {
485
+ configurable: false,
486
+ enumerable: true,
487
+ get() {
488
+ return new Date(Number(this.timestampNs) / 1_000_000);
489
+ },
490
+ });
491
+ }
492
+ return frame;
493
+ }
494
+
495
+ function normalizeOptionalCount(value, fieldName) {
496
+ if (value === undefined || value === null) {
497
+ return null;
498
+ }
499
+ if (typeof value === "bigint") {
500
+ if (value < 0n) {
501
+ throw new TypeError(`${fieldName} must be non-negative`);
502
+ }
503
+ return Number(value);
504
+ }
505
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
506
+ throw new TypeError(`${fieldName} must be non-negative`);
507
+ }
508
+ return Math.floor(value);
509
+ }
510
+
511
+ function normalizePollingTimeout(timeoutMs) {
512
+ if (timeoutMs === undefined || timeoutMs === null) {
513
+ return 1000;
514
+ }
515
+ if (typeof timeoutMs === "bigint") {
516
+ if (timeoutMs <= 0n) {
517
+ return 1000;
518
+ }
519
+ return Number(timeoutMs);
520
+ }
521
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs)) {
522
+ throw new TypeError("timeoutMs must be numeric");
523
+ }
524
+ if (timeoutMs <= 0) {
525
+ return 1000;
526
+ }
527
+ return timeoutMs;
199
528
  }
200
529
 
201
530
  class Stream {
531
+ /**
532
+ * @param {object} inner
533
+ * @returns {Stream}
534
+ */
202
535
  constructor(inner) {
203
536
  if (!native) {
204
537
  throw makeNativeUnavailableError();
@@ -206,6 +539,10 @@ class Stream {
206
539
  this._inner = inner;
207
540
  }
208
541
 
542
+ /**
543
+ * Read the next raw JSON message.
544
+ * @returns {Buffer|null}
545
+ */
209
546
  nextJson() {
210
547
  try {
211
548
  return this._inner.nextJson();
@@ -214,12 +551,35 @@ class Stream {
214
551
  }
215
552
  }
216
553
 
554
+ /**
555
+ * Iterate raw JSON messages.
556
+ * @returns {Iterator<Buffer>}
557
+ */
558
+ *[Symbol.iterator]() {
559
+ let message;
560
+ while ((message = this.nextJson()) !== null) {
561
+ yield message;
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Close the stream handle.
567
+ * @returns {void}
568
+ */
217
569
  close() {
218
570
  this._inner.close();
219
571
  }
572
+
573
+ [Symbol.dispose]() {
574
+ this.close();
575
+ }
220
576
  }
221
577
 
222
578
  class Lite3Stream {
579
+ /**
580
+ * @param {object} inner
581
+ * @returns {Lite3Stream}
582
+ */
223
583
  constructor(inner) {
224
584
  if (!native) {
225
585
  throw makeNativeUnavailableError();
@@ -227,52 +587,50 @@ class Lite3Stream {
227
587
  this._inner = inner;
228
588
  }
229
589
 
590
+ /**
591
+ * Read the next Lite3 frame.
592
+ * @returns {import("./types").Lite3Frame|null}
593
+ */
230
594
  next() {
231
595
  try {
232
- return this._inner.next();
596
+ return decorateLite3Frame(this._inner.next());
233
597
  } catch (err) {
234
598
  throw wrapNativeError(err);
235
599
  }
236
600
  }
237
601
 
602
+ /**
603
+ * Iterate Lite3 frames.
604
+ * @returns {Iterator<import("./types").Lite3Frame>}
605
+ */
606
+ *[Symbol.iterator]() {
607
+ let frame;
608
+ while ((frame = this.next()) !== null) {
609
+ yield frame;
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Close the Lite3 stream handle.
615
+ * @returns {void}
616
+ */
238
617
  close() {
239
618
  this._inner.close();
240
619
  }
241
- }
242
620
 
243
- async function* replay(pool, options = {}) {
244
- const { speed = 1.0, sinceSeq, maxMessages, timeoutMs } = options;
245
- const stream = pool.openStream(
246
- sinceSeq ?? null,
247
- maxMessages ?? null,
248
- timeoutMs ?? null,
249
- );
250
-
251
- const messages = [];
252
- try {
253
- let msg;
254
- while ((msg = stream.nextJson()) !== null) {
255
- messages.push(msg);
256
- }
257
- } finally {
258
- stream.close();
621
+ [Symbol.dispose]() {
622
+ this.close();
259
623
  }
624
+ }
260
625
 
261
- let prevMs = null;
262
- for (const msg of messages) {
263
- const parsed = JSON.parse(msg);
264
- const curMs = new Date(parsed.time).getTime();
265
-
266
- if (prevMs !== null && speed > 0) {
267
- const delay = (curMs - prevMs) / speed;
268
- if (delay > 0) {
269
- await sleep(delay);
270
- }
271
- }
272
-
273
- prevMs = curMs;
274
- yield msg;
275
- }
626
+ /**
627
+ * Backward-compatible replay helper.
628
+ * @param {Pool} pool
629
+ * @param {object} [options]
630
+ * @returns {AsyncGenerator<Message, void, unknown>}
631
+ */
632
+ async function* replay(pool, options = {}) {
633
+ yield* pool.replay(options);
276
634
  }
277
635
 
278
636
  module.exports = {
@@ -280,12 +638,16 @@ module.exports = {
280
638
  Pool,
281
639
  Stream,
282
640
  Lite3Stream,
283
- Durability: native ? native.Durability : Object.freeze({}),
284
- ErrorKind: native ? native.ErrorKind : Object.freeze({}),
641
+ Message,
642
+ DEFAULT_POOL_DIR,
643
+ DEFAULT_POOL_SIZE,
644
+ DEFAULT_POOL_SIZE_BYTES,
645
+ Durability,
646
+ ErrorKind,
285
647
  PlasmiteNativeError,
286
648
  RemoteClient,
287
649
  RemoteError,
288
650
  RemotePool,
289
- RemoteTail,
651
+ parseMessage,
290
652
  replay,
291
653
  };
package/mappings.js ADDED
@@ -0,0 +1,55 @@
1
+ /*
2
+ Purpose: Centralize Node binding value mappings shared across local and remote paths.
3
+ Key Exports: ERROR_KIND_VALUES, mapErrorKind, mapDurability.
4
+ Role: Keep error-kind and durability normalization behavior consistent.
5
+ Invariants: Error kind names map to stable numeric values for v0 semantics.
6
+ Invariants: Durability accepts fast/flush and numeric enum aliases 0/1.
7
+ */
8
+
9
+ const ERROR_KIND_VALUES = Object.freeze({
10
+ Internal: 1,
11
+ Usage: 2,
12
+ NotFound: 3,
13
+ AlreadyExists: 4,
14
+ Busy: 5,
15
+ Permission: 6,
16
+ Corrupt: 7,
17
+ Io: 8,
18
+ });
19
+
20
+ const DURABILITY_VALUES = Object.freeze({
21
+ fast: "fast",
22
+ flush: "flush",
23
+ 0: "fast",
24
+ 1: "flush",
25
+ });
26
+
27
+ function mapErrorKind(value, fallback = undefined) {
28
+ if (typeof value === "number" && Number.isFinite(value)) {
29
+ return value;
30
+ }
31
+ if (typeof value === "string") {
32
+ const mapped = ERROR_KIND_VALUES[value];
33
+ if (mapped !== undefined) {
34
+ return mapped;
35
+ }
36
+ }
37
+ return fallback;
38
+ }
39
+
40
+ function mapDurability(value) {
41
+ if (value === undefined || value === null) {
42
+ return "fast";
43
+ }
44
+ const mapped = DURABILITY_VALUES[String(value).toLowerCase()];
45
+ if (mapped) {
46
+ return mapped;
47
+ }
48
+ throw new TypeError("durability must be Durability.Fast or Durability.Flush");
49
+ }
50
+
51
+ module.exports = {
52
+ ERROR_KIND_VALUES,
53
+ mapErrorKind,
54
+ mapDurability,
55
+ };