nestjs-web-repl 2.0.1 → 2.1.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/README.md CHANGED
@@ -26,6 +26,10 @@ running server.
26
26
  npm install nestjs-web-repl
27
27
  ```
28
28
 
29
+ > **Upgrading from 1.x?** The `forRoot`/`forRootAsync` API is deprecated. v2 uses
30
+ > `register`/`registerAsync` — see [Quick start](#quick-start) and
31
+ > [Securing it](#securing-it).
32
+
29
33
  ## Quick start
30
34
 
31
35
  ```ts
@@ -157,6 +161,22 @@ WebReplModule.registerAsync({
157
161
  });
158
162
  ```
159
163
 
164
+ ### How the browser UI authenticates
165
+
166
+ Your guard runs in front of all three routes — the UI page, the SSE stream, and
167
+ the command POST. The bundled UI sends only what a browser attaches
168
+ automatically on a **same-origin** request: **cookies**. It sets no
169
+ `Authorization` header, and the SSE stream uses `EventSource`, which cannot send
170
+ custom headers at all. So protect these routes with **cookie/session** auth (or a
171
+ network-level control such as an IP allowlist, mTLS, or a VPN). A
172
+ bearer-token / `Authorization`-header guard rejects the UI — it still works for
173
+ direct `curl` clients, where you set the header yourself, but the browser cannot.
174
+
175
+ The UI is served from the same origin it calls, so a logged-in session cookie
176
+ rides along on the page load, the SSE connection, and every command. One caveat:
177
+ the UI sends no CSRF token, so authenticate off the session itself rather than
178
+ requiring a CSRF token on these routes.
179
+
160
180
  ## Adapter / multi-instance
161
181
 
162
182
  If you run more than one instance of your app (multiple processes, pods,
@@ -188,10 +208,10 @@ solves this with an **ownership + fan-out** protocol:
188
208
  never preempted this way. Takeover loses that channel's in-memory
189
209
  variables (the dead owner's session is gone) but restores availability
190
210
  instead of leaving the channel wedged fleet-wide (see
191
- [Limitations](#limitations-v1)).
211
+ [Limitations](#limitations)).
192
212
  - Because ownership is decided by whichever instance's `onCmd` handler runs
193
213
  first, two instances racing to claim the same brand-new channel at the
194
- same instant resolve **last-claim-wins** (see [Limitations](#limitations-v1)).
214
+ same instant resolve **last-claim-wins** (see [Limitations](#limitations)).
195
215
 
196
216
  By default this coordination happens via `InMemoryWebReplAdapter`, which only
197
217
  works within a single process (fine for local dev / single-instance
@@ -217,62 +237,56 @@ are distinct from, and not to be confused with, the client-visible `system`
217
237
  *SSE event type* documented under [Endpoints](#endpoints), which only ever
218
238
  carries `{ping}`/`{done}`/`{error}`).
219
239
 
220
- ### Redis adapter sketch
240
+ ### Redis (multi-instance)
241
+
242
+ Behind a load balancer the default in-memory adapter is per-process: a command
243
+ posted to one replica never reaches a session owned by another. Supply a Redis
244
+ adapter so every replica shares one pub/sub bus. Import it from the
245
+ `nestjs-web-repl/redis` subpath and hand it one connected client — the adapter
246
+ creates its own dedicated subscriber connection (Redis requires one for subscribe
247
+ mode) and closes only that connection on shutdown; your client stays yours.
248
+
249
+ **ioredis:**
221
250
 
222
251
  ```ts
223
- import { Injectable, OnModuleDestroy } from '@nestjs/common';
224
252
  import Redis from 'ioredis';
225
- import type { WebReplAdapter } from 'nestjs-web-repl';
226
-
227
- @Injectable()
228
- export class RedisWebReplAdapter implements WebReplAdapter, OnModuleDestroy {
229
- private readonly pub = new Redis(process.env.REDIS_URL);
230
- private readonly sub = new Redis(process.env.REDIS_URL);
231
-
232
- async publish(topic: string, message: string): Promise<void> {
233
- await this.pub.publish(topic, message);
234
- }
235
-
236
- async subscribe(topic: string, handler: (message: string) => void): Promise<void> {
237
- await this.sub.subscribe(topic);
238
- this.sub.on('message', (channel, message) => {
239
- if (channel === topic) handler(message);
240
- });
241
- }
242
-
243
- async onModuleDestroy(): Promise<void> {
244
- await this.pub.quit();
245
- await this.sub.quit();
246
- }
247
- }
248
- ```
253
+ import { WebReplModule } from 'nestjs-web-repl';
254
+ import { IoRedisWebReplAdapter } from 'nestjs-web-repl/redis';
249
255
 
250
- ```ts
251
- // as a ready-made instance
252
256
  WebReplModule.register({
253
257
  enabled: process.env.REPL_ENABLED === 'true',
254
- adapter: new RedisWebReplAdapter(),
255
- instanceId: process.env.HOSTNAME, // shows up in `command` SSE events and
256
- // internal webrepl:sys claim messages
258
+ adapter: new IoRedisWebReplAdapter(new Redis(process.env.REDIS_URL!)),
257
259
  });
260
+ ```
258
261
 
259
- // or let Nest construct it (and its dependencies) for you
260
- WebReplModule.register({
261
- enabled: process.env.REPL_ENABLED === 'true',
262
- adapter: { useClass: RedisWebReplAdapter, imports: [RedisModule] },
263
- });
262
+ **node-redis:**
263
+
264
+ ```ts
265
+ import { createClient } from 'redis';
266
+ import { WebReplModule } from 'nestjs-web-repl';
267
+ import { NodeRedisWebReplAdapter } from 'nestjs-web-repl/redis';
264
268
 
265
- // or build it with a factory
266
269
  WebReplModule.register({
267
270
  enabled: process.env.REPL_ENABLED === 'true',
268
271
  adapter: {
269
- useFactory: (redis: RedisService) => new RedisWebReplAdapter(redis),
270
- inject: [RedisService],
271
- imports: [RedisModule],
272
+ useFactory: async () => {
273
+ const client = createClient({ url: process.env.REDIS_URL });
274
+ await client.connect();
275
+ return new NodeRedisWebReplAdapter(client);
276
+ },
272
277
  },
273
278
  });
274
279
  ```
275
280
 
281
+ `ioredis` and `redis` are optional peer dependencies — install whichever you use.
282
+ Both adapters wrap a small shared base; to target another broker, subclass
283
+ `BaseRedisWebReplAdapter` or implement `WebReplAdapter` directly.
284
+
285
+ The `adapter` extra also accepts a DI-configured provider — `{ useClass, imports? }`
286
+ or `{ useFactory, inject?, imports? }` — so a custom adapter can pull its own
287
+ dependencies (a shared client, a config service) from a Nest module. See the
288
+ [extras table](#options-webreplmoduleoptions) below.
289
+
276
290
  ## Options (`WebReplModuleOptions`)
277
291
 
278
292
  | Option | Type | Default | Notes |
@@ -305,27 +319,6 @@ and `WEB_REPL_OPTIONS` (the DI token for the resolved options, useful when
305
319
  injecting them into a sibling-registered controller subclass), plus the types
306
320
  `WebReplAdapter`, `WebReplModuleOptions`, `WebReplModuleExtras`,
307
321
  `WebReplAdapterConfig`, `WebReplEvent`, `SseEventType`.
308
-
309
- ## Migrating from 1.x → 2.0
310
-
311
- - `WebReplModule.forRoot(...)` → `WebReplModule.register(...)`.
312
- - `WebReplModule.forRootAsync(...)` → `WebReplModule.registerAsync(...)`.
313
- - `adapter` is no longer an option resolved by `useFactory`; it's a static
314
- "extra" passed alongside the options (or alongside `useFactory`/`inject` for
315
- the async form), and now also accepts `{ useClass, imports? }` or
316
- `{ useFactory, inject?, imports? }` in addition to a ready instance — see
317
- [Adapter / multi-instance](#adapter--multi-instance).
318
- - `registerController: false` is gone. To run your own guarded controller
319
- instead of the default, subclass `WebReplController`, add `@UseGuards(...)`,
320
- and pass it as the `controller` extra to `register`/`registerAsync` — see
321
- [Securing it](#securing-it). Unlike the old `registerController` flag, this
322
- works identically for both the sync and async form.
323
- - A disabled module (`enabled: false`) now still registers the
324
- controller/service, but every route 404s and the module never subscribes to
325
- the adapter — it no longer silently registers nothing. If you were relying
326
- on a disabled module contributing zero routes/providers to the Nest module
327
- graph, that is no longer the case.
328
-
329
322
  ## AI skill
330
323
 
331
324
  This package ships a [Claude Code](https://claude.com/claude-code) skill that
@@ -341,7 +334,7 @@ agent picks it up on its next session. The command never clobbers a modified
341
334
  skill file silently — if you have edited it, re-run with `--force` to refresh it
342
335
  after upgrading the package.
343
336
 
344
- ## Limitations (v1)
337
+ ## Limitations
345
338
 
346
339
  - **Monaco loads from a CDN** (`cdn.jsdelivr.net`) inside the `/ui` page — the
347
340
  *browser* needs internet access to load the editor; the server side has no
@@ -389,10 +382,10 @@ We tell you this because the honest thing to do is let you judge the code on
389
382
  its merits rather than guess at its origins. If you are skeptical of AI-written
390
383
  code, here is what to actually look at:
391
384
 
392
- - **The tests.** 83 automated tests, including a two-instance end-to-end test
393
- that proves cross-instance command routing and output fan-out, and an
385
+ - **The tests.** A thorough automated suite, including a two-instance end-to-end
386
+ test that proves cross-instance command routing and output fan-out, and an
394
387
  execution-proof test that resolves a real provider through the live REPL
395
- context. `npm test`, `npm run build`, and `npx tsc --noEmit` are all green.
388
+ context. `npm test`, `npm run build`, and the typecheck all run green in CI.
396
389
  - **The commit history.** The real TDD trail is preserved — failing test,
397
390
  implementation, fixes — including several rounds where review caught genuine
398
391
  defects (the trickiest: `node:repl` completion detection on modern Node, and
@@ -404,72 +397,6 @@ code, here is what to actually look at:
404
397
 
405
398
  AI assistance does not exempt the code from scrutiny — it raises the bar for
406
399
  it. Issues and fixes are welcome from anyone who finds something we missed.
407
-
408
- ## AI usage & training
409
-
410
- The source here is published so people and their tools can **use** it — read
411
- it, run it, debug against it, integrate it. It is not offered as training data.
412
- [`robots.txt`](./robots.txt) and [`ai.txt`](./ai.txt) record a request that AI
413
- model *training* crawlers (GPTBot, ClaudeBot, CCBot, Google-Extended, and
414
- others) not ingest this repository. Those files are advisory — they express
415
- intent and do not, and cannot, technically enforce anything, nor do they bind
416
- GitHub's own hosting. Using an AI coding assistant to help you *work with* this
417
- library is entirely fine and expected.
418
-
419
- ## Releasing
420
-
421
- Releases are **fully automated**. Merging a PR to `main` with releasable
422
- [Conventional Commits](https://www.conventionalcommits.org/) triggers
423
- `.github/workflows/release.yml`, which runs the full test suite and then
424
- [semantic-release](https://semantic-release.gitbook.io/):
425
-
426
- | Commit type | Version bump |
427
- | ---------------------- | ----------------- |
428
- | `fix:` | patch (x.y.**z**) |
429
- | `feat:` | minor (x.**y**.0) |
430
- | `feat!:` / `BREAKING CHANGE:` in body | major (**x**.0.0) |
431
- | `docs:` `chore:` `test:` `ci:` `refactor:` | no release |
432
-
433
- It computes the next version, updates `CHANGELOG.md`, publishes to npm
434
- (**tokenless via OIDC trusted publishing, with provenance attached
435
- automatically**), tags the commit, cuts a GitHub Release, and commits the
436
- version/changelog bump back to `main` as `chore(release): x.y.z [skip ci]`.
437
- Do not bump `version` in `package.json` by hand.
438
-
439
- ### One-time bootstrap (maintainer, once)
440
-
441
- npm's OIDC trusted publishing cannot perform a package's *first* publish, so a
442
- maintainer does this once:
443
-
444
- 1. **Create the package on npm with a placeholder:**
445
- ```bash
446
- npm login
447
- npm version 0.0.0 --no-git-tag-version # temp, do not commit
448
- npm publish --access public
449
- git checkout -- package.json # restore working version
450
- ```
451
- Do **not** create a git tag for `0.0.0`; with no tags semantic-release's
452
- first automated release is `1.0.0`.
453
- 2. **Register the Trusted Publisher** at
454
- `https://www.npmjs.com/package/nestjs-web-repl/access` → *Trusted Publishers*
455
- → GitHub Actions: owner `p-dim-popov`, repository `nestjs-web-repl`, workflow
456
- `release.yml` (leave environment blank). After this, no token is needed.
457
- 3. (Optional, after `1.0.0` ships) `npm deprecate nestjs-web-repl@0.0.0 "placeholder"`.
458
-
459
- ### Verifying the first real release
460
-
461
- After the bootstrap, the next merge to `main` containing a `feat:`/`fix:` commit
462
- should produce `1.0.0`. Confirm:
463
-
464
- - `npm view nestjs-web-repl version` → `1.0.0`
465
- - the npm package page shows a provenance / "Published via GitHub Actions" badge
466
- - a `v1.0.0` git tag and a matching GitHub Release with generated notes exist
467
- - `CHANGELOG.md` and a `chore(release): 1.0.0` commit are on `main`
468
- - the `release.yml` run is green
469
-
470
- If the publish step fails with an auth error, the Trusted Publisher registration
471
- (step 2) is missing or its repo/workflow fields don't match exactly.
472
-
473
400
  ## Contributing
474
401
 
475
402
  Contributions are welcome — see [`CONTRIBUTING.md`](./CONTRIBUTING.md). AI-
@@ -477,6 +404,12 @@ assisted PRs are fine; we just ask you to disclose the assistance and to
477
404
  understand what you submit. Agents working in this repo should start with
478
405
  [`AGENTS.md`](./AGENTS.md).
479
406
 
407
+ PRs merge as a single squash commit whose message is the **PR title and
408
+ description**, and that commit drives an automated release. Write the PR title as
409
+ a [Conventional Commit](https://www.conventionalcommits.org/) (`feat:`, `fix:`,
410
+ `docs:`, …) so the version bump is correct; put any `BREAKING CHANGE:` note in the
411
+ description.
412
+
480
413
  ## License
481
414
 
482
415
  [MIT](./LICENSE) © Petar Popov
@@ -0,0 +1,20 @@
1
+ import type { WebReplAdapter } from '../../interfaces/web-repl-adapter.interface';
2
+ type Handler = (message: string) => void;
3
+ /**
4
+ * Library-agnostic Redis pub/sub bridge. Subclasses wire the abstract hooks to a
5
+ * concrete client. Topics are opaque; the service owns the `webrepl:*` names.
6
+ */
7
+ export declare abstract class BaseRedisWebReplAdapter implements WebReplAdapter {
8
+ private readonly handlers;
9
+ private readonly subscribed;
10
+ private readonly logger;
11
+ publish(topic: string, message: string): Promise<void>;
12
+ subscribe(topic: string, handler: Handler): Promise<void>;
13
+ /** Fan an inbound message out to every handler registered for the topic. */
14
+ protected dispatch(topic: string, message: string): void;
15
+ onModuleDestroy(): Promise<void>;
16
+ protected abstract doPublish(topic: string, message: string): Promise<void>;
17
+ protected abstract ensureSubscribed(topic: string): Promise<void>;
18
+ protected abstract teardown(): Promise<void>;
19
+ }
20
+ export {};
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BaseRedisWebReplAdapter = void 0;
4
+ const common_1 = require("@nestjs/common");
5
+ /**
6
+ * Library-agnostic Redis pub/sub bridge. Subclasses wire the abstract hooks to a
7
+ * concrete client. Topics are opaque; the service owns the `webrepl:*` names.
8
+ */
9
+ class BaseRedisWebReplAdapter {
10
+ constructor() {
11
+ this.handlers = new Map();
12
+ this.subscribed = new Set();
13
+ this.logger = new common_1.Logger(this.constructor.name);
14
+ }
15
+ async publish(topic, message) {
16
+ await this.doPublish(topic, message);
17
+ }
18
+ async subscribe(topic, handler) {
19
+ let set = this.handlers.get(topic);
20
+ if (!set) {
21
+ set = new Set();
22
+ this.handlers.set(topic, set);
23
+ }
24
+ set.add(handler);
25
+ if (!this.subscribed.has(topic)) {
26
+ await this.ensureSubscribed(topic);
27
+ this.subscribed.add(topic);
28
+ }
29
+ }
30
+ /** Fan an inbound message out to every handler registered for the topic. */
31
+ dispatch(topic, message) {
32
+ const set = this.handlers.get(topic);
33
+ if (!set)
34
+ return;
35
+ for (const handler of set) {
36
+ try {
37
+ handler(message);
38
+ }
39
+ catch (err) {
40
+ // A handler fault (e.g. malformed inbound JSON on a shared Redis
41
+ // channel) must not escape into the client's message emitter and crash
42
+ // the process, nor abort delivery to the remaining handlers.
43
+ this.logger.error(`web-repl adapter handler for "${topic}" threw: ${String(err)}`);
44
+ }
45
+ }
46
+ }
47
+ async onModuleDestroy() {
48
+ await this.teardown();
49
+ this.handlers.clear();
50
+ this.subscribed.clear();
51
+ }
52
+ }
53
+ exports.BaseRedisWebReplAdapter = BaseRedisWebReplAdapter;
@@ -0,0 +1,5 @@
1
+ export { BaseRedisWebReplAdapter } from './base-redis-web-repl.adapter';
2
+ export { IoRedisWebReplAdapter } from './ioredis-web-repl.adapter';
3
+ export type { IoRedisLike } from './ioredis-web-repl.adapter';
4
+ export { NodeRedisWebReplAdapter } from './node-redis-web-repl.adapter';
5
+ export type { NodeRedisLike } from './node-redis-web-repl.adapter';
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeRedisWebReplAdapter = exports.IoRedisWebReplAdapter = exports.BaseRedisWebReplAdapter = void 0;
4
+ var base_redis_web_repl_adapter_1 = require("./base-redis-web-repl.adapter");
5
+ Object.defineProperty(exports, "BaseRedisWebReplAdapter", { enumerable: true, get: function () { return base_redis_web_repl_adapter_1.BaseRedisWebReplAdapter; } });
6
+ var ioredis_web_repl_adapter_1 = require("./ioredis-web-repl.adapter");
7
+ Object.defineProperty(exports, "IoRedisWebReplAdapter", { enumerable: true, get: function () { return ioredis_web_repl_adapter_1.IoRedisWebReplAdapter; } });
8
+ var node_redis_web_repl_adapter_1 = require("./node-redis-web-repl.adapter");
9
+ Object.defineProperty(exports, "NodeRedisWebReplAdapter", { enumerable: true, get: function () { return node_redis_web_repl_adapter_1.NodeRedisWebReplAdapter; } });
@@ -0,0 +1,22 @@
1
+ import { BaseRedisWebReplAdapter } from './base-redis-web-repl.adapter';
2
+ /** Minimal structural surface of an ioredis client (no `ioredis` import). */
3
+ export interface IoRedisLike {
4
+ publish(channel: string, message: string): Promise<number>;
5
+ subscribe(channel: string): Promise<unknown>;
6
+ duplicate(): IoRedisLike;
7
+ on(event: 'message', listener: (channel: string, message: string) => void): unknown;
8
+ quit(): Promise<unknown>;
9
+ }
10
+ /**
11
+ * Redis adapter backed by ioredis. Pass one connected client (the publisher);
12
+ * the adapter creates and owns a duplicated subscriber connection.
13
+ */
14
+ export declare class IoRedisWebReplAdapter extends BaseRedisWebReplAdapter {
15
+ private readonly publisher;
16
+ private subscriber?;
17
+ constructor(publisher: IoRedisLike);
18
+ private ensureSubscriber;
19
+ protected doPublish(topic: string, message: string): Promise<void>;
20
+ protected ensureSubscribed(topic: string): Promise<void>;
21
+ protected teardown(): Promise<void>;
22
+ }
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IoRedisWebReplAdapter = void 0;
4
+ const base_redis_web_repl_adapter_1 = require("./base-redis-web-repl.adapter");
5
+ /**
6
+ * Redis adapter backed by ioredis. Pass one connected client (the publisher);
7
+ * the adapter creates and owns a duplicated subscriber connection.
8
+ */
9
+ class IoRedisWebReplAdapter extends base_redis_web_repl_adapter_1.BaseRedisWebReplAdapter {
10
+ constructor(publisher) {
11
+ super();
12
+ this.publisher = publisher;
13
+ }
14
+ ensureSubscriber() {
15
+ if (!this.subscriber) {
16
+ this.subscriber = this.publisher.duplicate();
17
+ this.subscriber.on('message', (channel, message) => this.dispatch(channel, message));
18
+ }
19
+ return this.subscriber;
20
+ }
21
+ async doPublish(topic, message) {
22
+ await this.publisher.publish(topic, message);
23
+ }
24
+ async ensureSubscribed(topic) {
25
+ await this.ensureSubscriber().subscribe(topic);
26
+ }
27
+ async teardown() {
28
+ if (this.subscriber) {
29
+ await this.subscriber.quit();
30
+ this.subscriber = undefined;
31
+ }
32
+ }
33
+ }
34
+ exports.IoRedisWebReplAdapter = IoRedisWebReplAdapter;
@@ -0,0 +1,24 @@
1
+ import { BaseRedisWebReplAdapter } from './base-redis-web-repl.adapter';
2
+ /** Minimal structural surface of a node-redis v4 client (no `redis` import). */
3
+ export interface NodeRedisLike {
4
+ publish(channel: string, message: string): Promise<number>;
5
+ subscribe(channel: string, listener: (message: string, channel: string) => void): Promise<void>;
6
+ duplicate(): NodeRedisLike;
7
+ connect(): Promise<unknown>;
8
+ quit(): Promise<unknown>;
9
+ }
10
+ /**
11
+ * Redis adapter backed by node-redis v4. Pass one connected client (the
12
+ * publisher); the adapter duplicates it, connects the copy (v4 duplicates start
13
+ * disconnected), and owns that subscriber connection.
14
+ */
15
+ export declare class NodeRedisWebReplAdapter extends BaseRedisWebReplAdapter {
16
+ private readonly publisher;
17
+ private subscriber?;
18
+ private connecting?;
19
+ constructor(publisher: NodeRedisLike);
20
+ private ensureSubscriber;
21
+ protected doPublish(topic: string, message: string): Promise<void>;
22
+ protected ensureSubscribed(topic: string): Promise<void>;
23
+ protected teardown(): Promise<void>;
24
+ }
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NodeRedisWebReplAdapter = void 0;
4
+ const base_redis_web_repl_adapter_1 = require("./base-redis-web-repl.adapter");
5
+ /**
6
+ * Redis adapter backed by node-redis v4. Pass one connected client (the
7
+ * publisher); the adapter duplicates it, connects the copy (v4 duplicates start
8
+ * disconnected), and owns that subscriber connection.
9
+ */
10
+ class NodeRedisWebReplAdapter extends base_redis_web_repl_adapter_1.BaseRedisWebReplAdapter {
11
+ constructor(publisher) {
12
+ super();
13
+ this.publisher = publisher;
14
+ }
15
+ async ensureSubscriber() {
16
+ if (this.subscriber)
17
+ return this.subscriber;
18
+ if (!this.connecting) {
19
+ const sub = this.publisher.duplicate();
20
+ this.connecting = sub.connect().then(() => {
21
+ this.subscriber = sub;
22
+ return sub;
23
+ });
24
+ }
25
+ return this.connecting;
26
+ }
27
+ async doPublish(topic, message) {
28
+ await this.publisher.publish(topic, message);
29
+ }
30
+ async ensureSubscribed(topic) {
31
+ const sub = await this.ensureSubscriber();
32
+ await sub.subscribe(topic, (message) => this.dispatch(topic, message));
33
+ }
34
+ async teardown() {
35
+ if (this.subscriber) {
36
+ await this.subscriber.quit();
37
+ this.subscriber = undefined;
38
+ this.connecting = undefined;
39
+ }
40
+ }
41
+ }
42
+ exports.NodeRedisWebReplAdapter = NodeRedisWebReplAdapter;
@@ -0,0 +1 @@
1
+ export * from './adapters/redis';
package/dist/redis.js ADDED
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./adapters/redis"), exports);
package/package.json CHANGED
@@ -1,10 +1,28 @@
1
1
  {
2
2
  "name": "nestjs-web-repl",
3
- "version": "2.0.1",
3
+ "version": "2.1.1",
4
4
  "description": "Expose a live NestJS REPL over the network (HTTP + SSE + Monaco UI).",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./redis": {
14
+ "types": "./dist/redis.d.ts",
15
+ "default": "./dist/redis.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "typesVersions": {
20
+ "*": {
21
+ "redis": [
22
+ "dist/redis.d.ts"
23
+ ]
24
+ }
25
+ },
8
26
  "bin": {
9
27
  "nestjs-web-repl": "dist/cli/index.js"
10
28
  },
@@ -35,7 +53,17 @@
35
53
  "peerDependencies": {
36
54
  "@nestjs/common": "^10 || ^11",
37
55
  "@nestjs/core": "^10 || ^11",
38
- "rxjs": "^7"
56
+ "rxjs": "^7",
57
+ "ioredis": "^5",
58
+ "redis": "^4 || ^5"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "ioredis": {
62
+ "optional": true
63
+ },
64
+ "redis": {
65
+ "optional": true
66
+ }
39
67
  },
40
68
  "devDependencies": {
41
69
  "@nestjs/common": "^11",
@@ -47,6 +75,7 @@
47
75
  "@swc/core": "^1.15.43",
48
76
  "@types/node": "^20",
49
77
  "@types/supertest": "^6",
78
+ "ioredis-mock": "^8.13.1",
50
79
  "reflect-metadata": "^0.2",
51
80
  "rxjs": "^7",
52
81
  "semantic-release": "^25.0.8",
package/skill/SKILL.md CHANGED
@@ -89,13 +89,14 @@ Open `http://localhost:3000/repl/main/ui` in a browser for the interactive UI.
89
89
  `@UseGuards(...)`, and pass it as `controller:` to `register`/`registerAsync`.
90
90
  See "Securing it" in the package README.
91
91
  - **Multiple app instances** (load-balanced replicas): the default in-memory
92
- adapter is per-process; supply a shared adapter via the `adapter` extra
93
- (a ready instance, `{ useClass, imports }`, or `{ useFactory, inject, imports }`)
94
- so a command and its output reach the owning instance. See "Adapter /
95
- multi-instance" in the package README.
96
- - **Custom adapter:** implement the adapter interface (publish/subscribe over
97
- the message topics) and pass it via the `adapter` extra. See "Adapter /
98
- multi-instance" in the README.
92
+ adapter is per-process. For Redis, import a ready-made adapter from the
93
+ `nestjs-web-repl/redis` subpath `IoRedisWebReplAdapter` (ioredis) or
94
+ `NodeRedisWebReplAdapter` (node-redis) and pass it as the `adapter` extra in
95
+ the same `register` call:
96
+ `register({ enabled, adapter: new IoRedisWebReplAdapter(client) })`. Hand it
97
+ one connected client; it owns its own subscriber connection. For any other
98
+ broker, supply a custom adapter (a ready instance, `{ useClass, imports }`, or
99
+ `{ useFactory, inject, imports }`). See "Adapter / multi-instance" in the README.
99
100
  - **Options** (base path, TTLs, heartbeats) and **exported symbols:** see
100
101
  "Options" and "Exports" in the README.
101
102
  - **Runnable example gotcha:** run examples with `ts-node`, not `tsx` —