procmesh-js 0.1.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/LICENSE +21 -0
- package/README.md +331 -0
- package/package.json +46 -0
- package/src/broker-bin.js +41 -0
- package/src/broker.js +676 -0
- package/src/cli.js +146 -0
- package/src/client.js +512 -0
- package/src/codec.js +54 -0
- package/src/errors.js +43 -0
- package/src/hashring.js +27 -0
- package/src/index.js +49 -0
- package/src/locks.js +129 -0
- package/src/persistence.js +327 -0
- package/src/protocol.js +169 -0
- package/src/sharded-client.js +338 -0
- package/src/store.js +155 -0
- package/src/transport.js +32 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 shahzadhamza
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# procmesh
|
|
2
|
+
|
|
3
|
+
Shared in-memory cache, pub/sub, RPC, and locks across **independent Node.js
|
|
4
|
+
processes on the same machine** โ no Redis, no external server, no native addons.
|
|
5
|
+
|
|
6
|
+
A tiny **broker process** holds the authoritative in-memory state and routes all
|
|
7
|
+
messaging over a local socket (Unix domain socket on Linux/macOS, named pipe on
|
|
8
|
+
Windows). The first client to connect auto-spawns the broker, so there's nothing to
|
|
9
|
+
run or manage. Every operation serializes through the one broker process, so the
|
|
10
|
+
cache, atomic ops, and locks are strongly consistent and correct by construction.
|
|
11
|
+
|
|
12
|
+
**What you get:**
|
|
13
|
+
|
|
14
|
+
- ๐๏ธ **Cache** โ shared key/value store with TTL and LRU eviction
|
|
15
|
+
- โ **Atomic ops** โ `incr` / `decr` / `cas`, TTL-preserving, race-free
|
|
16
|
+
- ๐ฃ **Pub/Sub** โ channels with prefix wildcards and slow-consumer protection
|
|
17
|
+
- ๐ **RPC** โ request/response across processes that doubles as a load-balanced worker pool
|
|
18
|
+
- ๐ **Locks** โ TTL'd mutexes with fencing tokens, auto-released on crash
|
|
19
|
+
- ๐ฆ **Zero infra** โ no external server; only `lru-cache` required, no native deps
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install procmesh-js
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Contents
|
|
26
|
+
|
|
27
|
+
- [Quick start](#quick-start)
|
|
28
|
+
- [Core API](#core-api)
|
|
29
|
+
- [Cache](#cache) ยท [Atomic ops](#atomic-ops) ยท [Pub/Sub](#pubsub) ยท [RPC](#rpc-requestresponse-across-processes) ยท [Locks](#locks)
|
|
30
|
+
- [Advanced](#advanced)
|
|
31
|
+
- [Fencing tokens](#fencing-tokens-safe-under-ttl-overrun) ยท [Sharding](#sharding-scale-past-one-core) ยท [Persistence & crash survival](#persistence--crash-survival-ha)
|
|
32
|
+
- [Operations](#operations)
|
|
33
|
+
- [Running the broker explicitly](#running-the-broker-explicitly) ยท [Observability](#observability) ยท [Events](#events)
|
|
34
|
+
- [Configuration reference](#configuration-reference)
|
|
35
|
+
- [How it works](#how-it-works)
|
|
36
|
+
- [Limitations](#limitations-v1)
|
|
37
|
+
- [License](#license)
|
|
38
|
+
|
|
39
|
+
## Quick start
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const { createClient } = require('procmesh-js');
|
|
43
|
+
|
|
44
|
+
// In process A
|
|
45
|
+
const mesh = await createClient(); // auto-spawns a broker if needed
|
|
46
|
+
await mesh.set('user:1', { name: 'Ada' }, { ttl: 60_000 });
|
|
47
|
+
|
|
48
|
+
// In process B (a totally separate `node` invocation)
|
|
49
|
+
const mesh = await createClient();
|
|
50
|
+
console.log(await mesh.get('user:1')); // -> { name: 'Ada' }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
All processes that use the same `name` (default `'default'`) share one broker.
|
|
54
|
+
|
|
55
|
+
## Core API
|
|
56
|
+
|
|
57
|
+
### Cache
|
|
58
|
+
```js
|
|
59
|
+
await mesh.set(key, value, { ttl }) // ttl in ms (optional)
|
|
60
|
+
await mesh.get(key) // -> value | undefined
|
|
61
|
+
await mesh.has(key) // -> boolean
|
|
62
|
+
await mesh.del(key) // -> boolean
|
|
63
|
+
await mesh.keys() // -> string[]
|
|
64
|
+
await mesh.clear()
|
|
65
|
+
await mesh.mget(['a', 'b']) // -> [valA, valB]
|
|
66
|
+
await mesh.mset({ a: 1, b: 2 }) // or [['a',1], ['b',2]]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Atomic ops
|
|
70
|
+
```js
|
|
71
|
+
await mesh.incr('count') // -> new value (starts at 0)
|
|
72
|
+
await mesh.decr('count', 2)
|
|
73
|
+
await mesh.cas('cfg', expected, next) // compare-and-set -> boolean
|
|
74
|
+
```
|
|
75
|
+
`incr`/`decr`/`cas` are read-modify-write and **preserve any per-item TTL** on the key โ a counter
|
|
76
|
+
created with an expiry keeps counting down rather than becoming immortal on the next update.
|
|
77
|
+
|
|
78
|
+
### Pub/Sub
|
|
79
|
+
```js
|
|
80
|
+
const off = await mesh.subscribe('orders', (payload, channel) => { ... });
|
|
81
|
+
await mesh.publish('orders', { id: 7 }); // -> number of subscribers reached
|
|
82
|
+
await off(); // unsubscribe
|
|
83
|
+
|
|
84
|
+
// Wildcard (prefix) subscriptions โ a trailing * matches by prefix:
|
|
85
|
+
await mesh.subscribe('orders.*', (p, channel) => { /* orders.created, orders.shipped, ... */ });
|
|
86
|
+
```
|
|
87
|
+
A slow subscriber can't make the broker run out of memory: pub/sub frames to a
|
|
88
|
+
backed-up consumer are dropped past the high-water mark (see `sendHighWaterMark`),
|
|
89
|
+
while replies/RPC stay reliable.
|
|
90
|
+
|
|
91
|
+
### RPC (request/response across processes)
|
|
92
|
+
```js
|
|
93
|
+
// worker process
|
|
94
|
+
await mesh.register('resize', async (w, h) => doResize(w, h));
|
|
95
|
+
|
|
96
|
+
// caller process
|
|
97
|
+
const result = await mesh.call('resize', [800, 600], { timeout: 5000 });
|
|
98
|
+
```
|
|
99
|
+
**Worker pools:** if several processes `register` the *same* name, the broker
|
|
100
|
+
load-balances calls across them (least-busy first), so RPC doubles as a work queue.
|
|
101
|
+
A call in flight when its worker dies is failed with `EHANDLERGONE`; the pool keeps
|
|
102
|
+
serving from the remaining workers. A worker that *hangs* while staying connected can't leak broker
|
|
103
|
+
state either: the broker reaps the pending call shortly after the caller's `timeout` elapses, frees
|
|
104
|
+
the worker's in-flight slot (so dispatch isn't skewed), and โ for a caller without its own timeout โ
|
|
105
|
+
fails it with `ECALLTIMEOUT`.
|
|
106
|
+
|
|
107
|
+
### Locks
|
|
108
|
+
```js
|
|
109
|
+
// manual
|
|
110
|
+
const release = await mesh.lock('job:42', { wait: 10_000, ttl: 30_000 });
|
|
111
|
+
if (release) { try { /* critical section */ } finally { await release(); } }
|
|
112
|
+
|
|
113
|
+
// scoped โ acquires, runs, always releases
|
|
114
|
+
await mesh.withLock('job:42', async () => { /* critical section */ });
|
|
115
|
+
```
|
|
116
|
+
`wait` is how long to block for the lock (0 = fail immediately, returning `null`).
|
|
117
|
+
`ttl` auto-releases the lock if the holder crashes, preventing deadlocks. A holder's
|
|
118
|
+
locks are also released automatically when it disconnects.
|
|
119
|
+
|
|
120
|
+
## Advanced
|
|
121
|
+
|
|
122
|
+
### Fencing tokens (safe under TTL overrun)
|
|
123
|
+
|
|
124
|
+
If a critical section runs longer than the lock's `ttl`, the lock auto-releases and another
|
|
125
|
+
client can acquire it โ the classic "two holders" hazard. Each grant therefore carries a
|
|
126
|
+
monotonically increasing **fencing token**, and procmesh-mediated writes can be *fenced*: a
|
|
127
|
+
write from a holder whose lock already expired is rejected with `EFENCED`.
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
// withLock hands you a fenced context โ set/cas/del here are guarded by this grant's token:
|
|
131
|
+
await mesh.withLock('account:42', async ({ token, set, cas }) => {
|
|
132
|
+
await set('account:42:balance', 100); // rejected with EFENCED if our lock has expired
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// manual: the release closure carries the token
|
|
136
|
+
const release = await mesh.lock('account:42', { ttl: 30_000 });
|
|
137
|
+
await mesh.fencedSet('account:42', release.token, 'account:42:balance', 100);
|
|
138
|
+
await release();
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Limitation:** procmesh can only *enforce* fencing for state kept in its own store. If your
|
|
142
|
+
critical section writes to an external resource (DB row, file, API), that resource must check
|
|
143
|
+
`token` itself โ procmesh gives you the token but cannot police a foreign system.
|
|
144
|
+
|
|
145
|
+
### Sharding (scale past one core)
|
|
146
|
+
|
|
147
|
+
A single broker serializes everything on one event loop โ correct by construction, but a
|
|
148
|
+
one-core ceiling (~100k small ops/sec). To go faster, **shard** across N brokers (N cores)
|
|
149
|
+
with one option. The returned handle has the *identical* API, so nothing else in your code changes:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
// dev: auto-spawn 4 shard brokers (named default#0 .. default#3)
|
|
153
|
+
const mesh = await createClient({ shards: 4 });
|
|
154
|
+
|
|
155
|
+
// prod: point at explicit, supervised brokers
|
|
156
|
+
const mesh = await createClient({
|
|
157
|
+
shards: ['/run/procmesh/0.sock', '/run/procmesh/1.sock', '/run/procmesh/2.sock'],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await mesh.set('user:1', { name: 'Ada' }); // same API, transparently routed
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**How work is routed.** Every key/name/channel hashes (FNV-1a, mod N) to exactly one broker:
|
|
164
|
+
|
|
165
|
+
| Primitive | Routes by | Scales across cores? |
|
|
166
|
+
|-----------------------------------|----------------|-----------------------------------------------|
|
|
167
|
+
| cache / atomic (`get`/`incr`/โฆ) | the **key** | yes โ keyspace spread evenly |
|
|
168
|
+
| locks + fenced writes | the **lock key** | yes |
|
|
169
|
+
| RPC (`register`/`call`) | the **proc name** | yes, across *distinct* names (one busy name still pins to one broker) |
|
|
170
|
+
| pub/sub publish + exact subscribe | the **channel** | yes |
|
|
171
|
+
| pub/sub **wildcard** subscribe | **all shards** | the pattern occupies a slot on every broker |
|
|
172
|
+
|
|
173
|
+
Because each key lands on exactly one broker, atomic ops and locks stay correct by construction โ
|
|
174
|
+
exactly as with a single broker. A publish still hits exactly one broker (no duplicate delivery);
|
|
175
|
+
wildcard subscriptions register everywhere so they catch a publish on any shard.
|
|
176
|
+
|
|
177
|
+
**Invariant โ agree on N.** Every process joining the same mesh **must** use the same shard count
|
|
178
|
+
and the same per-shard naming/addresses, or a given key hashes to different brokers across processes
|
|
179
|
+
(writer/reader split-brain). Pin `shards` in shared config. (Numeric `shards` is incompatible with
|
|
180
|
+
`PROCMESH_SOCKET`, which would collapse every shard onto one socket โ that throws.)
|
|
181
|
+
|
|
182
|
+
**Lock/data colocation gotcha.** Inside `withLock(L, fn)`, `ctx.set(k, โฆ)` writes to **`L`'s** shard
|
|
183
|
+
(fencing is per-broker), *not* `k`'s shard. So a fenced write may not be visible via `mesh.get(k)`
|
|
184
|
+
when `k` and `L` hash differently. Keep them colocated: use the same string for the lock and the
|
|
185
|
+
guarded key, or namespace guarded keys under the lock key.
|
|
186
|
+
|
|
187
|
+
**Events & stats.** The sharded handle emits `connect` once **all** shards are up and `disconnect`
|
|
188
|
+
once **all** are down, plus per-shard `shard-reconnect(i)` / `shard-disconnect(i)` (a forwarded
|
|
189
|
+
`error` carries `err.shard`). `mesh.stats()` returns summed counters plus a `shards: [...]` array for
|
|
190
|
+
per-shard drill-down; `cpuCoreFraction` is the **sum** across brokers (so > 1.0 is expected and good),
|
|
191
|
+
and `subscriptions` over-counts wildcards (present on every shard).
|
|
192
|
+
|
|
193
|
+
### Persistence & crash survival (HA)
|
|
194
|
+
|
|
195
|
+
By default the broker is purely in-memory (zero-config). For production you can enable
|
|
196
|
+
**snapshot + append-only-log** persistence so cache and atomic counters survive a broker crash
|
|
197
|
+
or restart:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
npx procmesh serve --persist-dir /var/lib/procmesh # persistence on (fsync everysec)
|
|
201
|
+
# or: PROCMESH_PERSIST_DIR=/var/lib/procmesh npx procmesh serve
|
|
202
|
+
npx procmesh serve --no-persist # explicitly in-memory only
|
|
203
|
+
```
|
|
204
|
+
```js
|
|
205
|
+
createBroker({ persist: { dir: '/var/lib/procmesh', mode: 'everysec' } });
|
|
206
|
+
// mode: 'no' (OS-buffered) | 'everysec' (default, โค1s loss on power failure) | 'always' (sync, durable)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
What survives a restart and what doesn't โ **by design**:
|
|
210
|
+
- **Survives:** cache values, atomic counters (with correct remaining TTL), and the fencing
|
|
211
|
+
counter (so a stale pre-crash token is still rejected after restart).
|
|
212
|
+
- **Released:** **all locks.** A lock's owner is a live connection that dies with the broker;
|
|
213
|
+
treat a broker restart exactly as you treat a lock TTL expiry โ clients re-acquire on reconnect.
|
|
214
|
+
- **Rebuilt automatically:** subscriptions and RPC registrations (clients replay them on reconnect).
|
|
215
|
+
|
|
216
|
+
Run a **dedicated, supervised** broker (not auto-spawn) so it outlives your workers, with an
|
|
217
|
+
**explicit** persist dir (not the OS temp dir, which may clear on reboot):
|
|
218
|
+
|
|
219
|
+
```ini
|
|
220
|
+
# systemd: /etc/systemd/system/procmesh.service
|
|
221
|
+
[Service]
|
|
222
|
+
ExecStart=/usr/bin/node /path/to/node_modules/procmesh-js/src/broker-bin.js
|
|
223
|
+
Environment=PROCMESH_BROKER_OPTS={"name":"default","idleTimeout":0,"persist":{"dir":"/var/lib/procmesh"}}
|
|
224
|
+
Restart=always
|
|
225
|
+
RestartSec=1
|
|
226
|
+
```
|
|
227
|
+
(PM2: `pm2 start node_modules/procmesh-js/src/broker-bin.js`; Windows: wrap it with NSSM.)
|
|
228
|
+
|
|
229
|
+
## Operations
|
|
230
|
+
|
|
231
|
+
### Running the broker explicitly
|
|
232
|
+
|
|
233
|
+
Auto-spawn is convenient, but you can also run a long-lived broker (e.g. under
|
|
234
|
+
systemd or PM2) so it never idles out:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
npx procmesh serve # foreground broker
|
|
238
|
+
npx procmesh status # is a broker up?
|
|
239
|
+
npx procmesh stop # ask it to shut down
|
|
240
|
+
# all accept [--name <name>] [--socket <addr>] [--token <secret>]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Observability
|
|
244
|
+
|
|
245
|
+
```js
|
|
246
|
+
const s = await mesh.stats(); // { uptimeMs, connections, cacheSize, ops, dropped, reaped,
|
|
247
|
+
// locks, lockWaiters, pendingCalls, subscriptions, procs,
|
|
248
|
+
// memory, cpuCoreFraction }
|
|
249
|
+
```
|
|
250
|
+
Or from the shell: `npx procmesh stats [--json]`. Counters are bumped on the hot path with a
|
|
251
|
+
single integer increment per op, so this is cheap to poll for a Prometheus exporter, etc.
|
|
252
|
+
|
|
253
|
+
### Events
|
|
254
|
+
|
|
255
|
+
- **Client:** `connect`, `disconnect`, `reconnect`, `error`.
|
|
256
|
+
- **Broker:** `connect`/`disconnect` (connId), `drop` (`{ channel, connId }`),
|
|
257
|
+
`reap` (connId reaped), `stats` (snapshot, if `statsInterval` set), `persist-error` (err).
|
|
258
|
+
|
|
259
|
+
## Configuration reference
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
await createClient({
|
|
263
|
+
name: 'default', // logical mesh; maps to a socket. Use distinct names to isolate meshes.
|
|
264
|
+
codec: 'json', // 'json' (default) | 'msgpack' (needs optional `msgpackr`) | custom { encode, decode }
|
|
265
|
+
// NOTE: a custom { encode, decode } codec can't be forwarded to an
|
|
266
|
+
// auto-spawned broker (functions can't cross `spawn`). Run the broker
|
|
267
|
+
// yourself with the same codec and pass autoSpawn:false (else it throws).
|
|
268
|
+
autoSpawn: true, // spawn a broker if none is running
|
|
269
|
+
reconnect: true, // auto-reconnect + replay subscriptions/registrations
|
|
270
|
+
callTimeout: 30_000, // default per-request timeout (ms)
|
|
271
|
+
pingInterval: 0, // ms; client-side keepalive. >0 = ping on this interval and reconnect if a
|
|
272
|
+
// PONG doesn't return in time (detects a dead broker on a half-open
|
|
273
|
+
// socket / when the broker heartbeat is off). 0 = rely on broker heartbeat.
|
|
274
|
+
token: undefined, // shared secret; if the broker requires one, must match (else EAUTH)
|
|
275
|
+
cache: { max: 10_000, ttl: 0, maxSize: 0 }, // broker cache bounds (used when this client spawns the broker)
|
|
276
|
+
shards: undefined, // scale past one core: a count N, or an array of broker addresses/names (see Sharding)
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Broker-side options (passed to `createBroker`, `procmesh serve`, or forwarded by an
|
|
281
|
+
auto-spawning client):
|
|
282
|
+
```js
|
|
283
|
+
createBroker({
|
|
284
|
+
token: undefined, // require this shared secret on HELLO (omit = allow all, zero-config default)
|
|
285
|
+
callTimeout: 30_000, // ms; backstop deadline to reap an RPC call whose worker hangs while
|
|
286
|
+
// connected (frees pending state + worker inflight). Used when a
|
|
287
|
+
// caller doesn't send its own per-call timeout; otherwise caller's + grace.
|
|
288
|
+
heartbeatInterval: 30_000, // ms; broker pings idle conns and reaps unresponsive ones (3ร interval)
|
|
289
|
+
sendHighWaterMark: 16<<20, // bytes; pub/sub frames dropped for a consumer buffered past this
|
|
290
|
+
sendHardLimit: 64<<20, // bytes; a consumer buffered past this is disconnected (slow-consumer protection)
|
|
291
|
+
idleTimeout: 0, // ms; auto-shutdown after the last client leaves (0 = never)
|
|
292
|
+
statsInterval: 0, // ms; if set, emit a 'stats' snapshot on this interval (0 = off)
|
|
293
|
+
cache: { max: 10_000, ttl: 0, maxSize: 0 },
|
|
294
|
+
persist: undefined, // crash-survival persistence โ see below (off by default)
|
|
295
|
+
});
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
## How it works
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
node app-a node app-b node worker
|
|
302
|
+
\ | /
|
|
303
|
+
\ | /
|
|
304
|
+
[ procmesh broker process ]
|
|
305
|
+
Unix socket / Windows named pipe
|
|
306
|
+
cache ยท pub/sub ยท rpc router ยท lock manager
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
- **Transport:** Node's built-in `net` only โ zero native dependencies.
|
|
310
|
+
- **Framing:** 4-byte length-prefixed frames; JSON payloads by default.
|
|
311
|
+
- **Eviction/TTL:** backed by [`lru-cache`](https://www.npmjs.com/package/lru-cache).
|
|
312
|
+
- **Robustness:** per-connection high-water-mark backpressure (slow consumers can't OOM the
|
|
313
|
+
broker), heartbeats that reap dead connections, worker-pool RPC with least-busy dispatch,
|
|
314
|
+
and optional shared-secret auth.
|
|
315
|
+
|
|
316
|
+
## Limitations (v1)
|
|
317
|
+
|
|
318
|
+
- **Same machine only.** No cross-host networking (use Redis/NATS for that).
|
|
319
|
+
- **Single broker = single core.** All ops serialize through one event loop; that's what makes
|
|
320
|
+
cache/atomic/locks correct by construction, but it caps throughput at one core (โ100k small
|
|
321
|
+
ops/sec). To scale past it, **shard** across N brokers with `createClient({ shards: N })` โ see
|
|
322
|
+
[Sharding](#sharding-scale-past-one-core).
|
|
323
|
+
- **Persistence is opt-in** (see above). Without it, state is gone when the broker exits. Even
|
|
324
|
+
with it, locks are released on restart (they're connection-scoped), and pub/sub is at-most-once
|
|
325
|
+
(messages published during a disconnect are not buffered).
|
|
326
|
+
- Every cache op is a local IPC round-trip (~0.05โ0.2 ms). Fine for coordination;
|
|
327
|
+
not a substitute for a per-process hot cache in ultra-hot paths.
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "procmesh-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared in-memory cache, pub/sub, RPC and locks across independent Node.js processes on one machine",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"procmesh": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/",
|
|
11
|
+
"prepublishOnly": "npm test"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cache",
|
|
15
|
+
"ipc",
|
|
16
|
+
"shared-memory",
|
|
17
|
+
"pubsub",
|
|
18
|
+
"rpc",
|
|
19
|
+
"lock",
|
|
20
|
+
"multiprocess",
|
|
21
|
+
"in-memory"
|
|
22
|
+
],
|
|
23
|
+
"author": "Shahzad Hamza",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/shahzadhamza/procmesh-js.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/shahzadhamza/procmesh-js/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/shahzadhamza/procmesh-js#readme",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"lru-cache": "^11.0.0"
|
|
37
|
+
},
|
|
38
|
+
"optionalDependencies": {
|
|
39
|
+
"msgpackr": "^1.11.0"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"src",
|
|
43
|
+
"README.md"
|
|
44
|
+
],
|
|
45
|
+
"license": "MIT"
|
|
46
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Standalone broker entrypoint. Spawned (detached) by clients on auto-start, and
|
|
5
|
+
// invoked by the CLI `serve` command. Configuration arrives as JSON via the
|
|
6
|
+
// PROCMESH_BROKER_OPTS environment variable.
|
|
7
|
+
|
|
8
|
+
const Broker = require('./broker');
|
|
9
|
+
|
|
10
|
+
let opts = {};
|
|
11
|
+
try {
|
|
12
|
+
opts = JSON.parse(process.env.PROCMESH_BROKER_OPTS || '{}');
|
|
13
|
+
} catch {
|
|
14
|
+
opts = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const broker = new Broker(opts);
|
|
18
|
+
|
|
19
|
+
broker
|
|
20
|
+
.start()
|
|
21
|
+
.then(() => {
|
|
22
|
+
// Signal readiness to a parent that used an IPC channel (not the detached case).
|
|
23
|
+
if (typeof process.send === 'function') process.send('ready');
|
|
24
|
+
if (process.env.PROCMESH_VERBOSE) {
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
console.log(`procmesh broker listening on ${broker.address}`);
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
.catch((err) => {
|
|
30
|
+
// Lost the race to bind โ another broker already owns the socket. That's fine.
|
|
31
|
+
if (err && err.code === 'EADDRINUSE') process.exit(0);
|
|
32
|
+
// eslint-disable-next-line no-console
|
|
33
|
+
console.error('procmesh broker failed to start:', err && err.message);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function shutdown() {
|
|
38
|
+
broker.close().then(() => process.exit(0));
|
|
39
|
+
}
|
|
40
|
+
process.on('SIGINT', shutdown);
|
|
41
|
+
process.on('SIGTERM', shutdown);
|