plasmite 0.1.8
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/README.md +92 -0
- package/bin/plasmite.js +24 -0
- package/index.d.ts +48 -0
- package/index.js +233 -0
- package/index.node +0 -0
- package/libplasmite.so +0 -0
- package/package.json +52 -0
- package/plasmite +0 -0
- package/remote.js +242 -0
- package/types.d.ts +158 -0
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Plasmite Node Bindings (v0)
|
|
2
|
+
|
|
3
|
+
These bindings wrap the `libplasmite` C ABI via a N-API addon.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Install from npm (canonical package name):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install plasmite
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Published package artifacts bundle:
|
|
14
|
+
- `index.node` N-API addon
|
|
15
|
+
- `libplasmite.(dylib|so)` beside the addon
|
|
16
|
+
- `plasmite` CLI binary exposed as `npx plasmite`
|
|
17
|
+
|
|
18
|
+
For development/testing from this repo, see Build & Test below.
|
|
19
|
+
|
|
20
|
+
## Build Requirements
|
|
21
|
+
|
|
22
|
+
- Node 20+
|
|
23
|
+
- Rust toolchain (for building the addon)
|
|
24
|
+
- `libplasmite` built from this repo (`cargo build -p plasmite`)
|
|
25
|
+
|
|
26
|
+
## Build & Test
|
|
27
|
+
|
|
28
|
+
From the repo root:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cargo build -p plasmite
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Canonical repo-root command:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
just bindings-node-test
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Equivalent manual command (from `bindings/node`):
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
PLASMITE_LIB_DIR="$(pwd)/../../target/debug" npm test
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Pack/install smoke (ensures bundled assets work without env vars):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
./scripts/node_pack_smoke.sh
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
const { Client, Durability } = require("plasmite")
|
|
56
|
+
|
|
57
|
+
const client = new Client("./data")
|
|
58
|
+
const pool = client.createPool("docs", 64 * 1024 * 1024)
|
|
59
|
+
const payload = Buffer.from(JSON.stringify({ kind: "note", text: "hi" }))
|
|
60
|
+
const message = pool.appendJson(payload, ["note"], Durability.Fast)
|
|
61
|
+
console.log(message.toString("utf8"))
|
|
62
|
+
|
|
63
|
+
const frame = pool.getLite3(BigInt(1))
|
|
64
|
+
console.log(frame.payload.length)
|
|
65
|
+
|
|
66
|
+
pool.close()
|
|
67
|
+
client.close()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Local binding failures throw `PlasmiteNativeError` with structured metadata fields (`kind`, `path`, `seq`, `offset`) when available.
|
|
71
|
+
|
|
72
|
+
## Remote Client (HTTP/JSON)
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
const { RemoteClient } = require("plasmite")
|
|
76
|
+
|
|
77
|
+
const client = new RemoteClient("http://127.0.0.1:9700")
|
|
78
|
+
const pool = await client.openPool("docs")
|
|
79
|
+
const message = await pool.append({ kind: "note", text: "hi" }, ["note"])
|
|
80
|
+
console.log(message.seq, message.data)
|
|
81
|
+
|
|
82
|
+
const tail = await pool.tail({
|
|
83
|
+
sinceSeq: message.seq,
|
|
84
|
+
tags: ["note"],
|
|
85
|
+
maxMessages: 1,
|
|
86
|
+
timeoutMs: 500,
|
|
87
|
+
})
|
|
88
|
+
console.log(await tail.next())
|
|
89
|
+
tail.cancel()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`tail({ tags: [...] })` performs exact tag matching and composes with other filters via AND semantics.
|
package/bin/plasmite.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
Purpose: Provide an npm bin entrypoint that runs the bundled plasmite CLI.
|
|
4
|
+
Key Exports: CLI entrypoint only.
|
|
5
|
+
Role: Ensure `npx plasmite` works from npm-installed package artifacts.
|
|
6
|
+
Invariants: Uses packaged binary first, then falls back to PATH.
|
|
7
|
+
Invariants: Forwards argv verbatim and propagates child exit status.
|
|
8
|
+
Notes: Prints a concise error when no CLI binary is available.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { spawnSync } = require("node:child_process");
|
|
12
|
+
const fs = require("node:fs");
|
|
13
|
+
const path = require("node:path");
|
|
14
|
+
|
|
15
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
16
|
+
const bundled = path.join(packageRoot, "plasmite");
|
|
17
|
+
const target = fs.existsSync(bundled) ? bundled : "plasmite";
|
|
18
|
+
|
|
19
|
+
const result = spawnSync(target, process.argv.slice(2), { stdio: "inherit" });
|
|
20
|
+
if (result.error) {
|
|
21
|
+
console.error(`plasmite: failed to execute ${target}: ${result.error.message}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
process.exit(result.status ?? 1);
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Purpose: JavaScript entry point for the Plasmite Node binding.
|
|
3
|
+
Key Exports: Client, Pool, Stream, Durability, ErrorKind, replay.
|
|
4
|
+
Role: Thin wrapper around the native N-API addon.
|
|
5
|
+
Invariants: Exports align with native symbols and v0 API semantics.
|
|
6
|
+
Notes: Requires libplasmite to be discoverable at runtime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const native = require("./index.node");
|
|
10
|
+
const { RemoteClient, RemoteError, RemotePool, RemoteTail } = require("./remote");
|
|
11
|
+
|
|
12
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
13
|
+
|
|
14
|
+
class PlasmiteNativeError extends Error {
|
|
15
|
+
constructor(message, details = {}, cause = undefined) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "PlasmiteNativeError";
|
|
18
|
+
this.kind = details.kind;
|
|
19
|
+
this.path = details.path;
|
|
20
|
+
this.seq = details.seq;
|
|
21
|
+
this.offset = details.offset;
|
|
22
|
+
this.cause = cause;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseNativeError(err) {
|
|
27
|
+
if (!(err instanceof Error) || typeof err.message !== "string") {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const prefix = "plasmite error:";
|
|
31
|
+
if (!err.message.startsWith(prefix)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const parts = err.message
|
|
35
|
+
.slice(prefix.length)
|
|
36
|
+
.split(";")
|
|
37
|
+
.map((part) => part.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
if (parts.length === 0) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const details = {};
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
const [key, ...valueParts] = part.split("=");
|
|
45
|
+
if (!key || valueParts.length === 0) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const value = valueParts.join("=");
|
|
49
|
+
if (key === "seq" || key === "offset") {
|
|
50
|
+
const parsed = Number(value);
|
|
51
|
+
details[key] = Number.isFinite(parsed) ? parsed : undefined;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
details[key] = value;
|
|
55
|
+
}
|
|
56
|
+
return new PlasmiteNativeError(err.message, details, err);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function wrapNativeError(err) {
|
|
60
|
+
return parseNativeError(err) ?? err;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class Client {
|
|
64
|
+
constructor(poolDir) {
|
|
65
|
+
this._inner = new native.Client(poolDir);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
createPool(poolRef, sizeBytes) {
|
|
69
|
+
try {
|
|
70
|
+
return new Pool(this._inner.createPool(poolRef, sizeBytes));
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw wrapNativeError(err);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
openPool(poolRef) {
|
|
77
|
+
try {
|
|
78
|
+
return new Pool(this._inner.openPool(poolRef));
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw wrapNativeError(err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
close() {
|
|
85
|
+
this._inner.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
class Pool {
|
|
90
|
+
constructor(inner) {
|
|
91
|
+
this._inner = inner;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
appendJson(payload, tags, durability) {
|
|
95
|
+
try {
|
|
96
|
+
return this._inner.appendJson(payload, tags, durability);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw wrapNativeError(err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
appendLite3(payload, durability) {
|
|
103
|
+
try {
|
|
104
|
+
return this._inner.appendLite3(payload, durability);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
throw wrapNativeError(err);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getJson(seq) {
|
|
111
|
+
try {
|
|
112
|
+
return this._inner.getJson(seq);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
throw wrapNativeError(err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
getLite3(seq) {
|
|
119
|
+
try {
|
|
120
|
+
return this._inner.getLite3(seq);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
throw wrapNativeError(err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
openStream(sinceSeq, maxMessages, timeoutMs) {
|
|
127
|
+
try {
|
|
128
|
+
return new Stream(this._inner.openStream(sinceSeq, maxMessages, timeoutMs));
|
|
129
|
+
} catch (err) {
|
|
130
|
+
throw wrapNativeError(err);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
openLite3Stream(sinceSeq, maxMessages, timeoutMs) {
|
|
135
|
+
try {
|
|
136
|
+
return new Lite3Stream(
|
|
137
|
+
this._inner.openLite3Stream(sinceSeq, maxMessages, timeoutMs),
|
|
138
|
+
);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
throw wrapNativeError(err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
close() {
|
|
145
|
+
this._inner.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
class Stream {
|
|
150
|
+
constructor(inner) {
|
|
151
|
+
this._inner = inner;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
nextJson() {
|
|
155
|
+
try {
|
|
156
|
+
return this._inner.nextJson();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
throw wrapNativeError(err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
close() {
|
|
163
|
+
this._inner.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
class Lite3Stream {
|
|
168
|
+
constructor(inner) {
|
|
169
|
+
this._inner = inner;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
next() {
|
|
173
|
+
try {
|
|
174
|
+
return this._inner.next();
|
|
175
|
+
} catch (err) {
|
|
176
|
+
throw wrapNativeError(err);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
close() {
|
|
181
|
+
this._inner.close();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function* replay(pool, options = {}) {
|
|
186
|
+
const { speed = 1.0, sinceSeq, maxMessages, timeoutMs } = options;
|
|
187
|
+
const stream = pool.openStream(
|
|
188
|
+
sinceSeq ?? null,
|
|
189
|
+
maxMessages ?? null,
|
|
190
|
+
timeoutMs ?? null,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const messages = [];
|
|
194
|
+
try {
|
|
195
|
+
let msg;
|
|
196
|
+
while ((msg = stream.nextJson()) !== null) {
|
|
197
|
+
messages.push(msg);
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
stream.close();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let prevMs = null;
|
|
204
|
+
for (const msg of messages) {
|
|
205
|
+
const parsed = JSON.parse(msg);
|
|
206
|
+
const curMs = new Date(parsed.time).getTime();
|
|
207
|
+
|
|
208
|
+
if (prevMs !== null && speed > 0) {
|
|
209
|
+
const delay = (curMs - prevMs) / speed;
|
|
210
|
+
if (delay > 0) {
|
|
211
|
+
await sleep(delay);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
prevMs = curMs;
|
|
216
|
+
yield msg;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
Client,
|
|
222
|
+
Pool,
|
|
223
|
+
Stream,
|
|
224
|
+
Lite3Stream,
|
|
225
|
+
Durability: native.Durability,
|
|
226
|
+
ErrorKind: native.ErrorKind,
|
|
227
|
+
PlasmiteNativeError,
|
|
228
|
+
RemoteClient,
|
|
229
|
+
RemoteError,
|
|
230
|
+
RemotePool,
|
|
231
|
+
RemoteTail,
|
|
232
|
+
replay,
|
|
233
|
+
};
|
package/index.node
ADDED
|
Binary file
|
package/libplasmite.so
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "plasmite",
|
|
3
|
+
"version": "0.1.8",
|
|
4
|
+
"description": "Persistent JSON message queues for Node.js - native bindings for local and remote IPC",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "types.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"plasmite": "bin/plasmite.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"index.node",
|
|
14
|
+
"index.d.ts",
|
|
15
|
+
"types.d.ts",
|
|
16
|
+
"remote.js",
|
|
17
|
+
"bin/plasmite.js",
|
|
18
|
+
"libplasmite.dylib",
|
|
19
|
+
"libplasmite.so",
|
|
20
|
+
"plasmite"
|
|
21
|
+
],
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ipc",
|
|
24
|
+
"queue",
|
|
25
|
+
"json",
|
|
26
|
+
"messaging",
|
|
27
|
+
"pubsub",
|
|
28
|
+
"napi",
|
|
29
|
+
"native"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/sandover/plasmite.git",
|
|
34
|
+
"directory": "bindings/node"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepare-native": "node scripts/prepare_native_assets.js",
|
|
38
|
+
"build": "napi build --cargo-cwd native",
|
|
39
|
+
"test": "npm run build && node --test test/*.test.js",
|
|
40
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
41
|
+
"prepack": "npm run build && npm run prepare-native",
|
|
42
|
+
"prepublishOnly": "npm run typecheck"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@napi-rs/cli": "^2.18.0",
|
|
46
|
+
"@types/node": "^25.2.1",
|
|
47
|
+
"typescript": "^5.9.3"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/plasmite
ADDED
|
Binary file
|
package/remote.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Purpose: Provide an HTTP/JSON RemoteClient for the Node binding.
|
|
3
|
+
Key Exports: RemoteClient, RemotePool, RemoteTail, RemoteError.
|
|
4
|
+
Role: JS-side remote access that mirrors the v0 server protocol.
|
|
5
|
+
Invariants: Uses JSON request/response envelopes from spec/remote/v0.
|
|
6
|
+
Invariants: Base URL must be http(s) without a path.
|
|
7
|
+
Invariants: Tail streams are JSONL and can be canceled.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { Readable } = require("node:stream");
|
|
11
|
+
const readline = require("node:readline");
|
|
12
|
+
|
|
13
|
+
class RemoteError extends Error {
|
|
14
|
+
constructor(payload, status) {
|
|
15
|
+
const error = payload && payload.error ? payload.error : payload;
|
|
16
|
+
const message = error && error.message ? error.message : `Remote error ${status}`;
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "RemoteError";
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.kind = error && error.kind ? error.kind : "Io";
|
|
21
|
+
this.hint = error && error.hint ? error.hint : undefined;
|
|
22
|
+
this.path = error && error.path ? error.path : undefined;
|
|
23
|
+
this.seq = error && error.seq ? error.seq : undefined;
|
|
24
|
+
this.offset = error && error.offset ? error.offset : undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class RemoteClient {
|
|
29
|
+
constructor(baseUrl, options = {}) {
|
|
30
|
+
this.baseUrl = normalizeBaseUrl(baseUrl);
|
|
31
|
+
this.token = options.token || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
withToken(token) {
|
|
35
|
+
this.token = token;
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async createPool(pool, sizeBytes) {
|
|
40
|
+
const payload = { pool, size_bytes: Number(sizeBytes) };
|
|
41
|
+
const url = buildUrl(this.baseUrl, ["v0", "pools"]);
|
|
42
|
+
const data = await this._requestJson("POST", url, payload);
|
|
43
|
+
return data.pool;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async openPool(pool) {
|
|
47
|
+
const payload = { pool };
|
|
48
|
+
const url = buildUrl(this.baseUrl, ["v0", "pools", "open"]);
|
|
49
|
+
await this._requestJson("POST", url, payload);
|
|
50
|
+
return new RemotePool(this, pool);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async poolInfo(pool) {
|
|
54
|
+
const url = buildUrl(this.baseUrl, ["v0", "pools", pool, "info"]);
|
|
55
|
+
const data = await this._requestJson("GET", url, null);
|
|
56
|
+
return data.pool;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async listPools() {
|
|
60
|
+
const url = buildUrl(this.baseUrl, ["v0", "pools"]);
|
|
61
|
+
const data = await this._requestJson("GET", url, null);
|
|
62
|
+
return data.pools;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async deletePool(pool) {
|
|
66
|
+
const url = buildUrl(this.baseUrl, ["v0", "pools", pool]);
|
|
67
|
+
await this._requestJson("DELETE", url, null);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async _requestJson(method, url, body) {
|
|
71
|
+
const headers = { Accept: "application/json" };
|
|
72
|
+
if (this.token) {
|
|
73
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
74
|
+
}
|
|
75
|
+
let payload;
|
|
76
|
+
if (method !== "GET" && method !== "DELETE") {
|
|
77
|
+
headers["Content-Type"] = "application/json";
|
|
78
|
+
payload = JSON.stringify(body);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const response = await fetch(url.toString(), {
|
|
82
|
+
method,
|
|
83
|
+
headers,
|
|
84
|
+
body: payload,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw await parseRemoteError(response);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (response.status === 204) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return response.json();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async _requestStream(url, controller) {
|
|
98
|
+
const headers = { Accept: "application/json" };
|
|
99
|
+
if (this.token) {
|
|
100
|
+
headers.Authorization = `Bearer ${this.token}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const response = await fetch(url.toString(), {
|
|
104
|
+
method: "GET",
|
|
105
|
+
headers,
|
|
106
|
+
signal: controller.signal,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (!response.ok) {
|
|
110
|
+
throw await parseRemoteError(response);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return response;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
class RemotePool {
|
|
118
|
+
constructor(client, pool) {
|
|
119
|
+
this.client = client;
|
|
120
|
+
this.pool = pool;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
poolRef() {
|
|
124
|
+
return this.pool;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async append(data, tags = [], durability = "fast") {
|
|
128
|
+
const payload = { data, tags, durability };
|
|
129
|
+
const url = buildUrl(this.client.baseUrl, ["v0", "pools", this.pool, "append"]);
|
|
130
|
+
const response = await this.client._requestJson("POST", url, payload);
|
|
131
|
+
return response.message;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async get(seq) {
|
|
135
|
+
const url = buildUrl(this.client.baseUrl, [
|
|
136
|
+
"v0",
|
|
137
|
+
"pools",
|
|
138
|
+
this.pool,
|
|
139
|
+
"messages",
|
|
140
|
+
String(seq),
|
|
141
|
+
]);
|
|
142
|
+
const response = await this.client._requestJson("GET", url, null);
|
|
143
|
+
return response.message;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async tail(options = {}) {
|
|
147
|
+
const url = buildUrl(this.client.baseUrl, ["v0", "pools", this.pool, "tail"]);
|
|
148
|
+
if (options.sinceSeq !== undefined) {
|
|
149
|
+
url.searchParams.set("since_seq", String(options.sinceSeq));
|
|
150
|
+
}
|
|
151
|
+
if (options.maxMessages !== undefined) {
|
|
152
|
+
url.searchParams.set("max", String(options.maxMessages));
|
|
153
|
+
}
|
|
154
|
+
if (options.timeoutMs !== undefined) {
|
|
155
|
+
url.searchParams.set("timeout_ms", String(options.timeoutMs));
|
|
156
|
+
}
|
|
157
|
+
if (options.tags !== undefined) {
|
|
158
|
+
const tags = Array.isArray(options.tags) ? options.tags : [options.tags];
|
|
159
|
+
for (const tag of tags) {
|
|
160
|
+
url.searchParams.append("tag", String(tag));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
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
|
+
if (!response.body) {
|
|
173
|
+
throw new Error("remote tail response has no body");
|
|
174
|
+
}
|
|
175
|
+
this.controller = controller;
|
|
176
|
+
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;
|
|
200
|
+
}
|
|
201
|
+
this.done = true;
|
|
202
|
+
this.controller.abort();
|
|
203
|
+
this.reader.close();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeBaseUrl(raw) {
|
|
208
|
+
const url = new URL(raw);
|
|
209
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
210
|
+
throw new Error("remote base URL must use http or https");
|
|
211
|
+
}
|
|
212
|
+
if (url.pathname && url.pathname !== "/") {
|
|
213
|
+
throw new Error("remote base URL must not include a path");
|
|
214
|
+
}
|
|
215
|
+
url.pathname = "/";
|
|
216
|
+
url.search = "";
|
|
217
|
+
url.hash = "";
|
|
218
|
+
return url;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function buildUrl(baseUrl, segments) {
|
|
222
|
+
const url = new URL(baseUrl.toString());
|
|
223
|
+
url.pathname = `/${segments.map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
224
|
+
return url;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function parseRemoteError(response) {
|
|
228
|
+
let payload = null;
|
|
229
|
+
try {
|
|
230
|
+
payload = await response.json();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
payload = null;
|
|
233
|
+
}
|
|
234
|
+
return new RemoteError(payload, response.status);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = {
|
|
238
|
+
RemoteClient,
|
|
239
|
+
RemotePool,
|
|
240
|
+
RemoteTail,
|
|
241
|
+
RemoteError,
|
|
242
|
+
};
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Purpose: Stable TypeScript declarations for the public Node bindings API.
|
|
3
|
+
Key Exports: Client, Pool, Stream, Lite3Stream, replay, and remote client types.
|
|
4
|
+
Role: Preserve complete JS + native type surface independently of generated files.
|
|
5
|
+
Invariants: Public runtime exports from index.js are represented here.
|
|
6
|
+
Invariants: Numeric sequence fields accept number or bigint input.
|
|
7
|
+
Notes: Kept separate from NAPI-generated index.d.ts to avoid regeneration loss.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const enum Durability {
|
|
11
|
+
Fast = 0,
|
|
12
|
+
Flush = 1,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const enum ErrorKind {
|
|
16
|
+
Internal = 1,
|
|
17
|
+
Usage = 2,
|
|
18
|
+
NotFound = 3,
|
|
19
|
+
AlreadyExists = 4,
|
|
20
|
+
Busy = 5,
|
|
21
|
+
Permission = 6,
|
|
22
|
+
Corrupt = 7,
|
|
23
|
+
Io = 8,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface Lite3Frame {
|
|
27
|
+
seq: bigint
|
|
28
|
+
timestampNs: bigint
|
|
29
|
+
flags: number
|
|
30
|
+
payload: Buffer
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class PlasmiteNativeError extends Error {
|
|
34
|
+
kind?: string
|
|
35
|
+
path?: string
|
|
36
|
+
seq?: number
|
|
37
|
+
offset?: number
|
|
38
|
+
cause?: unknown
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Client {
|
|
42
|
+
constructor(poolDir: string)
|
|
43
|
+
createPool(poolRef: string, sizeBytes: number | bigint): Pool
|
|
44
|
+
openPool(poolRef: string): Pool
|
|
45
|
+
close(): void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class Pool {
|
|
49
|
+
appendJson(payload: Buffer, tags: string[], durability: Durability): Buffer
|
|
50
|
+
appendLite3(payload: Buffer, durability: Durability): bigint
|
|
51
|
+
getJson(seq: number | bigint): Buffer
|
|
52
|
+
getLite3(seq: number | bigint): Lite3Frame
|
|
53
|
+
openStream(
|
|
54
|
+
sinceSeq?: number | bigint | null,
|
|
55
|
+
maxMessages?: number | bigint | null,
|
|
56
|
+
timeoutMs?: number | bigint | null,
|
|
57
|
+
): Stream
|
|
58
|
+
openLite3Stream(
|
|
59
|
+
sinceSeq?: number | bigint | null,
|
|
60
|
+
maxMessages?: number | bigint | null,
|
|
61
|
+
timeoutMs?: number | bigint | null,
|
|
62
|
+
): Lite3Stream
|
|
63
|
+
close(): void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class Stream {
|
|
67
|
+
nextJson(): Buffer | null
|
|
68
|
+
close(): void
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class Lite3Stream {
|
|
72
|
+
next(): Lite3Frame | null
|
|
73
|
+
close(): void
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ReplayOptions {
|
|
77
|
+
speed?: number
|
|
78
|
+
sinceSeq?: number | bigint
|
|
79
|
+
maxMessages?: number | bigint
|
|
80
|
+
timeoutMs?: number | bigint
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function replay(
|
|
84
|
+
pool: Pool,
|
|
85
|
+
options?: ReplayOptions,
|
|
86
|
+
): AsyncGenerator<Buffer, void, unknown>
|
|
87
|
+
|
|
88
|
+
export interface RemoteClientOptions {
|
|
89
|
+
token?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface RemotePoolInfo {
|
|
93
|
+
pool: string
|
|
94
|
+
path: string
|
|
95
|
+
file_size: number
|
|
96
|
+
ring_size: number
|
|
97
|
+
bounds: {
|
|
98
|
+
oldest_seq: number | null
|
|
99
|
+
newest_seq: number | null
|
|
100
|
+
}
|
|
101
|
+
write_cursor: number
|
|
102
|
+
index_inline: boolean
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface RemoteMessage {
|
|
106
|
+
seq: number
|
|
107
|
+
time: string
|
|
108
|
+
data: unknown
|
|
109
|
+
meta?: {
|
|
110
|
+
tags?: string[]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface RemoteTailOptions {
|
|
115
|
+
sinceSeq?: number | bigint
|
|
116
|
+
maxMessages?: number | bigint
|
|
117
|
+
timeoutMs?: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class RemoteError extends Error {
|
|
121
|
+
status: number
|
|
122
|
+
kind: string
|
|
123
|
+
hint?: string
|
|
124
|
+
path?: string
|
|
125
|
+
seq?: number
|
|
126
|
+
offset?: number
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export class RemoteClient {
|
|
130
|
+
constructor(baseUrl: string, options?: RemoteClientOptions)
|
|
131
|
+
baseUrl: URL
|
|
132
|
+
token: string | null
|
|
133
|
+
withToken(token: string): RemoteClient
|
|
134
|
+
createPool(pool: string, sizeBytes: number | bigint): Promise<string>
|
|
135
|
+
openPool(pool: string): Promise<RemotePool>
|
|
136
|
+
poolInfo(pool: string): Promise<RemotePoolInfo>
|
|
137
|
+
listPools(): Promise<string[]>
|
|
138
|
+
deletePool(pool: string): Promise<void>
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class RemotePool {
|
|
142
|
+
constructor(client: RemoteClient, pool: string)
|
|
143
|
+
client: RemoteClient
|
|
144
|
+
pool: string
|
|
145
|
+
poolRef(): string
|
|
146
|
+
append(
|
|
147
|
+
data: unknown,
|
|
148
|
+
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
|
|
158
|
+
}
|