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 +66 -133
- package/dist/adapters/redis/base-redis-web-repl.adapter.d.ts +20 -0
- package/dist/adapters/redis/base-redis-web-repl.adapter.js +53 -0
- package/dist/adapters/redis/index.d.ts +5 -0
- package/dist/adapters/redis/index.js +9 -0
- package/dist/adapters/redis/ioredis-web-repl.adapter.d.ts +22 -0
- package/dist/adapters/redis/ioredis-web-repl.adapter.js +34 -0
- package/dist/adapters/redis/node-redis-web-repl.adapter.d.ts +24 -0
- package/dist/adapters/redis/node-redis-web-repl.adapter.js +42 -0
- package/dist/redis.d.ts +1 -0
- package/dist/redis.js +17 -0
- package/package.json +31 -2
- package/skill/SKILL.md +8 -7
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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: (
|
|
270
|
-
|
|
271
|
-
|
|
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
|
|
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.**
|
|
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
|
|
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;
|
package/dist/redis.d.ts
ADDED
|
@@ -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.
|
|
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
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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` —
|