bhooai-nexus 0.1.2
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 +184 -0
- package/apps/admin/index.html +12 -0
- package/apps/admin/package.json +25 -0
- package/apps/admin/postcss.config.js +6 -0
- package/apps/admin/public/anime-girl.svg +84 -0
- package/apps/admin/public/bhooai-nexus-logo.png +0 -0
- package/apps/admin/public/bhooai-nexus-logo.svg +135 -0
- package/apps/admin/public/favicon.ico +0 -0
- package/apps/admin/src/main.tsx +10 -0
- package/apps/admin/src/vite-env.d.ts +18 -0
- package/apps/admin/tailwind.config.js +10 -0
- package/apps/admin/tsconfig.json +17 -0
- package/apps/admin/vite.config.ts +68 -0
- package/bin/nexus.js +52 -0
- package/package.json +108 -0
- package/packages/nexus-admin/package.json +32 -0
- package/packages/nexus-admin/src/App.tsx +4878 -0
- package/packages/nexus-admin/src/alertCenter.tsx +150 -0
- package/packages/nexus-admin/src/api.ts +601 -0
- package/packages/nexus-admin/src/assets/bhooai-nexus-logo.svg +25 -0
- package/packages/nexus-admin/src/index.css +3635 -0
- package/packages/nexus-admin/src/vite-env.d.ts +19 -0
- package/packages/nexus-ads/package.json +20 -0
- package/packages/nexus-ads/src/GaqlBuilder.ts +72 -0
- package/packages/nexus-ads/src/GoogleAdsClient.ts +121 -0
- package/packages/nexus-ads/src/fetchTransport.ts +10 -0
- package/packages/nexus-ads/src/index.ts +20 -0
- package/packages/nexus-ads/src/types.ts +41 -0
- package/packages/nexus-ai-client/package.json +18 -0
- package/packages/nexus-ai-client/src/client.ts +159 -0
- package/packages/nexus-ai-client/src/errors.ts +21 -0
- package/packages/nexus-ai-client/src/index.ts +4 -0
- package/packages/nexus-ai-client/src/sse.ts +53 -0
- package/packages/nexus-ai-client/src/types.ts +174 -0
- package/packages/nexus-auth/package.json +22 -0
- package/packages/nexus-auth/src/auth.ts +116 -0
- package/packages/nexus-auth/src/bcryptjs.d.ts +10 -0
- package/packages/nexus-auth/src/cors.ts +91 -0
- package/packages/nexus-auth/src/csrf.ts +158 -0
- package/packages/nexus-auth/src/headers.ts +55 -0
- package/packages/nexus-auth/src/index.ts +11 -0
- package/packages/nexus-auth/src/jwt.ts +65 -0
- package/packages/nexus-auth/src/middleware.ts +94 -0
- package/packages/nexus-auth/src/oauth.ts +199 -0
- package/packages/nexus-auth/src/password.ts +33 -0
- package/packages/nexus-auth/src/rateLimit.ts +59 -0
- package/packages/nexus-auth/src/rbac.ts +94 -0
- package/packages/nexus-auth/src/session.ts +91 -0
- package/packages/nexus-cache/package.json +21 -0
- package/packages/nexus-cache/src/Cache.ts +155 -0
- package/packages/nexus-cache/src/PubSub.ts +98 -0
- package/packages/nexus-cache/src/RateLimiter.ts +43 -0
- package/packages/nexus-cache/src/index.ts +9 -0
- package/packages/nexus-cache/src/types.ts +47 -0
- package/packages/nexus-cli/package.json +25 -0
- package/packages/nexus-cli/src/commands/cluster.ts +133 -0
- package/packages/nexus-cli/src/commands/dev.ts +200 -0
- package/packages/nexus-cli/src/commands/doctor.ts +199 -0
- package/packages/nexus-cli/src/commands/init.ts +1009 -0
- package/packages/nexus-cli/src/commands/node.ts +106 -0
- package/packages/nexus-cli/src/commands/pysetup.ts +142 -0
- package/packages/nexus-cli/src/commands/sync.ts +116 -0
- package/packages/nexus-cli/src/commands/uninstall.ts +287 -0
- package/packages/nexus-cli/src/config-sync.ts +384 -0
- package/packages/nexus-cli/src/dotenv.ts +39 -0
- package/packages/nexus-cli/src/index.ts +94 -0
- package/packages/nexus-cli/src/supervisor.ts +394 -0
- package/packages/nexus-cli/src/util.ts +132 -0
- package/packages/nexus-cli/src/wizard.ts +149 -0
- package/packages/nexus-cli/templates/Dockerfile +60 -0
- package/packages/nexus-cli/templates/README.md +69 -0
- package/packages/nexus-cli/templates/apps/admin/index.html +12 -0
- package/packages/nexus-cli/templates/apps/admin/package.json +24 -0
- package/packages/nexus-cli/templates/apps/admin/postcss.config.js +6 -0
- package/packages/nexus-cli/templates/apps/admin/src/main.tsx +10 -0
- package/packages/nexus-cli/templates/apps/admin/src/vite-env.d.ts +18 -0
- package/packages/nexus-cli/templates/apps/admin/tailwind.config.js +9 -0
- package/packages/nexus-cli/templates/apps/admin/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/apps/admin/vite.config.ts +64 -0
- package/packages/nexus-cli/templates/apps/ai-server/main.py +43 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/providers/base.py +111 -0
- package/packages/nexus-cli/templates/apps/ai-server/requirements.txt +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/__init__.py +3 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/chat.py +47 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/embeddings.py +30 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/lint.py +167 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/models.py +23 -0
- package/packages/nexus-cli/templates/apps/ai-server/routers/preflight.py +169 -0
- package/packages/nexus-cli/templates/apps/ai-server/settings.py +48 -0
- package/packages/nexus-cli/templates/apps/backend/package.json +34 -0
- package/packages/nexus-cli/templates/apps/backend/src/main.ts +375 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/adminRoutes.ts +732 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/clusterRoutes.ts +391 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/databaseRoutes.ts +161 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/lintProxy.ts +89 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/preflightProxy.ts +242 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/roleCatalog.ts +78 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/admin/schemaRoutes.ts +449 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/ai/aiProxy.ts +265 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/auth/authRoutes.ts +220 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentRoutes.ts +100 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/payments/paymentStore.ts +172 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/requests/requestLog.ts +175 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userGraph.ts +83 -0
- package/packages/nexus-cli/templates/apps/backend/src/modules/users/userModel.ts +88 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/CronScheduler.ts +69 -0
- package/packages/nexus-cli/templates/apps/backend/src/plugins/loadPlugins.ts +107 -0
- package/packages/nexus-cli/templates/apps/backend/tsconfig.json +14 -0
- package/packages/nexus-cli/templates/apps/frontend/index.html +12 -0
- package/packages/nexus-cli/templates/apps/frontend/package.json +19 -0
- package/packages/nexus-cli/templates/apps/frontend/src/main.tsx +64 -0
- package/packages/nexus-cli/templates/apps/frontend/vite.config.ts +72 -0
- package/packages/nexus-cli/templates/bin/nexus.js +35 -0
- package/packages/nexus-cli/templates/bin/serve-all.mjs +45 -0
- package/packages/nexus-cli/templates/dockerignore +15 -0
- package/packages/nexus-cli/templates/gitignore +12 -0
- package/packages/nexus-cli/templates/nexus.config.ts +69 -0
- package/packages/nexus-cli/templates/package.json +47 -0
- package/packages/nexus-cli/templates/tsconfig.json +17 -0
- package/packages/nexus-cli/templates/uploads/.gitkeep +0 -0
- package/packages/nexus-core/package.json +38 -0
- package/packages/nexus-core/src/config/ConfigLoader.ts +161 -0
- package/packages/nexus-core/src/config/defaults.ts +155 -0
- package/packages/nexus-core/src/config/env.ts +113 -0
- package/packages/nexus-core/src/config/index.ts +6 -0
- package/packages/nexus-core/src/config/merge.ts +28 -0
- package/packages/nexus-core/src/config/schema.ts +184 -0
- package/packages/nexus-core/src/config/types.ts +352 -0
- package/packages/nexus-core/src/di/Container.ts +98 -0
- package/packages/nexus-core/src/di/index.ts +1 -0
- package/packages/nexus-core/src/errors.ts +62 -0
- package/packages/nexus-core/src/http/Router.ts +149 -0
- package/packages/nexus-core/src/http/Server.ts +145 -0
- package/packages/nexus-core/src/http/bodyParser.ts +129 -0
- package/packages/nexus-core/src/http/context.ts +112 -0
- package/packages/nexus-core/src/http/index.ts +6 -0
- package/packages/nexus-core/src/http/static.ts +85 -0
- package/packages/nexus-core/src/http/uploads.ts +94 -0
- package/packages/nexus-core/src/index.ts +4 -0
- package/packages/nexus-crypto/package.json +18 -0
- package/packages/nexus-crypto/src/csr.ts +165 -0
- package/packages/nexus-crypto/src/der.ts +262 -0
- package/packages/nexus-crypto/src/https.ts +36 -0
- package/packages/nexus-crypto/src/index.ts +5 -0
- package/packages/nexus-crypto/src/keypair.ts +59 -0
- package/packages/nexus-crypto/src/x509.ts +156 -0
- package/packages/nexus-data/package.json +27 -0
- package/packages/nexus-data/src/connection/Connection.ts +57 -0
- package/packages/nexus-data/src/connection/ConnectionManager.ts +37 -0
- package/packages/nexus-data/src/errors.ts +17 -0
- package/packages/nexus-data/src/index.ts +66 -0
- package/packages/nexus-data/src/model/Document.ts +126 -0
- package/packages/nexus-data/src/model/Model.ts +189 -0
- package/packages/nexus-data/src/model/hydrate.ts +12 -0
- package/packages/nexus-data/src/populate/populate.ts +114 -0
- package/packages/nexus-data/src/projects.ts +185 -0
- package/packages/nexus-data/src/query/Query.ts +158 -0
- package/packages/nexus-data/src/schema/Schema.ts +173 -0
- package/packages/nexus-data/src/schema/SchemaType.ts +80 -0
- package/packages/nexus-data/src/schema/validators.ts +88 -0
- package/packages/nexus-email/package.json +22 -0
- package/packages/nexus-email/src/EmailService.ts +76 -0
- package/packages/nexus-email/src/TemplateEngine.ts +62 -0
- package/packages/nexus-email/src/index.ts +37 -0
- package/packages/nexus-email/src/providers.ts +63 -0
- package/packages/nexus-email/src/queue.ts +103 -0
- package/packages/nexus-email/src/types.ts +49 -0
- package/packages/nexus-graphql/package.json +24 -0
- package/packages/nexus-graphql/src/federation/composeSupergraph.ts +153 -0
- package/packages/nexus-graphql/src/federation/createFederatedGateway.ts +36 -0
- package/packages/nexus-graphql/src/federation/executor.ts +305 -0
- package/packages/nexus-graphql/src/federation/parseMetadata.ts +87 -0
- package/packages/nexus-graphql/src/gateway/createGateway.ts +77 -0
- package/packages/nexus-graphql/src/gateway/fieldResolver.ts +45 -0
- package/packages/nexus-graphql/src/gateway/graphqlHttpHandler.ts +90 -0
- package/packages/nexus-graphql/src/gateway/publicSchema.ts +26 -0
- package/packages/nexus-graphql/src/index.ts +14 -0
- package/packages/nexus-graphql/src/subgraph/defineSubgraph.ts +115 -0
- package/packages/nexus-graphql/src/subgraph/federationDirectives.ts +47 -0
- package/packages/nexus-graphql/src/subgraph/parseKeys.ts +25 -0
- package/packages/nexus-graphql/src/subscriptions/PubSub.ts +64 -0
- package/packages/nexus-graphql/src/subscriptions/SubscriptionServer.ts +203 -0
- package/packages/nexus-graphql/src/types.ts +77 -0
- package/packages/nexus-payments/package.json +20 -0
- package/packages/nexus-payments/src/errors.ts +15 -0
- package/packages/nexus-payments/src/http.ts +53 -0
- package/packages/nexus-payments/src/index.ts +106 -0
- package/packages/nexus-payments/src/providers/PayPalProvider.ts +192 -0
- package/packages/nexus-payments/src/providers/PayUProvider.ts +164 -0
- package/packages/nexus-payments/src/providers/PayoneerProvider.ts +122 -0
- package/packages/nexus-payments/src/providers/RazorpayProvider.ts +184 -0
- package/packages/nexus-payments/src/providers/SkrillProvider.ts +117 -0
- package/packages/nexus-payments/src/signature.ts +24 -0
- package/packages/nexus-payments/src/types.ts +150 -0
- package/packages/nexus-payments/src/webhooks.ts +58 -0
- package/packages/nexus-plugins/package.json +20 -0
- package/packages/nexus-plugins/src/HookBus.ts +42 -0
- package/packages/nexus-plugins/src/admin/adminExtensions.ts +53 -0
- package/packages/nexus-plugins/src/host.ts +105 -0
- package/packages/nexus-plugins/src/index.ts +8 -0
- package/packages/nexus-plugins/src/manifest.ts +73 -0
- package/packages/nexus-plugins/src/registry.ts +123 -0
- package/packages/nexus-plugins/src/runtime/TrustedPlugin.ts +134 -0
- package/packages/nexus-plugins/src/sandbox/SandboxedPlugin.ts +279 -0
- package/packages/nexus-plugins/src/sandbox/capabilityPolicy.ts +68 -0
- package/packages/nexus-plugins/src/sandbox/workerBootstrap.mjs +211 -0
- package/packages/nexus-realtime/package.json +26 -0
- package/packages/nexus-realtime/src/adapter/PubSubAdapter.ts +42 -0
- package/packages/nexus-realtime/src/adapter/RedisPubSubAdapter.ts +59 -0
- package/packages/nexus-realtime/src/index.ts +5 -0
- package/packages/nexus-realtime/src/mediasoup/MediasoupAdapter.ts +176 -0
- package/packages/nexus-realtime/src/server/RealtimeServer.ts +252 -0
- package/packages/nexus-realtime/src/types.ts +52 -0
- package/packages/nexus-telemetry/package.json +20 -0
- package/packages/nexus-telemetry/src/index.ts +3 -0
- package/packages/nexus-telemetry/src/logger/Logger.ts +178 -0
- package/packages/nexus-telemetry/src/metrics/Metrics.ts +123 -0
- package/packages/nexus-telemetry/src/trace.ts +28 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { createClient, type RedisClientType } from 'redis';
|
|
2
|
+
import type { CacheOptions } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Redis-backed cache wrapper. Lazily creates and connects a `node-redis`
|
|
6
|
+
* client on first use. All keys pass through {@link Cache.key} to apply an
|
|
7
|
+
* optional namespace/prefix.
|
|
8
|
+
*/
|
|
9
|
+
export class Cache {
|
|
10
|
+
private client: RedisClientType;
|
|
11
|
+
private connectPromise: Promise<void> | null = null;
|
|
12
|
+
private readonly keyPrefix: string;
|
|
13
|
+
protected readonly url?: string;
|
|
14
|
+
protected readonly connectTimeout?: number;
|
|
15
|
+
|
|
16
|
+
constructor(opts: CacheOptions = {}) {
|
|
17
|
+
this.url = opts.url;
|
|
18
|
+
this.connectTimeout = opts.connectTimeout;
|
|
19
|
+
this.keyPrefix = opts.keyPrefix ?? (opts.namespace ? `${opts.namespace}:` : '');
|
|
20
|
+
// `redis` v4 createClient returns a client in a closed state; we connect
|
|
21
|
+
// lazily on first use via ensureConnected().
|
|
22
|
+
this.client = createClient({
|
|
23
|
+
...(this.url ? { url: this.url } : {}),
|
|
24
|
+
...(this.connectTimeout != null ? { socket: { connectTimeout: this.connectTimeout } } : {}),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Connect the underlying client if not already connected. Idempotent. */
|
|
29
|
+
async ensureConnected(): Promise<void> {
|
|
30
|
+
if (this.client.isOpen) return;
|
|
31
|
+
if (!this.connectPromise) {
|
|
32
|
+
this.connectPromise = this.client.connect().then(() => {
|
|
33
|
+
// connected
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
await this.connectPromise;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// Reset so a later retry can attempt reconnection.
|
|
40
|
+
this.connectPromise = null;
|
|
41
|
+
throw err;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Build the fully-prefixed redis key for a logical name. */
|
|
46
|
+
key(name: string): string {
|
|
47
|
+
return `${this.keyPrefix}${name}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The underlying redis client (connected after `ensureConnected`). */
|
|
51
|
+
protected getClient(): RedisClientType {
|
|
52
|
+
return this.client;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Get a JSON-decoded value, or undefined on miss/null. */
|
|
56
|
+
async get<T>(name: string): Promise<T | undefined> {
|
|
57
|
+
await this.ensureConnected();
|
|
58
|
+
const raw = await this.client.get(this.key(name));
|
|
59
|
+
if (raw == null) return undefined;
|
|
60
|
+
return JSON.parse(raw) as T;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Set a JSON-encoded value, optionally with a TTL in seconds. */
|
|
64
|
+
async set<T>(name: string, value: T, ttlSeconds?: number): Promise<void> {
|
|
65
|
+
await this.ensureConnected();
|
|
66
|
+
const encoded = JSON.stringify(value);
|
|
67
|
+
if (ttlSeconds != null) {
|
|
68
|
+
await this.client.set(this.key(name), encoded, { EX: ttlSeconds });
|
|
69
|
+
} else {
|
|
70
|
+
await this.client.set(this.key(name), encoded);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Delete a key. */
|
|
75
|
+
async del(name: string): Promise<void> {
|
|
76
|
+
await this.ensureConnected();
|
|
77
|
+
await this.client.del(this.key(name));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* INCR a key. When `ttlSeconds` is supplied and the result becomes 1 (first
|
|
82
|
+
* hit in a fresh window) the key is expired after `ttlSeconds` seconds.
|
|
83
|
+
* Returns the post-increment integer value.
|
|
84
|
+
*/
|
|
85
|
+
async incr(name: string, ttlSeconds?: number): Promise<number> {
|
|
86
|
+
await this.ensureConnected();
|
|
87
|
+
const key = this.key(name);
|
|
88
|
+
const count = await this.client.incr(key);
|
|
89
|
+
if (ttlSeconds != null && count === 1) {
|
|
90
|
+
await this.client.expire(key, ttlSeconds);
|
|
91
|
+
}
|
|
92
|
+
return count;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether a key exists. */
|
|
96
|
+
async exists(name: string): Promise<boolean> {
|
|
97
|
+
await this.ensureConnected();
|
|
98
|
+
const n = await this.client.exists(this.key(name));
|
|
99
|
+
return n > 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Set an expiry (seconds) on an existing key. */
|
|
103
|
+
async expire(name: string, ttlSeconds: number): Promise<void> {
|
|
104
|
+
await this.ensureConnected();
|
|
105
|
+
await this.client.expire(this.key(name), ttlSeconds);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Remaining TTL in seconds (-1 no expiry, -2 missing key). */
|
|
109
|
+
async ttl(name: string): Promise<number> {
|
|
110
|
+
await this.ensureConnected();
|
|
111
|
+
return this.client.ttl(this.key(name));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Delete all keys matching a glob pattern using non-blocking SCAN + DEL.
|
|
116
|
+
* Returns the number of keys removed.
|
|
117
|
+
*/
|
|
118
|
+
async flushPattern(pattern: string): Promise<number> {
|
|
119
|
+
await this.ensureConnected();
|
|
120
|
+
const fullPattern = `${this.keyPrefix}${pattern}`;
|
|
121
|
+
let cursor = 0;
|
|
122
|
+
let removed = 0;
|
|
123
|
+
do {
|
|
124
|
+
const reply = await this.client.scan(cursor, { MATCH: fullPattern, COUNT: 100 });
|
|
125
|
+
cursor = Number(reply.cursor);
|
|
126
|
+
const keys = reply.keys;
|
|
127
|
+
if (keys.length > 0) {
|
|
128
|
+
await this.client.del(keys);
|
|
129
|
+
removed += keys.length;
|
|
130
|
+
}
|
|
131
|
+
} while (cursor !== 0);
|
|
132
|
+
return removed;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Cache-aside helper: return the cached value if present, otherwise invoke
|
|
137
|
+
* `loader`, store its result with `ttlSeconds`, and return it. No negative
|
|
138
|
+
* caching in v1 — loader errors propagate to the caller.
|
|
139
|
+
*/
|
|
140
|
+
async cacheAside<T>(name: string, ttlSeconds: number, loader: () => Promise<T>): Promise<T> {
|
|
141
|
+
const cached = await this.get<T>(name);
|
|
142
|
+
if (cached !== undefined) return cached;
|
|
143
|
+
const fresh = await loader();
|
|
144
|
+
await this.set(name, fresh, ttlSeconds);
|
|
145
|
+
return fresh;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Disconnect the underlying client. */
|
|
149
|
+
async close(): Promise<void> {
|
|
150
|
+
if (this.client.isOpen) {
|
|
151
|
+
await this.client.quit();
|
|
152
|
+
}
|
|
153
|
+
this.connectPromise = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createClient, type RedisClientType } from 'redis';
|
|
2
|
+
import type { Cache } from './Cache.js';
|
|
3
|
+
|
|
4
|
+
type Handler = (msg: unknown) => void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Redis pub/sub helper. Uses the {@link Cache} client for publishing and a
|
|
8
|
+
* separate, dedicated subscriber client (required by `redis` v4) for
|
|
9
|
+
* subscriptions. Both clients are connected lazily.
|
|
10
|
+
*/
|
|
11
|
+
export class PubSub {
|
|
12
|
+
private subscriber: RedisClientType;
|
|
13
|
+
private subscriberConnectPromise: Promise<void> | null = null;
|
|
14
|
+
private handlers = new Map<string, Set<Handler>>();
|
|
15
|
+
|
|
16
|
+
constructor(private cache: Cache) {
|
|
17
|
+
// Reuse the same URL/connection options as the cache's publisher client.
|
|
18
|
+
// The cache exposes its url/connectTimeout via protected fields; here we
|
|
19
|
+
// reconstruct from the cache's key prefix-agnostic config by reading the
|
|
20
|
+
// same options the cache used. We mirror the cache's createClient call.
|
|
21
|
+
const url = (cache as unknown as { url?: string }).url;
|
|
22
|
+
const connectTimeout = (cache as unknown as { connectTimeout?: number }).connectTimeout;
|
|
23
|
+
this.subscriber = createClient({
|
|
24
|
+
...(url ? { url } : {}),
|
|
25
|
+
...(connectTimeout != null ? { socket: { connectTimeout } } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private async ensureSubscriberConnected(): Promise<void> {
|
|
30
|
+
if (this.subscriber.isOpen) return;
|
|
31
|
+
if (!this.subscriberConnectPromise) {
|
|
32
|
+
this.subscriberConnectPromise = this.subscriber.connect().then(() => undefined);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await this.subscriberConnectPromise;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
this.subscriberConnectPromise = null;
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Publish a JSON-encoded message to `channel`. */
|
|
43
|
+
async publish(channel: string, message: unknown): Promise<void> {
|
|
44
|
+
// Ensure the cache (publisher) client is connected.
|
|
45
|
+
await this.cache.ensureConnected();
|
|
46
|
+
const publisher = (this.cache as unknown as { getClient: () => RedisClientType }).getClient();
|
|
47
|
+
await publisher.publish(channel, JSON.stringify(message));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Subscribe to `channel`; `handler` is invoked with the JSON-parsed payload.
|
|
52
|
+
* Returns an unsubscribe function that removes the handler and, when no
|
|
53
|
+
* handlers remain for the channel, unsubscribes from Redis.
|
|
54
|
+
*/
|
|
55
|
+
async subscribe(channel: string, handler: Handler): Promise<() => Promise<void>> {
|
|
56
|
+
await this.ensureSubscriberConnected();
|
|
57
|
+
let set = this.handlers.get(channel);
|
|
58
|
+
if (!set) {
|
|
59
|
+
set = new Set();
|
|
60
|
+
this.handlers.set(channel, set);
|
|
61
|
+
// `redis` v4 subscribe(channel, callback) registers a per-channel
|
|
62
|
+
// listener that receives the message string.
|
|
63
|
+
await this.subscriber.subscribe(channel, (message) => {
|
|
64
|
+
const handlers = this.handlers.get(channel);
|
|
65
|
+
if (!handlers) return;
|
|
66
|
+
let parsed: unknown;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(message);
|
|
69
|
+
} catch {
|
|
70
|
+
parsed = message;
|
|
71
|
+
}
|
|
72
|
+
for (const h of handlers) h(parsed);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
set.add(handler);
|
|
76
|
+
return async () => {
|
|
77
|
+
const current = this.handlers.get(channel);
|
|
78
|
+
if (!current) return;
|
|
79
|
+
current.delete(handler);
|
|
80
|
+
if (current.size === 0) {
|
|
81
|
+
this.handlers.delete(channel);
|
|
82
|
+
try {
|
|
83
|
+
await this.subscriber.unsubscribe(channel);
|
|
84
|
+
} catch {
|
|
85
|
+
/* ignore — client may be closing */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Close the dedicated subscriber client. */
|
|
92
|
+
async close(): Promise<void> {
|
|
93
|
+
if (this.subscriber.isOpen) {
|
|
94
|
+
await this.subscriber.quit();
|
|
95
|
+
}
|
|
96
|
+
this.subscriberConnectPromise = null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Cache } from './Cache.js';
|
|
2
|
+
import type { RateLimitResult, RateLimitRule } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fixed-window counter rate limiter backed by Redis INCR via a {@link Cache}.
|
|
6
|
+
* Each call to {@link RateLimiter.check} atomically increments a per-key
|
|
7
|
+
* counter and, on the first hit of a window, sets the window expiry.
|
|
8
|
+
*/
|
|
9
|
+
export class RateLimiter {
|
|
10
|
+
constructor(private cache: Cache) {}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check (and consume) one unit against the rule for `key`. Redis INCR both
|
|
14
|
+
* increments and returns the count, so the counter is consumed regardless of
|
|
15
|
+
* whether the request is allowed — matching the in-memory store semantics.
|
|
16
|
+
*/
|
|
17
|
+
async check(key: string, rule: RateLimitRule): Promise<RateLimitResult> {
|
|
18
|
+
const redisKey = `${key}:rate`;
|
|
19
|
+
const count = await this.cache.incr(redisKey, rule.windowSeconds);
|
|
20
|
+
const limit = rule.max;
|
|
21
|
+
const allowed = count <= limit;
|
|
22
|
+
const remaining = Math.max(0, limit - count);
|
|
23
|
+
// Use the live TTL to compute resetAt; falls back to now + window.
|
|
24
|
+
let ttl = await this.cache.ttl(redisKey);
|
|
25
|
+
if (ttl < 0) ttl = rule.windowSeconds;
|
|
26
|
+
const resetAt = Date.now() + ttl * 1000;
|
|
27
|
+
const result: RateLimitResult = {
|
|
28
|
+
allowed,
|
|
29
|
+
limit,
|
|
30
|
+
remaining,
|
|
31
|
+
resetAt,
|
|
32
|
+
};
|
|
33
|
+
if (!allowed) {
|
|
34
|
+
result.retryAfter = rule.windowSeconds;
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Alias of {@link check}; the counter is already consumed by INCR. */
|
|
40
|
+
async consume(key: string, rule: RateLimitRule): Promise<RateLimitResult> {
|
|
41
|
+
return this.check(key, rule);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for constructing a {@link Cache} instance.
|
|
3
|
+
*/
|
|
4
|
+
export interface CacheOptions {
|
|
5
|
+
/** Redis connection URL (e.g. `redis://localhost:6379`). */
|
|
6
|
+
url?: string;
|
|
7
|
+
/** Logical namespace applied to all keys as `${namespace}:` prefix. */
|
|
8
|
+
namespace?: string;
|
|
9
|
+
/** Explicit key prefix (overrides namespace prefix when provided). */
|
|
10
|
+
keyPrefix?: string;
|
|
11
|
+
/** Connection timeout in milliseconds passed to the redis client. */
|
|
12
|
+
connectTimeout?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A cached value entry (kept for symmetry; values are JSON-stringified as-is).
|
|
17
|
+
*/
|
|
18
|
+
export interface CacheEntry<T> {
|
|
19
|
+
value: T;
|
|
20
|
+
expiresAt?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Result of a rate-limit check.
|
|
25
|
+
*/
|
|
26
|
+
export interface RateLimitResult {
|
|
27
|
+
/** Whether the request is allowed under the current window. */
|
|
28
|
+
allowed: boolean;
|
|
29
|
+
/** Maximum requests permitted in the window. */
|
|
30
|
+
limit: number;
|
|
31
|
+
/** Remaining requests in the current window (>= 0). */
|
|
32
|
+
remaining: number;
|
|
33
|
+
/** Epoch ms when the current window resets. */
|
|
34
|
+
resetAt: number;
|
|
35
|
+
/** Seconds until the next request would be allowed (present when denied). */
|
|
36
|
+
retryAfter?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Fixed-window rate-limit rule.
|
|
41
|
+
*/
|
|
42
|
+
export interface RateLimitRule {
|
|
43
|
+
/** Window length in seconds. */
|
|
44
|
+
windowSeconds: number;
|
|
45
|
+
/** Maximum requests permitted per window. */
|
|
46
|
+
max: number;
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bhooai/nexus-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"nexus": "./bin/nexus.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc -p tsconfig.json",
|
|
13
|
+
"test": "vitest run"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@bhooai/nexus-cluster": "^0.1.0",
|
|
17
|
+
"@bhooai/nexus-core": "^0.1.0",
|
|
18
|
+
"@bhooai/nexus-data": "^0.1.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.5.0",
|
|
22
|
+
"typescript": "^5.6.2",
|
|
23
|
+
"vitest": "^2.1.1"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { loadConfigAuto } from '../../../nexus-core/src/index.js';
|
|
4
|
+
import { ClusterManager } from '@bhooai/nexus-cluster';
|
|
5
|
+
import type { DeepPartial, NexusConfig } from '@bhooai/nexus-core';
|
|
6
|
+
|
|
7
|
+
const GREEN = '\x1b[32m';
|
|
8
|
+
const RED = '\x1b[31m';
|
|
9
|
+
const YELLOW = '\x1b[33m';
|
|
10
|
+
const CYAN = '\x1b[36m';
|
|
11
|
+
const DIM = '\x1b[2m';
|
|
12
|
+
const RESET = '\x1b[0m';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `nexus cluster <subcommand>` - central-side mesh orchestration.
|
|
16
|
+
*
|
|
17
|
+
* cluster status list linked nodes + LB RPS
|
|
18
|
+
* cluster link <url> handshake + register a node by its agent URL
|
|
19
|
+
* cluster unlink <id> forget a node
|
|
20
|
+
* cluster start|stop|restart|kill <id>
|
|
21
|
+
* cluster scale <n> target exactly n backend nodes
|
|
22
|
+
* cluster serve hold process: LB + autoscaler (foreground)
|
|
23
|
+
* cluster check poll every node once (health + metrics)
|
|
24
|
+
*/
|
|
25
|
+
export async function cluster(args: string[]): Promise<number> {
|
|
26
|
+
const cfg = await loadConfigAuto({ root: process.cwd() });
|
|
27
|
+
const [sub, a, b] = args;
|
|
28
|
+
|
|
29
|
+
// `serve` is the explicit "start the cluster" command - if a prior admin
|
|
30
|
+
// stop wrote {cluster:{enabled:false}} into nexus.runtime.json, that
|
|
31
|
+
// override wins over nexus.config.ts and would silently keep the cluster
|
|
32
|
+
// off. Re-enable it in runtime.json so the LB actually starts.
|
|
33
|
+
if (sub === 'serve' && !cfg.cluster.enabled) {
|
|
34
|
+
enableClusterInRuntime(process.cwd());
|
|
35
|
+
console.log(`${YELLOW}cluster was disabled in nexus.runtime.json - re-enabling${RESET}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// For non-serve commands, warn (don't block) when the cluster is off.
|
|
39
|
+
if (sub !== 'serve' && !cfg.cluster.enabled) {
|
|
40
|
+
console.warn(`${RED}cluster.enabled is false${RESET} - run ${CYAN}nexus cluster serve${RESET} to start it, or set cluster: { enabled: true } in nexus.config.ts`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const manager = new ClusterManager({ config: cfg.cluster, root: process.cwd(), aiServerUrl: cfg.ai.serverUrl });
|
|
44
|
+
|
|
45
|
+
switch (sub ?? 'help') {
|
|
46
|
+
case 'status': {
|
|
47
|
+
const nodes = manager.list();
|
|
48
|
+
const rps = manager.rps();
|
|
49
|
+
console.log(`${CYAN}cluster${RESET} - ${nodes.length} linked node(s)`);
|
|
50
|
+
for (const n of nodes) {
|
|
51
|
+
const color = n.status === 'ready' ? GREEN : n.status === 'unreachable' ? RED : CYAN;
|
|
52
|
+
console.log(` ${color}${n.status}${RESET} ${DIM}${n.identity.id}${RESET} role=${n.identity.role} tier=${n.identity.tier} enabled=${n.enabled} rps=${rps[n.identity.id] ?? 0}${n.lastMetrics ? ` cpu=${n.lastMetrics.cpu}% mem=${n.lastMetrics.memoryMb}MiB` : ''}`);
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
case 'link': {
|
|
57
|
+
if (!a) { console.error('cluster link <agent-url>'); return 1; }
|
|
58
|
+
try {
|
|
59
|
+
const node = await manager.link(a);
|
|
60
|
+
const color = node.status === 'ready' ? GREEN : CYAN;
|
|
61
|
+
console.log(`${color}linked${RESET} ${node.identity.id} role=${node.identity.role} base=${node.identity.baseUrl}`);
|
|
62
|
+
return 0;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(`${RED}link failed${RESET}: ${(err as Error).message}`);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
case 'unlink': {
|
|
69
|
+
if (!a) { console.error('cluster unlink <id>'); return 1; }
|
|
70
|
+
return manager.unlink(a) ? (console.log(`removed ${a}`), 0) : (console.error(`no such node: ${a}`), 1);
|
|
71
|
+
}
|
|
72
|
+
case 'start':
|
|
73
|
+
case 'stop':
|
|
74
|
+
case 'restart':
|
|
75
|
+
case 'kill': {
|
|
76
|
+
if (!a) { console.error(`cluster ${sub} <id>`); return 1; }
|
|
77
|
+
await manager.exec(a, sub === 'kill' ? 'stop' : sub);
|
|
78
|
+
console.log(`${sub} ${a}`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
case 'scale': {
|
|
82
|
+
if (!a || !Number.isFinite(Number(a))) { console.error('cluster scale <n>'); return 1; }
|
|
83
|
+
try {
|
|
84
|
+
await manager.scaleTo(Number(a));
|
|
85
|
+
console.log(`scaled to ${a} backend node(s)`);
|
|
86
|
+
return 0;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(`${RED}${(err as Error).message}${RESET}`);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
case 'check': {
|
|
93
|
+
await manager.pollAll();
|
|
94
|
+
console.log(`checked ${manager.list().length} node(s)`);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
case 'serve': {
|
|
98
|
+
console.log(`${CYAN}cluster${RESET} LB on ${cfg.cluster.lbHost}:${cfg.cluster.lbPort}${RESET}`);
|
|
99
|
+
await manager.listenLb(cfg.cluster.lbHost);
|
|
100
|
+
manager.autoscaler.start(10_000);
|
|
101
|
+
setInterval(() => void manager.pollAll().catch(() => {}), 15_000);
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
case 'help':
|
|
105
|
+
default:
|
|
106
|
+
console.log(`Usage: nexus cluster <status|link|unlink|start|stop|restart|kill|scale|check|serve>`);
|
|
107
|
+
return 0;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Patch `nexus.runtime.json` to set `cluster.enabled = true`. The admin
|
|
113
|
+
* "stop cluster" endpoint writes `{cluster:{enabled:false}}` there, and that
|
|
114
|
+
* override beats `nexus.config.ts` in the loader precedence - so `nexus
|
|
115
|
+
* cluster serve` would read `enabled:false` and the LB would never bind.
|
|
116
|
+
* This removes the stale override so `serve` actually starts the cluster.
|
|
117
|
+
*/
|
|
118
|
+
function enableClusterInRuntime(root: string): void {
|
|
119
|
+
const runtimePath = resolve(root, 'nexus.runtime.json');
|
|
120
|
+
let runtime: Record<string, unknown> = {};
|
|
121
|
+
if (existsSync(runtimePath)) {
|
|
122
|
+
try {
|
|
123
|
+
runtime = JSON.parse(readFileSync(runtimePath, 'utf8')) as Record<string, unknown>;
|
|
124
|
+
} catch {
|
|
125
|
+
// corrupt runtime file - start fresh
|
|
126
|
+
runtime = {};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const cluster = (runtime['cluster'] ?? {}) as Record<string, unknown>;
|
|
130
|
+
cluster['enabled'] = true;
|
|
131
|
+
runtime['cluster'] = cluster;
|
|
132
|
+
writeFileSync(runtimePath, `${JSON.stringify(runtime, null, 2)}\n`);
|
|
133
|
+
}
|