@tradejs/infra 3.1.8-beta.207 → 3.1.8-beta.210

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.
@@ -27,17 +27,17 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  ));
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
 
30
- // src/runtimeStrategyReleases.ts
31
- var runtimeStrategyReleases_exports = {};
32
- __export(runtimeStrategyReleases_exports, {
33
- assertRuntimeStrategyReleaseConfig: () => assertRuntimeStrategyReleaseConfig,
34
- getRuntimeStrategyRelease: () => getRuntimeStrategyRelease,
35
- listRuntimeStrategyReleases: () => listRuntimeStrategyReleases,
36
- publishRuntimeStrategyRelease: () => publishRuntimeStrategyRelease,
30
+ // src/runtimeControls.ts
31
+ var runtimeControls_exports = {};
32
+ __export(runtimeControls_exports, {
33
+ emptyRuntimeControls: () => emptyRuntimeControls,
34
+ getRuntimeControls: () => getRuntimeControls,
35
+ pauseRuntimeStrategy: () => pauseRuntimeStrategy,
37
36
  recordRuntimeStrategyControlEvent: () => recordRuntimeStrategyControlEvent,
38
- verifyRuntimeStrategyRelease: () => verifyRuntimeStrategyRelease
37
+ resumeRuntimeStrategy: () => resumeRuntimeStrategy,
38
+ verifyRuntimeControls: () => verifyRuntimeControls
39
39
  });
40
- module.exports = __toCommonJS(runtimeStrategyReleases_exports);
40
+ module.exports = __toCommonJS(runtimeControls_exports);
41
41
  var import_node_crypto = require("crypto");
42
42
  var import_types = require("@tradejs/types");
43
43
 
@@ -158,152 +158,92 @@ var toResultString = (result) => {
158
158
  var DEFAULT_OPTIONS = {
159
159
  expire: TTL_1D
160
160
  };
161
- var parseJsonOrDeleteKey = async (redis, key, raw, fallback) => {
162
- try {
163
- return JSON.parse(raw);
164
- } catch (e) {
165
- logger.log("error", "failed JSON.parse(%s): %s", key, String(e));
166
- await redis.del(key);
167
- return fallback;
161
+ var requireReadyRedis = async () => {
162
+ if (redisUnavailable) {
163
+ throw new Error("Redis is unavailable");
168
164
  }
169
- };
170
- var getKeys = async (prefix) => {
171
- if (redisUnavailable) return [];
172
165
  const redis = await getReadyRedis();
173
- if (!redis) return [];
174
- const keys = [];
166
+ if (!redis) {
167
+ throw new Error("Redis is unavailable");
168
+ }
169
+ return redis;
170
+ };
171
+ var parseJsonStrict = (key, raw) => {
175
172
  try {
176
- let cursor = "0";
177
- do {
178
- const [nextCursor, batch] = await redis.scan(
179
- cursor,
180
- "MATCH",
181
- `${prefix}*`,
182
- "COUNT",
183
- "200"
184
- );
185
- cursor = nextCursor;
186
- for (const key of batch) {
187
- if (key.startsWith(prefix)) {
188
- keys.push(key);
189
- }
190
- }
191
- } while (cursor !== "0");
192
- } catch (e) {
193
- if (e instanceof Error && isRedisConnectivityError(e)) {
194
- markRedisUnavailable(e);
195
- return [];
196
- }
197
- logger.log("warn", "failed SCAN for %s: %s", prefix, String(e));
198
- return [];
173
+ return JSON.parse(raw);
174
+ } catch (error) {
175
+ throw new Error(`Invalid JSON stored at ${key}`, { cause: error });
199
176
  }
200
- return keys;
201
177
  };
202
- var getData = async (key, fallback = []) => {
203
- if (redisUnavailable) return fallback;
204
- const redis = await getReadyRedis();
205
- if (!redis) return fallback;
178
+ var getDataStrict = async (key) => {
179
+ const redis = await requireReadyRedis();
206
180
  try {
207
- const rawJson = await redis.call("JSON.GET", key);
208
- const raw = toResultString(rawJson);
209
- if (raw == null) return fallback;
210
- return parseJsonOrDeleteKey(redis, key, raw, fallback);
211
- } catch (e) {
212
- if (e instanceof Error && isRedisConnectivityError(e)) {
213
- markRedisUnavailable(e);
214
- return fallback;
181
+ const raw = toResultString(await redis.call("JSON.GET", key));
182
+ return raw == null ? null : parseJsonStrict(key, raw);
183
+ } catch (error) {
184
+ if (error instanceof Error && isRedisConnectivityError(error)) {
185
+ markRedisUnavailable(error);
186
+ throw error;
187
+ }
188
+ if (error instanceof Error && error.message.startsWith("Invalid JSON")) {
189
+ throw error;
215
190
  }
216
191
  logger.log(
217
192
  "error",
218
- "failed JSON.GET %s: %s (fallback to GET)",
193
+ "failed strict JSON.GET %s: %s (fallback to GET)",
219
194
  key,
220
- String(e)
195
+ String(error)
221
196
  );
222
197
  }
223
198
  try {
224
199
  const raw = await redis.get(key);
225
- if (raw == null) return fallback;
226
- return parseJsonOrDeleteKey(redis, key, raw, fallback);
227
- } catch (e) {
228
- if (e instanceof Error && isRedisConnectivityError(e)) {
229
- markRedisUnavailable(e);
230
- return fallback;
200
+ return raw == null ? null : parseJsonStrict(key, raw);
201
+ } catch (error) {
202
+ if (error instanceof Error && isRedisConnectivityError(error)) {
203
+ markRedisUnavailable(error);
204
+ }
205
+ throw error;
206
+ }
207
+ };
208
+ var delKeyStrict = async (key) => {
209
+ const redis = await requireReadyRedis();
210
+ try {
211
+ return await redis.del(key) === 1;
212
+ } catch (error) {
213
+ if (error instanceof Error && isRedisConnectivityError(error)) {
214
+ markRedisUnavailable(error);
231
215
  }
232
- logger.log("error", "failed GET %s: %s", key, String(e));
233
- return fallback;
216
+ throw error;
234
217
  }
235
218
  };
236
- var setData = async (key, data, options = {}) => {
237
- if (redisUnavailable) return;
219
+ var setDataStrict = async (key, data, options = {}) => {
238
220
  const { expire } = { ...DEFAULT_OPTIONS, ...options };
239
- const redis = await getReadyRedis();
240
- if (!redis) return;
221
+ const redis = await requireReadyRedis();
241
222
  const value = toJson(data);
242
223
  try {
243
224
  await redis.call("JSON.SET", key, "$", value);
244
225
  if (expire) {
245
226
  await redis.expire(key, expire);
246
227
  }
247
- } catch (e) {
248
- if (e instanceof Error && isRedisConnectivityError(e)) {
249
- markRedisUnavailable(e);
250
- return;
251
- }
252
- logger.log(
253
- "error",
254
- "failed JSON.SET %s: %s (fallback to SET)",
255
- key,
256
- String(e)
257
- );
258
- try {
259
- if (expire) {
260
- await redis.set(key, value, "EX", expire);
261
- } else {
262
- await redis.set(key, value);
263
- }
264
- } catch (e2) {
265
- if (e2 instanceof Error && isRedisConnectivityError(e2)) {
266
- markRedisUnavailable(e2);
267
- return;
268
- }
269
- logger.log("error", "failed SET %s: %s", key, String(e2));
270
- }
271
- }
272
- };
273
- var setDataIfAbsent = async (key, data) => {
274
- if (redisUnavailable) return false;
275
- const redis = await getReadyRedis();
276
- if (!redis) return false;
277
- try {
278
- return await redis.call("JSON.SET", key, "$", toJson(data), "NX") === "OK";
228
+ return;
279
229
  } catch (error) {
280
230
  if (error instanceof Error && isRedisConnectivityError(error)) {
281
231
  markRedisUnavailable(error);
282
- return false;
232
+ throw error;
283
233
  }
284
234
  logger.log(
285
- "warn",
286
- "failed JSON.SET NX %s: %s (fallback to SET NX)",
235
+ "error",
236
+ "failed strict JSON.SET %s: %s (fallback to SET)",
287
237
  key,
288
238
  String(error)
289
239
  );
290
- try {
291
- return await redis.set(key, toJson(data), "NX") === "OK";
292
- } catch (fallbackError) {
293
- if (fallbackError instanceof Error && isRedisConnectivityError(fallbackError)) {
294
- markRedisUnavailable(fallbackError);
295
- }
296
- logger.log("error", "failed SET NX %s: %s", key, String(fallbackError));
297
- return false;
298
- }
299
240
  }
300
- };
301
- var incrementKey = async (key) => {
302
- if (redisUnavailable) throw new Error("Redis is unavailable");
303
- const redis = await getReadyRedis();
304
- if (!redis) throw new Error("Redis is unavailable");
305
241
  try {
306
- return await redis.incr(key);
242
+ if (expire) {
243
+ await redis.set(key, value, "EX", expire);
244
+ } else {
245
+ await redis.set(key, value);
246
+ }
307
247
  } catch (error) {
308
248
  if (error instanceof Error && isRedisConnectivityError(error)) {
309
249
  markRedisUnavailable(error);
@@ -316,18 +256,14 @@ var redisKeys = {
316
256
  user: (userName) => `users:index:${userName}`,
317
257
  tradingAccounts: (userName) => `users:${userName}:trading-accounts:`,
318
258
  tradingAccount: (userName, accountId) => `users:${userName}:trading-accounts:${accountId}`,
319
- runtimeDeployments: (userName) => `users:${userName}:runtime:deployments:`,
320
- runtimeDeployment: (userName, deploymentId) => `users:${userName}:runtime:deployments:${deploymentId}`,
321
259
  runtimeDeploymentHeartbeat: (userName, deploymentId) => `users:${userName}:runtime:deployments:${deploymentId}:heartbeat`,
260
+ runtimeControls: (userName) => `users:${userName}:runtime:controls`,
322
261
  bots: (userName) => `users:${userName}:bots`,
323
262
  botsPrefix: () => "users:",
324
263
  bot: (userName, botId) => `users:${userName}:bots:${botId}`,
325
264
  backtestConfig: (userName, config) => `users:${userName}:backtests:configs:${config}`,
326
265
  strategies: (userName) => `users:${userName}:strategies`,
327
266
  strategyConfig: (userName, strategyName, configId = "config") => `users:${userName}:strategies:${strategyName}:${configId}`,
328
- runtimeStrategyReleases: (userName, strategyName) => `users:${userName}:strategies:${strategyName}:releases:`,
329
- runtimeStrategyRelease: (userName, strategyName, releaseVersion) => `users:${userName}:strategies:${strategyName}:releases:${releaseVersion}`,
330
- runtimeStrategyReleaseSequence: (userName, strategyName) => `users:${userName}:strategies:${strategyName}:release-seq`,
331
267
  runtimeStrategyControlEvents: (userName) => `users:${userName}:runtime:strategy-control-events:`,
332
268
  runtimeStrategyControlEvent: (userName, eventId) => `users:${userName}:runtime:strategy-control-events:${eventId}`,
333
269
  strategyResults: (userName, strategyName) => `users:${userName}:strategies:${strategyName}:results`,
@@ -386,157 +322,96 @@ var redisKeys = {
386
322
  mlResult: (strategyName, signalId) => `ml:${strategyName}:results:${signalId}`
387
323
  };
388
324
 
389
- // src/runtimeStrategyReleases.ts
390
- var SHA256_RE = /^[a-f0-9]{64}$/;
391
- var FORBIDDEN_RELEASE_CONFIG_KEYS = /* @__PURE__ */ new Set([
392
- "ACCOUNT_ID",
393
- "AI_REPLAY_ANALYSES",
394
- "DEPLOYMENT_ID",
395
- "ENABLE",
396
- "ENV",
397
- "MAKE_ORDERS",
398
- "RECORD_RUNTIME_TRADES",
399
- "configId"
400
- ]);
401
- var SECRET_CONFIG_KEY_RE = /(?:^|_)(?:API_KEY|API_SECRET|TOKEN|PASSWORD|PRIVATE_KEY)$/i;
402
- var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
403
- var canonicalJson = (value) => {
404
- const normalize = (current) => {
405
- if (Array.isArray(current)) return current.map(normalize);
406
- const record = asRecord(current);
407
- if (!record) return current;
408
- return Object.fromEntries(
409
- Object.entries(record).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalize(entry)])
410
- );
411
- };
412
- return JSON.stringify(normalize(value));
413
- };
414
- var releaseContentSha256 = (release) => (0, import_node_crypto.createHash)("sha256").update(canonicalJson(release)).digest("hex");
415
- var assertStrategyName = (strategyName) => {
416
- if (!/^[a-zA-Z0-9_-]+$/.test(strategyName)) {
417
- throw new Error(`Invalid strategy name: ${strategyName}`);
418
- }
419
- };
420
- var assertPackageIdentity = (value, label) => {
421
- if (typeof value !== "string" || !value.trim()) {
422
- throw new Error(`${label} is required for a runtime strategy release`);
423
- }
424
- };
425
- var assertRuntimeStrategyReleaseConfig = (config) => {
426
- const record = asRecord(config);
427
- if (!record) throw new Error("Strategy release config must be an object");
428
- for (const key of FORBIDDEN_RELEASE_CONFIG_KEYS) {
429
- if (record[key] !== void 0) {
430
- throw new Error(`${key} is a deployment binding, not release config`);
431
- }
325
+ // src/runtimeControls.ts
326
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
327
+ var verifyPauseOverride = (value) => isRecord(value) && Object.keys(value).every(
328
+ (key) => ["entriesPaused", "updatedAt", "updatedBy"].includes(key)
329
+ ) && value.entriesPaused === true && typeof value.updatedAt === "string" && Number.isFinite(Date.parse(value.updatedAt)) && typeof value.updatedBy === "string" && Boolean(value.updatedBy.trim());
330
+ var emptyRuntimeControls = () => ({
331
+ schema: import_types.RUNTIME_CONTROLS_SCHEMA,
332
+ deployments: {}
333
+ });
334
+ var verifyRuntimeControls = (value) => {
335
+ if (!isRecord(value) || Object.keys(value).some(
336
+ (key) => !["schema", "deployments"].includes(key)
337
+ ) || value.schema !== import_types.RUNTIME_CONTROLS_SCHEMA || !isRecord(value.deployments)) {
338
+ throw new Error("Invalid runtime controls");
432
339
  }
433
- const inspect = (value, parentPath = "") => {
434
- if (Array.isArray(value)) {
435
- value.forEach(
436
- (entry, index) => inspect(entry, `${parentPath}[${index}]`)
437
- );
438
- return;
340
+ for (const [deploymentId, strategyOverrides] of Object.entries(
341
+ value.deployments
342
+ )) {
343
+ if (!deploymentId.trim() || !isRecord(strategyOverrides)) {
344
+ throw new Error("Invalid runtime controls");
439
345
  }
440
- const current = asRecord(value);
441
- if (!current) return;
442
- for (const [key, entry] of Object.entries(current)) {
443
- const keyPath = parentPath ? `${parentPath}.${key}` : key;
444
- if (SECRET_CONFIG_KEY_RE.test(key)) {
445
- throw new Error(`${keyPath} is secret material, not release config`);
346
+ for (const [strategyName, override] of Object.entries(strategyOverrides)) {
347
+ if (!strategyName.trim() || !verifyPauseOverride(override)) {
348
+ throw new Error("Invalid runtime controls");
446
349
  }
447
- inspect(entry, keyPath);
448
350
  }
449
- };
450
- inspect(record);
451
- if (typeof record.INTERVAL !== "string" || !record.INTERVAL.trim()) {
452
- throw new Error("Published strategy config must define INTERVAL");
453
351
  }
454
- if (record.UNIVERSE !== "crypto" && record.UNIVERSE !== "tradfi") {
455
- throw new Error("Published strategy config must define a valid UNIVERSE");
456
- }
457
- };
458
- var isRelease = (value) => {
459
- const release = asRecord(value);
460
- return Boolean(
461
- release?.schema === import_types.RUNTIME_STRATEGY_RELEASE_SCHEMA && typeof release.strategyName === "string" && Number.isSafeInteger(release.releaseVersion) && Number(release.releaseVersion) > 0 && asRecord(release.config) && typeof release.strategyPackage === "string" && Boolean(release.strategyPackage.trim()) && typeof release.strategyPackageVersion === "string" && Boolean(release.strategyPackageVersion.trim()) && typeof release.runtimePackageVersion === "string" && Boolean(release.runtimePackageVersion.trim()) && typeof release.createdAt === "number" && typeof release.createdBy === "string" && typeof release.contentSha256 === "string" && SHA256_RE.test(release.contentSha256)
462
- );
463
- };
464
- var verifyRuntimeStrategyRelease = (value) => {
465
- if (!isRelease(value)) throw new Error("Invalid runtime strategy release");
466
- const { contentSha256, ...content } = value;
467
- if (releaseContentSha256(content) !== contentSha256) {
468
- throw new Error(
469
- `${value.strategyName} v${value.releaseVersion} content checksum mismatch`
470
- );
471
- }
472
- assertRuntimeStrategyReleaseConfig(value.config);
473
352
  return value;
474
353
  };
475
- var getRuntimeStrategyRelease = async (userName, strategyName, releaseVersion) => {
476
- const value = await getData(
477
- redisKeys.runtimeStrategyRelease(userName, strategyName, releaseVersion),
478
- null
479
- );
480
- return value == null ? null : verifyRuntimeStrategyRelease(value);
481
- };
482
- var listRuntimeStrategyReleases = async (userName, strategyName) => {
483
- const keys = await getKeys(
484
- redisKeys.runtimeStrategyReleases(userName, strategyName)
485
- );
486
- const releases = await Promise.all(
487
- keys.map(async (key) => {
488
- const value = await getData(key, null);
489
- return value == null ? null : verifyRuntimeStrategyRelease(value);
490
- })
491
- );
492
- return releases.filter((release) => release != null).sort((left, right) => right.releaseVersion - left.releaseVersion);
354
+ var getRuntimeControls = async (userName) => {
355
+ const value = await getDataStrict(redisKeys.runtimeControls(userName));
356
+ return value == null ? emptyRuntimeControls() : verifyRuntimeControls(value);
493
357
  };
494
- var publishRuntimeStrategyRelease = async ({
358
+ var pauseRuntimeStrategy = async ({
495
359
  userName,
360
+ deploymentId,
496
361
  strategyName,
497
- config,
498
- strategyPackage,
499
- strategyPackageVersion,
500
- runtimePackageVersion,
501
- createdBy
362
+ updatedBy,
363
+ updatedAt = (/* @__PURE__ */ new Date()).toISOString()
502
364
  }) => {
503
- assertStrategyName(strategyName);
504
- assertRuntimeStrategyReleaseConfig(config);
505
- assertPackageIdentity(strategyPackage, "strategyPackage");
506
- assertPackageIdentity(strategyPackageVersion, "strategyPackageVersion");
507
- assertPackageIdentity(runtimePackageVersion, "runtimePackageVersion");
508
- const releaseVersion = await incrementKey(
509
- redisKeys.runtimeStrategyReleaseSequence(userName, strategyName)
510
- );
511
- const content = {
512
- schema: import_types.RUNTIME_STRATEGY_RELEASE_SCHEMA,
513
- strategyName,
514
- releaseVersion,
515
- config,
516
- strategyPackage,
517
- strategyPackageVersion,
518
- runtimePackageVersion,
519
- createdAt: Date.now(),
520
- createdBy
521
- };
522
- const release = {
523
- ...content,
524
- contentSha256: releaseContentSha256(content)
365
+ const controls = await getRuntimeControls(userName);
366
+ const next = {
367
+ ...controls,
368
+ deployments: {
369
+ ...controls.deployments,
370
+ [deploymentId]: {
371
+ ...controls.deployments[deploymentId],
372
+ [strategyName]: {
373
+ entriesPaused: true,
374
+ updatedAt,
375
+ updatedBy: updatedBy.trim()
376
+ }
377
+ }
378
+ }
525
379
  };
526
- const stored = await setDataIfAbsent(
527
- redisKeys.runtimeStrategyRelease(userName, strategyName, releaseVersion),
528
- release
529
- );
530
- if (!stored) {
531
- throw new Error(`Refused to overwrite ${strategyName} v${releaseVersion}`);
380
+ const verified = verifyRuntimeControls(next);
381
+ await setDataStrict(redisKeys.runtimeControls(userName), verified, {
382
+ expire: 0
383
+ });
384
+ return verified;
385
+ };
386
+ var resumeRuntimeStrategy = async ({
387
+ userName,
388
+ deploymentId,
389
+ strategyName
390
+ }) => {
391
+ const controls = await getRuntimeControls(userName);
392
+ const deployment = { ...controls.deployments[deploymentId] };
393
+ delete deployment[strategyName];
394
+ const deployments = { ...controls.deployments };
395
+ if (Object.keys(deployment).length) {
396
+ deployments[deploymentId] = deployment;
397
+ } else {
398
+ delete deployments[deploymentId];
399
+ }
400
+ const next = verifyRuntimeControls({ ...controls, deployments });
401
+ if (Object.keys(deployments).length) {
402
+ await setDataStrict(redisKeys.runtimeControls(userName), next, {
403
+ expire: 0
404
+ });
405
+ } else {
406
+ await delKeyStrict(redisKeys.runtimeControls(userName));
532
407
  }
533
- return release;
408
+ return next;
534
409
  };
535
410
  var recordRuntimeStrategyControlEvent = async ({
536
411
  userName,
537
412
  deploymentId,
538
413
  strategyName,
539
- releaseVersion,
414
+ version,
540
415
  previousState,
541
416
  nextState,
542
417
  createdBy
@@ -547,14 +422,14 @@ var recordRuntimeStrategyControlEvent = async ({
547
422
  eventId: `${createdAt}-${(0, import_node_crypto.randomUUID)()}`,
548
423
  deploymentId,
549
424
  strategyName,
550
- releaseVersion,
425
+ version,
551
426
  action,
552
427
  previousState,
553
428
  nextState,
554
429
  createdAt,
555
430
  createdBy
556
431
  };
557
- await setData(
432
+ await setDataStrict(
558
433
  redisKeys.runtimeStrategyControlEvent(userName, event.eventId),
559
434
  event,
560
435
  { expire: 0 }
@@ -563,10 +438,10 @@ var recordRuntimeStrategyControlEvent = async ({
563
438
  };
564
439
  // Annotate the CommonJS export names for ESM import in node:
565
440
  0 && (module.exports = {
566
- assertRuntimeStrategyReleaseConfig,
567
- getRuntimeStrategyRelease,
568
- listRuntimeStrategyReleases,
569
- publishRuntimeStrategyRelease,
441
+ emptyRuntimeControls,
442
+ getRuntimeControls,
443
+ pauseRuntimeStrategy,
570
444
  recordRuntimeStrategyControlEvent,
571
- verifyRuntimeStrategyRelease
445
+ resumeRuntimeStrategy,
446
+ verifyRuntimeControls
572
447
  });
@@ -0,0 +1,133 @@
1
+ import {
2
+ delKeyStrict,
3
+ getDataStrict,
4
+ redisKeys,
5
+ setDataStrict
6
+ } from "./chunk-NN4CAPDW.mjs";
7
+
8
+ // src/runtimeControls.ts
9
+ import { randomUUID } from "crypto";
10
+ import {
11
+ RUNTIME_CONTROLS_SCHEMA
12
+ } from "@tradejs/types";
13
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
14
+ var verifyPauseOverride = (value) => isRecord(value) && Object.keys(value).every(
15
+ (key) => ["entriesPaused", "updatedAt", "updatedBy"].includes(key)
16
+ ) && value.entriesPaused === true && typeof value.updatedAt === "string" && Number.isFinite(Date.parse(value.updatedAt)) && typeof value.updatedBy === "string" && Boolean(value.updatedBy.trim());
17
+ var emptyRuntimeControls = () => ({
18
+ schema: RUNTIME_CONTROLS_SCHEMA,
19
+ deployments: {}
20
+ });
21
+ var verifyRuntimeControls = (value) => {
22
+ if (!isRecord(value) || Object.keys(value).some(
23
+ (key) => !["schema", "deployments"].includes(key)
24
+ ) || value.schema !== RUNTIME_CONTROLS_SCHEMA || !isRecord(value.deployments)) {
25
+ throw new Error("Invalid runtime controls");
26
+ }
27
+ for (const [deploymentId, strategyOverrides] of Object.entries(
28
+ value.deployments
29
+ )) {
30
+ if (!deploymentId.trim() || !isRecord(strategyOverrides)) {
31
+ throw new Error("Invalid runtime controls");
32
+ }
33
+ for (const [strategyName, override] of Object.entries(strategyOverrides)) {
34
+ if (!strategyName.trim() || !verifyPauseOverride(override)) {
35
+ throw new Error("Invalid runtime controls");
36
+ }
37
+ }
38
+ }
39
+ return value;
40
+ };
41
+ var getRuntimeControls = async (userName) => {
42
+ const value = await getDataStrict(redisKeys.runtimeControls(userName));
43
+ return value == null ? emptyRuntimeControls() : verifyRuntimeControls(value);
44
+ };
45
+ var pauseRuntimeStrategy = async ({
46
+ userName,
47
+ deploymentId,
48
+ strategyName,
49
+ updatedBy,
50
+ updatedAt = (/* @__PURE__ */ new Date()).toISOString()
51
+ }) => {
52
+ const controls = await getRuntimeControls(userName);
53
+ const next = {
54
+ ...controls,
55
+ deployments: {
56
+ ...controls.deployments,
57
+ [deploymentId]: {
58
+ ...controls.deployments[deploymentId],
59
+ [strategyName]: {
60
+ entriesPaused: true,
61
+ updatedAt,
62
+ updatedBy: updatedBy.trim()
63
+ }
64
+ }
65
+ }
66
+ };
67
+ const verified = verifyRuntimeControls(next);
68
+ await setDataStrict(redisKeys.runtimeControls(userName), verified, {
69
+ expire: 0
70
+ });
71
+ return verified;
72
+ };
73
+ var resumeRuntimeStrategy = async ({
74
+ userName,
75
+ deploymentId,
76
+ strategyName
77
+ }) => {
78
+ const controls = await getRuntimeControls(userName);
79
+ const deployment = { ...controls.deployments[deploymentId] };
80
+ delete deployment[strategyName];
81
+ const deployments = { ...controls.deployments };
82
+ if (Object.keys(deployment).length) {
83
+ deployments[deploymentId] = deployment;
84
+ } else {
85
+ delete deployments[deploymentId];
86
+ }
87
+ const next = verifyRuntimeControls({ ...controls, deployments });
88
+ if (Object.keys(deployments).length) {
89
+ await setDataStrict(redisKeys.runtimeControls(userName), next, {
90
+ expire: 0
91
+ });
92
+ } else {
93
+ await delKeyStrict(redisKeys.runtimeControls(userName));
94
+ }
95
+ return next;
96
+ };
97
+ var recordRuntimeStrategyControlEvent = async ({
98
+ userName,
99
+ deploymentId,
100
+ strategyName,
101
+ version,
102
+ previousState,
103
+ nextState,
104
+ createdBy
105
+ }) => {
106
+ const action = nextState === "entries_paused" ? "pause_entries" : "resume";
107
+ const createdAt = Date.now();
108
+ const event = {
109
+ eventId: `${createdAt}-${randomUUID()}`,
110
+ deploymentId,
111
+ strategyName,
112
+ version,
113
+ action,
114
+ previousState,
115
+ nextState,
116
+ createdAt,
117
+ createdBy
118
+ };
119
+ await setDataStrict(
120
+ redisKeys.runtimeStrategyControlEvent(userName, event.eventId),
121
+ event,
122
+ { expire: 0 }
123
+ );
124
+ return event;
125
+ };
126
+ export {
127
+ emptyRuntimeControls,
128
+ getRuntimeControls,
129
+ pauseRuntimeStrategy,
130
+ recordRuntimeStrategyControlEvent,
131
+ resumeRuntimeStrategy,
132
+ verifyRuntimeControls
133
+ };
@@ -0,0 +1,6 @@
1
+ import { RuntimeDeploymentHeartbeat } from '@tradejs/types';
2
+
3
+ declare const getRuntimeDeploymentHeartbeat: (userName: string, deploymentId: string) => Promise<RuntimeDeploymentHeartbeat | null>;
4
+ declare const saveRuntimeDeploymentHeartbeat: (userName: string, heartbeat: RuntimeDeploymentHeartbeat) => Promise<RuntimeDeploymentHeartbeat>;
5
+
6
+ export { getRuntimeDeploymentHeartbeat, saveRuntimeDeploymentHeartbeat };
@@ -0,0 +1,6 @@
1
+ import { RuntimeDeploymentHeartbeat } from '@tradejs/types';
2
+
3
+ declare const getRuntimeDeploymentHeartbeat: (userName: string, deploymentId: string) => Promise<RuntimeDeploymentHeartbeat | null>;
4
+ declare const saveRuntimeDeploymentHeartbeat: (userName: string, heartbeat: RuntimeDeploymentHeartbeat) => Promise<RuntimeDeploymentHeartbeat>;
5
+
6
+ export { getRuntimeDeploymentHeartbeat, saveRuntimeDeploymentHeartbeat };