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/message.js ADDED
@@ -0,0 +1,97 @@
1
+ /*
2
+ Purpose: Define the canonical Node Message model shared by local and remote clients.
3
+ Key Exports: Message, parseMessage, messageFromEnvelope.
4
+ Role: Keep local/remote message shapes and parsing behavior identical.
5
+ Invariants: Message timestamps parse as valid UTC Date values.
6
+ Invariants: Message meta tags are normalized to string arrays.
7
+ Notes: Raw bytes are preserved when source buffers are available.
8
+ */
9
+
10
+ function normalizeMessageSeq(value) {
11
+ if (typeof value === "bigint") {
12
+ return value;
13
+ }
14
+ if (typeof value === "number" && Number.isFinite(value)) {
15
+ return BigInt(Math.trunc(value));
16
+ }
17
+ if (typeof value === "string" && value.length) {
18
+ return BigInt(value);
19
+ }
20
+ throw new TypeError("message seq must be numeric");
21
+ }
22
+
23
+ function normalizeMessageTags(meta) {
24
+ if (!meta || typeof meta !== "object" || !Array.isArray(meta.tags)) {
25
+ return Object.freeze([]);
26
+ }
27
+ return Object.freeze(meta.tags.map((tag) => String(tag)));
28
+ }
29
+
30
+ function serializeSeq(seq) {
31
+ const asNumber = Number(seq);
32
+ if (Number.isSafeInteger(asNumber)) {
33
+ return asNumber;
34
+ }
35
+ return seq.toString();
36
+ }
37
+
38
+ class Message {
39
+ constructor(envelope, raw = null) {
40
+ if (!envelope || typeof envelope !== "object") {
41
+ throw new TypeError("message envelope must be an object");
42
+ }
43
+ const seq = normalizeMessageSeq(envelope.seq);
44
+ const timeRfc3339 = String(envelope.time);
45
+ const time = new Date(timeRfc3339);
46
+ if (!Number.isFinite(time.getTime())) {
47
+ throw new TypeError("message time must be RFC3339");
48
+ }
49
+ const tags = normalizeMessageTags(envelope.meta);
50
+ this.seq = seq;
51
+ this.time = time;
52
+ this.timeRfc3339 = timeRfc3339;
53
+ this.data = envelope.data;
54
+ this.meta = Object.freeze({ tags });
55
+ this._raw = Buffer.isBuffer(raw) ? raw : null;
56
+ }
57
+
58
+ get tags() {
59
+ return this.meta.tags;
60
+ }
61
+
62
+ get raw() {
63
+ if (!this._raw) {
64
+ this._raw = Buffer.from(JSON.stringify({
65
+ seq: serializeSeq(this.seq),
66
+ time: this.timeRfc3339,
67
+ data: this.data,
68
+ meta: { tags: [...this.meta.tags] },
69
+ }));
70
+ }
71
+ return this._raw;
72
+ }
73
+ }
74
+
75
+ function messageFromEnvelope(envelope, raw = null) {
76
+ return new Message(envelope, raw);
77
+ }
78
+
79
+ function parseMessage(payload) {
80
+ if (payload instanceof Message) {
81
+ return payload;
82
+ }
83
+ if (Buffer.isBuffer(payload)) {
84
+ const parsed = JSON.parse(payload.toString("utf8"));
85
+ return messageFromEnvelope(parsed, payload);
86
+ }
87
+ if (payload && typeof payload === "object") {
88
+ return messageFromEnvelope(payload);
89
+ }
90
+ throw new TypeError("payload must be Buffer, Message, or message envelope object");
91
+ }
92
+
93
+ module.exports = {
94
+ Message,
95
+ messageFromEnvelope,
96
+ parseMessage,
97
+ };
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plasmite",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
4
4
  "description": "Persistent JSON message queues for Node.js - native bindings for local and remote IPC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -11,7 +11,8 @@
11
11
  },
12
12
  "files": [
13
13
  "index.js",
14
- "index.d.ts",
14
+ "message.js",
15
+ "mappings.js",
15
16
  "types.d.ts",
16
17
  "remote.js",
17
18
  "bin/plasmite.js",
@@ -38,7 +39,8 @@
38
39
  "scripts": {
39
40
  "prepare-native": "node scripts/prepare_native_assets.js",
40
41
  "build": "napi build --cargo-cwd native",
41
- "test": "npm run build && node --test test/*.test.js",
42
+ "check:type-surface": "node scripts/check_type_surface.js",
43
+ "test": "npm run build && npm run check:type-surface && node --test test/*.test.js",
42
44
  "typecheck": "tsc --noEmit -p tsconfig.json",
43
45
  "prepack": "npm run build && npm run prepare-native",
44
46
  "prepublishOnly": "npm run typecheck"
package/remote.js CHANGED
@@ -1,23 +1,31 @@
1
1
  /*
2
2
  Purpose: Provide an HTTP/JSON RemoteClient for the Node binding.
3
- Key Exports: RemoteClient, RemotePool, RemoteTail, RemoteError.
3
+ Key Exports: RemoteClient, RemotePool, RemoteError.
4
4
  Role: JS-side remote access that mirrors the v0 server protocol.
5
5
  Invariants: Uses JSON request/response envelopes from spec/remote/v0.
6
6
  Invariants: Base URL must be http(s) without a path.
7
- Invariants: Tail streams are JSONL and can be canceled.
7
+ Invariants: Tail streams are JSONL and exposed as async iterables.
8
8
  */
9
9
 
10
10
  const { Readable } = require("node:stream");
11
11
  const readline = require("node:readline");
12
+ const { messageFromEnvelope } = require("./message");
13
+ const { ERROR_KIND_VALUES, mapDurability, mapErrorKind } = require("./mappings");
12
14
 
13
15
  class RemoteError extends Error {
16
+ /**
17
+ * Build a structured remote error from an error envelope.
18
+ * @param {unknown} payload
19
+ * @param {number} status
20
+ * @returns {RemoteError}
21
+ */
14
22
  constructor(payload, status) {
15
23
  const error = payload && payload.error ? payload.error : payload;
16
24
  const message = error && error.message ? error.message : `Remote error ${status}`;
17
25
  super(message);
18
26
  this.name = "RemoteError";
19
27
  this.status = status;
20
- this.kind = error && error.kind ? error.kind : "Io";
28
+ this.kind = mapErrorKind(error && error.kind, ERROR_KIND_VALUES.Io);
21
29
  this.hint = error && error.hint ? error.hint : undefined;
22
30
  this.path = error && error.path ? error.path : undefined;
23
31
  this.seq = error && error.seq ? error.seq : undefined;
@@ -26,16 +34,33 @@ class RemoteError extends Error {
26
34
  }
27
35
 
28
36
  class RemoteClient {
37
+ /**
38
+ * Create a remote client bound to a base URL.
39
+ * @param {string} baseUrl
40
+ * @param {{token?: string}} [options]
41
+ * @returns {RemoteClient}
42
+ */
29
43
  constructor(baseUrl, options = {}) {
30
44
  this.baseUrl = normalizeBaseUrl(baseUrl);
31
45
  this.token = options.token || null;
32
46
  }
33
47
 
48
+ /**
49
+ * Set bearer token and return the same client.
50
+ * @param {string} token
51
+ * @returns {RemoteClient}
52
+ */
34
53
  withToken(token) {
35
54
  this.token = token;
36
55
  return this;
37
56
  }
38
57
 
58
+ /**
59
+ * Create a pool on the remote server.
60
+ * @param {string} pool
61
+ * @param {number|bigint} sizeBytes
62
+ * @returns {Promise<unknown>}
63
+ */
39
64
  async createPool(pool, sizeBytes) {
40
65
  const payload = { pool, size_bytes: Number(sizeBytes) };
41
66
  const url = buildUrl(this.baseUrl, ["v0", "pools"]);
@@ -43,6 +68,11 @@ class RemoteClient {
43
68
  return data.pool;
44
69
  }
45
70
 
71
+ /**
72
+ * Open a remote pool handle.
73
+ * @param {string} pool
74
+ * @returns {Promise<RemotePool>}
75
+ */
46
76
  async openPool(pool) {
47
77
  const payload = { pool };
48
78
  const url = buildUrl(this.baseUrl, ["v0", "pools", "open"]);
@@ -50,18 +80,32 @@ class RemoteClient {
50
80
  return new RemotePool(this, pool);
51
81
  }
52
82
 
83
+ /**
84
+ * Fetch pool metadata.
85
+ * @param {string} pool
86
+ * @returns {Promise<unknown>}
87
+ */
53
88
  async poolInfo(pool) {
54
89
  const url = buildUrl(this.baseUrl, ["v0", "pools", pool, "info"]);
55
90
  const data = await this._requestJson("GET", url, null);
56
91
  return data.pool;
57
92
  }
58
93
 
94
+ /**
95
+ * List pools on the remote server.
96
+ * @returns {Promise<unknown[]>}
97
+ */
59
98
  async listPools() {
60
99
  const url = buildUrl(this.baseUrl, ["v0", "pools"]);
61
100
  const data = await this._requestJson("GET", url, null);
62
101
  return data.pools;
63
102
  }
64
103
 
104
+ /**
105
+ * Delete a pool by name.
106
+ * @param {string} pool
107
+ * @returns {Promise<void>}
108
+ */
65
109
  async deletePool(pool) {
66
110
  const url = buildUrl(this.baseUrl, ["v0", "pools", pool]);
67
111
  await this._requestJson("DELETE", url, null);
@@ -115,22 +159,43 @@ class RemoteClient {
115
159
  }
116
160
 
117
161
  class RemotePool {
162
+ /**
163
+ * @param {RemoteClient} client
164
+ * @param {string} pool
165
+ * @returns {RemotePool}
166
+ */
118
167
  constructor(client, pool) {
119
168
  this.client = client;
120
169
  this.pool = pool;
121
170
  }
122
171
 
172
+ /**
173
+ * Return pool reference string.
174
+ * @returns {string}
175
+ */
123
176
  poolRef() {
124
177
  return this.pool;
125
178
  }
126
179
 
180
+ /**
181
+ * Append message data to the remote pool.
182
+ * @param {unknown} data
183
+ * @param {string[]} [tags]
184
+ * @param {number|string} [durability]
185
+ * @returns {Promise<import("./message").Message>}
186
+ */
127
187
  async append(data, tags = [], durability = "fast") {
128
- const payload = { data, tags, durability };
188
+ const payload = { data, tags, durability: mapDurability(durability) };
129
189
  const url = buildUrl(this.client.baseUrl, ["v0", "pools", this.pool, "append"]);
130
190
  const response = await this.client._requestJson("POST", url, payload);
131
- return response.message;
191
+ return messageFromEnvelope(response.message);
132
192
  }
133
193
 
194
+ /**
195
+ * Get message by sequence from remote pool.
196
+ * @param {number|bigint} seq
197
+ * @returns {Promise<import("./message").Message>}
198
+ */
134
199
  async get(seq) {
135
200
  const url = buildUrl(this.client.baseUrl, [
136
201
  "v0",
@@ -140,10 +205,15 @@ class RemotePool {
140
205
  String(seq),
141
206
  ]);
142
207
  const response = await this.client._requestJson("GET", url, null);
143
- return response.message;
208
+ return messageFromEnvelope(response.message);
144
209
  }
145
210
 
146
- async tail(options = {}) {
211
+ /**
212
+ * Tail remote messages as an async iterable.
213
+ * @param {{sinceSeq?: number|bigint, maxMessages?: number|bigint, timeoutMs?: number, tags?: string[]}} [options]
214
+ * @returns {AsyncGenerator<import("./message").Message, void, unknown>}
215
+ */
216
+ async *tail(options = {}) {
147
217
  const url = buildUrl(this.client.baseUrl, ["v0", "pools", this.pool, "tail"]);
148
218
  if (options.sinceSeq !== undefined) {
149
219
  url.searchParams.set("since_seq", String(options.sinceSeq));
@@ -163,44 +233,23 @@ class RemotePool {
163
233
 
164
234
  const controller = new AbortController();
165
235
  const response = await this.client._requestStream(url, controller);
166
- return new RemoteTail(response, controller);
167
- }
168
- }
169
-
170
- class RemoteTail {
171
- constructor(response, controller) {
172
236
  if (!response.body) {
173
237
  throw new Error("remote tail response has no body");
174
238
  }
175
- this.controller = controller;
176
239
  const stream = Readable.fromWeb(response.body);
177
- this.reader = readline.createInterface({ input: stream, crlfDelay: Infinity });
178
- this.iterator = this.reader[Symbol.asyncIterator]();
179
- this.done = false;
180
- }
181
-
182
- async next() {
183
- if (this.done) {
184
- return null;
185
- }
186
- const { value, done } = await this.iterator.next();
187
- if (done) {
188
- this.done = true;
189
- return null;
190
- }
191
- if (!value || !value.trim()) {
192
- return this.next();
193
- }
194
- return JSON.parse(value);
195
- }
196
-
197
- cancel() {
198
- if (this.done) {
199
- return;
240
+ const reader = readline.createInterface({ input: stream, crlfDelay: Infinity });
241
+ try {
242
+ for await (const line of reader) {
243
+ if (!line || !line.trim()) {
244
+ continue;
245
+ }
246
+ const raw = Buffer.from(line, "utf8");
247
+ yield messageFromEnvelope(JSON.parse(line), raw);
248
+ }
249
+ } finally {
250
+ controller.abort();
251
+ reader.close();
200
252
  }
201
- this.done = true;
202
- this.controller.abort();
203
- this.reader.close();
204
253
  }
205
254
  }
206
255
 
@@ -237,6 +286,5 @@ async function parseRemoteError(response) {
237
286
  module.exports = {
238
287
  RemoteClient,
239
288
  RemotePool,
240
- RemoteTail,
241
289
  RemoteError,
242
290
  };
package/types.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /*
2
2
  Purpose: Stable TypeScript declarations for the public Node bindings API.
3
- Key Exports: Client, Pool, Stream, Lite3Stream, replay, and remote client types.
3
+ Key Exports: Client, Pool, Stream, Lite3Stream, parseMessage, replay, and remote client types.
4
4
  Role: Preserve complete JS + native type surface independently of generated files.
5
5
  Invariants: Public runtime exports from index.js are represented here.
6
6
  Invariants: Numeric sequence fields accept number or bigint input.
7
- Notes: Kept separate from NAPI-generated index.d.ts to avoid regeneration loss.
7
+ Notes: Canonical declaration source for package typing (`package.json#types`).
8
8
  */
9
9
 
10
10
  export const enum Durability {
@@ -22,16 +22,43 @@ export const enum ErrorKind {
22
22
  Corrupt = 7,
23
23
  Io = 8,
24
24
  }
25
+ export const DEFAULT_POOL_DIR: string
26
+ export const DEFAULT_POOL_SIZE_BYTES: number
27
+ export const DEFAULT_POOL_SIZE: number
25
28
 
26
29
  export interface Lite3Frame {
27
30
  seq: bigint
28
31
  timestampNs: bigint
29
32
  flags: number
30
33
  payload: Buffer
34
+ time: Date
35
+ }
36
+
37
+ export interface MessageMeta {
38
+ tags: string[]
39
+ }
40
+
41
+ export class Message {
42
+ readonly seq: bigint
43
+ readonly time: Date
44
+ readonly timeRfc3339: string
45
+ readonly data: unknown
46
+ readonly meta: MessageMeta
47
+ readonly raw: Buffer
48
+ get tags(): string[]
49
+ }
50
+
51
+ export interface MessageEnvelope {
52
+ seq: number | bigint | string
53
+ time: string
54
+ data: unknown
55
+ meta?: {
56
+ tags?: unknown[]
57
+ }
31
58
  }
32
59
 
33
60
  export class PlasmiteNativeError extends Error {
34
- kind?: string
61
+ kind?: ErrorKind
35
62
  path?: string
36
63
  seq?: number
37
64
  offset?: number
@@ -39,17 +66,23 @@ export class PlasmiteNativeError extends Error {
39
66
  }
40
67
 
41
68
  export class Client {
42
- constructor(poolDir: string)
43
- createPool(poolRef: string, sizeBytes: number | bigint): Pool
69
+ constructor(poolDir?: string)
70
+ createPool(poolRef: string, sizeBytes?: number | bigint): Pool
44
71
  openPool(poolRef: string): Pool
72
+ pool(poolRef: string, sizeBytes?: number | bigint): Pool
45
73
  close(): void
74
+ [Symbol.dispose](): void
46
75
  }
47
76
 
48
77
  export class Pool {
49
- appendJson(payload: Buffer, tags: string[], durability: Durability): Buffer
50
- appendLite3(payload: Buffer, durability: Durability): bigint
78
+ append(payload: unknown, tags?: string[], durability?: Durability): Message
79
+ appendJson(payload: unknown, tags?: string[], durability?: Durability): Buffer
80
+ appendLite3(payload: Buffer, durability?: Durability): bigint
81
+ get(seq: number | bigint): Message
51
82
  getJson(seq: number | bigint): Buffer
52
83
  getLite3(seq: number | bigint): Lite3Frame
84
+ tail(options?: LocalTailOptions): AsyncGenerator<Message, void, unknown>
85
+ replay(options?: ReplayOptions): AsyncGenerator<Message, void, unknown>
53
86
  openStream(
54
87
  sinceSeq?: number | bigint | null,
55
88
  maxMessages?: number | bigint | null,
@@ -61,16 +94,21 @@ export class Pool {
61
94
  timeoutMs?: number | bigint | null,
62
95
  ): Lite3Stream
63
96
  close(): void
97
+ [Symbol.dispose](): void
64
98
  }
65
99
 
66
100
  export class Stream {
101
+ [Symbol.iterator](): Iterator<Buffer>
67
102
  nextJson(): Buffer | null
68
103
  close(): void
104
+ [Symbol.dispose](): void
69
105
  }
70
106
 
71
107
  export class Lite3Stream {
108
+ [Symbol.iterator](): Iterator<Lite3Frame>
72
109
  next(): Lite3Frame | null
73
110
  close(): void
111
+ [Symbol.dispose](): void
74
112
  }
75
113
 
76
114
  export interface ReplayOptions {
@@ -78,12 +116,22 @@ export interface ReplayOptions {
78
116
  sinceSeq?: number | bigint
79
117
  maxMessages?: number | bigint
80
118
  timeoutMs?: number | bigint
119
+ tags?: string[]
120
+ }
121
+
122
+ export interface LocalTailOptions {
123
+ sinceSeq?: number | bigint
124
+ maxMessages?: number | bigint
125
+ timeoutMs?: number | bigint
126
+ tags?: string[]
81
127
  }
82
128
 
129
+ export function parseMessage(payload: Buffer | Message | MessageEnvelope): Message
130
+
83
131
  export function replay(
84
132
  pool: Pool,
85
133
  options?: ReplayOptions,
86
- ): AsyncGenerator<Buffer, void, unknown>
134
+ ): AsyncGenerator<Message, void, unknown>
87
135
 
88
136
  export interface RemoteClientOptions {
89
137
  token?: string
@@ -102,15 +150,6 @@ export interface RemotePoolInfo {
102
150
  index_inline: boolean
103
151
  }
104
152
 
105
- export interface RemoteMessage {
106
- seq: number
107
- time: string
108
- data: unknown
109
- meta?: {
110
- tags?: string[]
111
- }
112
- }
113
-
114
153
  export interface RemoteTailOptions {
115
154
  sinceSeq?: number | bigint
116
155
  maxMessages?: number | bigint
@@ -119,7 +158,7 @@ export interface RemoteTailOptions {
119
158
 
120
159
  export class RemoteError extends Error {
121
160
  status: number
122
- kind: string
161
+ kind: ErrorKind
123
162
  hint?: string
124
163
  path?: string
125
164
  seq?: number
@@ -146,13 +185,8 @@ export class RemotePool {
146
185
  append(
147
186
  data: unknown,
148
187
  tags?: string[],
149
- durability?: "fast" | "flush",
150
- ): Promise<RemoteMessage>
151
- get(seq: number | bigint): Promise<RemoteMessage>
152
- tail(options?: RemoteTailOptions): Promise<RemoteTail>
153
- }
154
-
155
- export class RemoteTail {
156
- next(): Promise<RemoteMessage | null>
157
- cancel(): void
188
+ durability?: Durability,
189
+ ): Promise<Message>
190
+ get(seq: number | bigint): Promise<Message>
191
+ tail(options?: RemoteTailOptions): AsyncGenerator<Message, void, unknown>
158
192
  }
package/index.d.ts DELETED
@@ -1,48 +0,0 @@
1
- /* tslint:disable */
2
- /* eslint-disable */
3
-
4
- /* auto-generated by NAPI-RS */
5
-
6
- export const enum Durability {
7
- Fast = 0,
8
- Flush = 1
9
- }
10
- export const enum ErrorKind {
11
- Internal = 1,
12
- Usage = 2,
13
- NotFound = 3,
14
- AlreadyExists = 4,
15
- Busy = 5,
16
- Permission = 6,
17
- Corrupt = 7,
18
- Io = 8
19
- }
20
- export interface Lite3Frame {
21
- seq: bigint
22
- timestampNs: bigint
23
- flags: number
24
- payload: Buffer
25
- }
26
- export declare class Client {
27
- constructor(poolDir: string)
28
- createPool(poolRef: string, sizeBytes: number | bigint): Pool
29
- openPool(poolRef: string): Pool
30
- close(): void
31
- }
32
- export declare class Pool {
33
- appendJson(payload: Buffer, tags: Array<string>, durability: Durability): Buffer
34
- appendLite3(payload: Buffer, durability: Durability): bigint
35
- getJson(seq: number | bigint): Buffer
36
- getLite3(seq: number | bigint): Lite3Frame
37
- openStream(sinceSeq?: number | bigint | undefined | null, maxMessages?: number | bigint | undefined | null, timeoutMs?: number | bigint | undefined | null): Stream
38
- openLite3Stream(sinceSeq?: number | bigint | undefined | null, maxMessages?: number | bigint | undefined | null, timeoutMs?: number | bigint | undefined | null): Lite3Stream
39
- close(): void
40
- }
41
- export declare class Stream {
42
- nextJson(): Buffer | null
43
- close(): void
44
- }
45
- export declare class Lite3Stream {
46
- next(): Lite3Frame | null
47
- close(): void
48
- }