tempest-express-sdk 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -5,7 +5,9 @@ var zodToOpenapi = require('@asteasolutions/zod-to-openapi');
5
5
  var zod = require('zod');
6
6
  var tempestDbJs = require('tempest-db-js');
7
7
  var crypto = require('crypto');
8
+ var child_process = require('child_process');
8
9
  var os = require('os');
10
+ var util = require('util');
9
11
  var promises = require('fs/promises');
10
12
  var path = require('path');
11
13
  var express2 = require('express');
@@ -7011,6 +7013,7 @@ var HTTPClient = class {
7011
7013
  return this.request("DELETE", url, init);
7012
7014
  }
7013
7015
  };
7016
+ var execFileAsync = util.promisify(child_process.execFile);
7014
7017
  function readCpu() {
7015
7018
  const cores = os.cpus().length;
7016
7019
  const load1 = os.loadavg()[0] ?? 0;
@@ -7039,7 +7042,27 @@ function readSystem() {
7039
7042
  uptimeSeconds: process.uptime()
7040
7043
  };
7041
7044
  }
7042
- function toPrometheus(snapshot = readSystem()) {
7045
+ async function readGpus() {
7046
+ try {
7047
+ const { stdout } = await execFileAsync("nvidia-smi", [
7048
+ "--query-gpu=index,utilization.gpu,memory.used,memory.total,temperature.gpu",
7049
+ "--format=csv,noheader,nounits"
7050
+ ]);
7051
+ return stdout.trim().split("\n").filter((line) => line.trim().length > 0).map((line) => {
7052
+ const [index, util, used, total, temp] = line.split(",").map((v) => Number(v.trim()));
7053
+ return {
7054
+ index: index ?? 0,
7055
+ utilizationPercent: util ?? 0,
7056
+ memoryUsedMb: used ?? 0,
7057
+ memoryTotalMb: total ?? 0,
7058
+ temperatureC: temp ?? 0
7059
+ };
7060
+ });
7061
+ } catch {
7062
+ return [];
7063
+ }
7064
+ }
7065
+ function toPrometheus(snapshot = readSystem(), gpus = []) {
7043
7066
  const lines = [
7044
7067
  "# HELP process_cpu_load_percent 1-minute load average as percent of cores",
7045
7068
  "# TYPE process_cpu_load_percent gauge",
@@ -7054,6 +7077,29 @@ function toPrometheus(snapshot = readSystem()) {
7054
7077
  "# TYPE process_uptime_seconds counter",
7055
7078
  `process_uptime_seconds ${snapshot.uptimeSeconds}`
7056
7079
  ];
7080
+ if (gpus.length > 0) {
7081
+ lines.push(
7082
+ "# HELP gpu_utilization_percent GPU utilization percent",
7083
+ "# TYPE gpu_utilization_percent gauge"
7084
+ );
7085
+ for (const gpu of gpus) {
7086
+ lines.push(`gpu_utilization_percent{gpu="${gpu.index}"} ${gpu.utilizationPercent}`);
7087
+ }
7088
+ lines.push(
7089
+ "# HELP gpu_memory_used_mb GPU memory used in MiB",
7090
+ "# TYPE gpu_memory_used_mb gauge"
7091
+ );
7092
+ for (const gpu of gpus) {
7093
+ lines.push(`gpu_memory_used_mb{gpu="${gpu.index}"} ${gpu.memoryUsedMb}`);
7094
+ }
7095
+ lines.push(
7096
+ "# HELP gpu_temperature_celsius GPU core temperature",
7097
+ "# TYPE gpu_temperature_celsius gauge"
7098
+ );
7099
+ for (const gpu of gpus) {
7100
+ lines.push(`gpu_temperature_celsius{gpu="${gpu.index}"} ${gpu.temperatureC}`);
7101
+ }
7102
+ }
7057
7103
  return `${lines.join("\n")}
7058
7104
  `;
7059
7105
  }
@@ -7061,6 +7107,7 @@ var MetricsUtils = {
7061
7107
  cpu: readCpu,
7062
7108
  memory: readMemory,
7063
7109
  system: readSystem,
7110
+ gpus: readGpus,
7064
7111
  toPrometheus
7065
7112
  };
7066
7113
 
@@ -7329,6 +7376,76 @@ function makeSessionMiddleware(service, options = {}) {
7329
7376
  };
7330
7377
  }
7331
7378
 
7379
+ // src/sessions/redisStore.ts
7380
+ var RedisSessionStore = class {
7381
+ /**
7382
+ * @param client - A connected node-redis v4 (or compatible) client.
7383
+ * @param prefix - Key prefix. Default `sess:`.
7384
+ */
7385
+ constructor(client, prefix = "sess:") {
7386
+ this.client = client;
7387
+ this.prefix = prefix;
7388
+ }
7389
+ client;
7390
+ prefix;
7391
+ key(idHash) {
7392
+ return `${this.prefix}${idHash}`;
7393
+ }
7394
+ userKey(userId) {
7395
+ return `${this.prefix}user:${userId}`;
7396
+ }
7397
+ async get(idHash) {
7398
+ const raw = await this.client.get(this.key(idHash));
7399
+ if (raw === null) return null;
7400
+ const session = JSON.parse(raw);
7401
+ if (session.expiresAt <= Date.now()) {
7402
+ await this.delete(idHash);
7403
+ return null;
7404
+ }
7405
+ return session;
7406
+ }
7407
+ async set(session) {
7408
+ const ttlSeconds = Math.max(1, Math.ceil((session.expiresAt - Date.now()) / 1e3));
7409
+ await this.client.set(this.key(session.idHash), JSON.stringify(session), {
7410
+ EX: ttlSeconds
7411
+ });
7412
+ await this.client.sAdd(this.userKey(session.userId), session.idHash);
7413
+ }
7414
+ async delete(idHash) {
7415
+ const raw = await this.client.get(this.key(idHash));
7416
+ await this.client.del(this.key(idHash));
7417
+ if (raw) {
7418
+ const session = JSON.parse(raw);
7419
+ await this.client.sRem(this.userKey(session.userId), idHash);
7420
+ }
7421
+ }
7422
+ async deleteByUser(userId) {
7423
+ const ids = await this.client.sMembers(this.userKey(userId));
7424
+ let count = 0;
7425
+ for (const idHash of ids) {
7426
+ await this.client.del(this.key(idHash));
7427
+ await this.client.sRem(this.userKey(userId), idHash);
7428
+ count += 1;
7429
+ }
7430
+ return count;
7431
+ }
7432
+ async listByUser(userId) {
7433
+ const ids = await this.client.sMembers(this.userKey(userId));
7434
+ const sessions = [];
7435
+ const now = Date.now();
7436
+ for (const idHash of ids) {
7437
+ const raw = await this.client.get(this.key(idHash));
7438
+ if (raw === null) {
7439
+ await this.client.sRem(this.userKey(userId), idHash);
7440
+ continue;
7441
+ }
7442
+ const session = JSON.parse(raw);
7443
+ if (session.expiresAt > now) sessions.push(session);
7444
+ }
7445
+ return sessions.sort((a, b) => a.createdAt - b.createdAt);
7446
+ }
7447
+ };
7448
+
7332
7449
  // src/sse/eventStream.ts
7333
7450
  var ServerSentEvent = class {
7334
7451
  constructor(init) {
@@ -7501,6 +7618,92 @@ var SSEBroker = class {
7501
7618
  }
7502
7619
  };
7503
7620
 
7621
+ // src/sse/redisBroker.ts
7622
+ var RedisSSEBroker = class {
7623
+ /**
7624
+ * @param publisher - The main Redis client (used to `publish`).
7625
+ * @param subscriber - A dedicated subscriber connection (`client.duplicate()`).
7626
+ * @param options - Channel prefix + per-stream options.
7627
+ */
7628
+ constructor(publisher, subscriber, options = {}) {
7629
+ this.publisher = publisher;
7630
+ this.subscriber = subscriber;
7631
+ this.prefix = options.prefix ?? "sse:";
7632
+ const { prefix: _p, ...streamOptions } = options;
7633
+ this.streamOptions = streamOptions;
7634
+ }
7635
+ publisher;
7636
+ subscriber;
7637
+ local = /* @__PURE__ */ new Map();
7638
+ prefix;
7639
+ streamOptions;
7640
+ channelKey(channel) {
7641
+ return `${this.prefix}${channel}`;
7642
+ }
7643
+ /** Emit a decoded payload to every local stream on a channel. */
7644
+ emitLocal(channel, data, event) {
7645
+ const set = this.local.get(channel);
7646
+ if (!set) return;
7647
+ for (const stream of set) stream.publish(data, event);
7648
+ }
7649
+ /**
7650
+ * Register a subscriber stream, subscribing to the Redis channel on first use.
7651
+ *
7652
+ * @param channel - The channel name.
7653
+ * @returns A fresh {@link EventStream} to serve to the client.
7654
+ */
7655
+ async register(channel) {
7656
+ const stream = new EventStream(this.streamOptions);
7657
+ let set = this.local.get(channel);
7658
+ if (!set) {
7659
+ set = /* @__PURE__ */ new Set();
7660
+ this.local.set(channel, set);
7661
+ await this.subscriber.subscribe(this.channelKey(channel), (raw) => {
7662
+ try {
7663
+ const { data, event } = JSON.parse(raw);
7664
+ this.emitLocal(channel, data, event);
7665
+ } catch {
7666
+ }
7667
+ });
7668
+ }
7669
+ set.add(stream);
7670
+ return stream;
7671
+ }
7672
+ /**
7673
+ * Remove a subscriber stream; unsubscribe from Redis when the last leaves.
7674
+ *
7675
+ * @param channel - The channel name.
7676
+ * @param stream - The stream to remove.
7677
+ */
7678
+ async unregister(channel, stream) {
7679
+ const set = this.local.get(channel);
7680
+ if (!set) return;
7681
+ set.delete(stream);
7682
+ stream.close();
7683
+ if (set.size === 0) {
7684
+ this.local.delete(channel);
7685
+ await this.subscriber.unsubscribe(this.channelKey(channel));
7686
+ }
7687
+ }
7688
+ /** Local subscriber count on `channel` (this replica only). */
7689
+ localSubscribers(channel) {
7690
+ return this.local.get(channel)?.size ?? 0;
7691
+ }
7692
+ /**
7693
+ * Publish to every subscriber across all replicas.
7694
+ *
7695
+ * @param channel - The channel name.
7696
+ * @param data - The payload (JSON-encoded).
7697
+ * @param event - Optional event name.
7698
+ */
7699
+ async publish(channel, data, event) {
7700
+ await this.publisher.publish(
7701
+ this.channelKey(channel),
7702
+ JSON.stringify({ data, ...event ? { event } : {} })
7703
+ );
7704
+ }
7705
+ };
7706
+
7504
7707
  // src/websockets/schemas.ts
7505
7708
  var wsEnvelopeSchema = zod.z.object({
7506
7709
  type: zod.z.string().openapi({ description: "Message type discriminator." }),
@@ -9161,6 +9364,16 @@ function makeHealthRouter(options = {}) {
9161
9364
  });
9162
9365
  return router;
9163
9366
  }
9367
+ function makeMetricsRouter(options = {}) {
9368
+ const path = options.path ?? "/metrics";
9369
+ const router = express2.Router();
9370
+ if (options.guard) router.use(path, options.guard);
9371
+ router.get(path, async (_req, res) => {
9372
+ const gpus = options.includeGpu ? await MetricsUtils.gpus() : [];
9373
+ res.type("text/plain").send(MetricsUtils.toPrometheus(MetricsUtils.system(), gpus));
9374
+ });
9375
+ return router;
9376
+ }
9164
9377
  var logger3 = new JSONLogger("tempest_express_sdk.api.server");
9165
9378
  function corsMiddleware(origins) {
9166
9379
  const allowAll = origins === "*";
@@ -9227,7 +9440,7 @@ function runServer(app, options = {}) {
9227
9440
  }
9228
9441
 
9229
9442
  // src/version.ts
9230
- var VERSION = "0.7.0";
9443
+ var VERSION = "0.9.0";
9231
9444
 
9232
9445
  Object.defineProperty(exports, "OpenAPIRegistry", {
9233
9446
  enumerable: true,
@@ -9422,6 +9635,8 @@ exports.PasswordUtils = PasswordUtils;
9422
9635
  exports.REQUEST_ID_HEADER = REQUEST_ID_HEADER;
9423
9636
  exports.RabbitBroker = RabbitBroker;
9424
9637
  exports.RedisCacheManager = RedisCacheManager;
9638
+ exports.RedisSSEBroker = RedisSSEBroker;
9639
+ exports.RedisSessionStore = RedisSessionStore;
9425
9640
  exports.Region = Region;
9426
9641
  exports.RetryPolicy = RetryPolicy;
9427
9642
  exports.SSEBroker = SSEBroker;
@@ -9496,6 +9711,7 @@ exports.makeAuthRouter = makeAuthRouter;
9496
9711
  exports.makeFlagGuard = makeFlagGuard;
9497
9712
  exports.makeHealthRouter = makeHealthRouter;
9498
9713
  exports.makeJwtAuthMiddleware = makeJwtAuthMiddleware;
9714
+ exports.makeMetricsRouter = makeMetricsRouter;
9499
9715
  exports.makeSessionMiddleware = makeSessionMiddleware;
9500
9716
  exports.makeTwilioWebhookRouter = makeTwilioWebhookRouter;
9501
9717
  exports.makeUnhandledExceptionHandler = makeUnhandledExceptionHandler;