@tradejs/infra 3.0.1 → 3.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.
@@ -0,0 +1,598 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/runtimeStrategyReleases.ts
31
+ var runtimeStrategyReleases_exports = {};
32
+ __export(runtimeStrategyReleases_exports, {
33
+ assertRuntimeStrategyReleaseConfig: () => assertRuntimeStrategyReleaseConfig,
34
+ getRuntimeStrategyDraft: () => getRuntimeStrategyDraft,
35
+ getRuntimeStrategyRelease: () => getRuntimeStrategyRelease,
36
+ listRuntimeStrategyReleases: () => listRuntimeStrategyReleases,
37
+ publishRuntimeStrategyRelease: () => publishRuntimeStrategyRelease,
38
+ recordRuntimeStrategyControlEvent: () => recordRuntimeStrategyControlEvent,
39
+ saveRuntimeStrategyDraft: () => saveRuntimeStrategyDraft,
40
+ verifyRuntimeStrategyRelease: () => verifyRuntimeStrategyRelease
41
+ });
42
+ module.exports = __toCommonJS(runtimeStrategyReleases_exports);
43
+ var import_node_crypto = require("crypto");
44
+ var import_types = require("@tradejs/types");
45
+
46
+ // src/redis.ts
47
+ var import_ioredis = __toESM(require("ioredis"));
48
+ var TTL_1D = 86400;
49
+ var SCREENSHOT_TOKEN_TTL_SECONDS = 15 * 60;
50
+ var toJson = (value) => JSON.stringify(value);
51
+ var logger = {
52
+ log: (level, message, ...args) => {
53
+ const method = level === "error" ? "error" : level === "warn" ? "warn" : "log";
54
+ console[method](`[infra:redis] ${message}`, ...args);
55
+ }
56
+ };
57
+ var redisConnectionWarningShown = false;
58
+ var redisUnavailable = false;
59
+ var isRedisConnectivityError = (error) => {
60
+ const errorText = [error.name, error.message, String(error)].join(" ");
61
+ return /ECONNREFUSED|ECONNRESET|ECONNABORTED|EPIPE|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|MaxRetriesPerRequestError|Connection is closed|Stream isn't writeable/i.test(
62
+ errorText
63
+ );
64
+ };
65
+ var toNonNegativeInt = (value, fallback) => {
66
+ const parsed = Number.parseInt(String(value ?? ""), 10);
67
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
68
+ };
69
+ var toPositiveInt = (value, fallback) => {
70
+ const parsed = Number.parseInt(String(value ?? ""), 10);
71
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
72
+ };
73
+ var markRedisUnavailable = (error) => {
74
+ redisUnavailable = true;
75
+ if (redisConnectionWarningShown) return;
76
+ redisConnectionWarningShown = true;
77
+ logger.log(
78
+ "warn",
79
+ "Redis is unavailable: %s. Cache-dependent features are temporarily disabled.",
80
+ error.message
81
+ );
82
+ };
83
+ var getRedis = () => {
84
+ if (!global.__redis__) {
85
+ const host = process.env.REDIS_HOST || "127.0.0.1";
86
+ const port = toPositiveInt(process.env.REDIS_PORT, 6379);
87
+ const connectTimeout = toPositiveInt(
88
+ process.env.REDIS_CONNECT_TIMEOUT_MS,
89
+ 3e3
90
+ );
91
+ const maxRetriesPerRequest = toNonNegativeInt(
92
+ process.env.REDIS_MAX_RETRIES_PER_REQUEST,
93
+ 1
94
+ );
95
+ global.__redis__ = new import_ioredis.default({
96
+ host,
97
+ port,
98
+ connectTimeout,
99
+ maxRetriesPerRequest,
100
+ enableOfflineQueue: false,
101
+ retryStrategy: (attempt) => Math.min(attempt * 200, 2e3)
102
+ });
103
+ global.__redis__.on("error", (error) => {
104
+ if (isRedisConnectivityError(error)) {
105
+ markRedisUnavailable(error);
106
+ return;
107
+ }
108
+ logger.log("error", "Redis client error: %s", String(error));
109
+ });
110
+ global.__redis__.on("ready", () => {
111
+ redisUnavailable = false;
112
+ if (redisConnectionWarningShown) {
113
+ redisConnectionWarningShown = false;
114
+ logger.log("info", "Redis connection restored");
115
+ }
116
+ });
117
+ }
118
+ return global.__redis__;
119
+ };
120
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
121
+ var getRedisStatus = (redis) => String(redis.status ?? "ready");
122
+ var waitForRedisReady = async (redis) => {
123
+ if (getRedisStatus(redis) === "ready") {
124
+ return true;
125
+ }
126
+ const readyTimeoutMs = toPositiveInt(
127
+ process.env.REDIS_READY_TIMEOUT_MS,
128
+ toPositiveInt(process.env.REDIS_CONNECT_TIMEOUT_MS, 3e3)
129
+ );
130
+ const startedAt = Date.now();
131
+ while (Date.now() - startedAt < readyTimeoutMs) {
132
+ if (getRedisStatus(redis) === "ready") {
133
+ return true;
134
+ }
135
+ if (getRedisStatus(redis) === "end") {
136
+ return false;
137
+ }
138
+ await sleep(50);
139
+ }
140
+ return getRedisStatus(redis) === "ready";
141
+ };
142
+ var getReadyRedis = async () => {
143
+ if (redisUnavailable) return null;
144
+ const redis = getRedis();
145
+ const ready = await waitForRedisReady(redis);
146
+ if (ready) {
147
+ return redis;
148
+ }
149
+ markRedisUnavailable(
150
+ new Error(`Redis is not ready (status=${getRedisStatus(redis)})`)
151
+ );
152
+ return null;
153
+ };
154
+ var toResultString = (result) => {
155
+ if (result == null) return null;
156
+ if (typeof result === "string") return result;
157
+ if (Buffer.isBuffer(result)) return result.toString("utf8");
158
+ return String(result);
159
+ };
160
+ var DEFAULT_OPTIONS = {
161
+ expire: TTL_1D
162
+ };
163
+ var parseJsonOrDeleteKey = async (redis, key, raw, fallback) => {
164
+ try {
165
+ return JSON.parse(raw);
166
+ } catch (e) {
167
+ logger.log("error", "failed JSON.parse(%s): %s", key, String(e));
168
+ await redis.del(key);
169
+ return fallback;
170
+ }
171
+ };
172
+ var getKeys = async (prefix) => {
173
+ if (redisUnavailable) return [];
174
+ const redis = await getReadyRedis();
175
+ if (!redis) return [];
176
+ const keys = [];
177
+ try {
178
+ let cursor = "0";
179
+ do {
180
+ const [nextCursor, batch] = await redis.scan(
181
+ cursor,
182
+ "MATCH",
183
+ `${prefix}*`,
184
+ "COUNT",
185
+ "200"
186
+ );
187
+ cursor = nextCursor;
188
+ for (const key of batch) {
189
+ if (key.startsWith(prefix)) {
190
+ keys.push(key);
191
+ }
192
+ }
193
+ } while (cursor !== "0");
194
+ } catch (e) {
195
+ if (e instanceof Error && isRedisConnectivityError(e)) {
196
+ markRedisUnavailable(e);
197
+ return [];
198
+ }
199
+ logger.log("warn", "failed SCAN for %s: %s", prefix, String(e));
200
+ return [];
201
+ }
202
+ return keys;
203
+ };
204
+ var getData = async (key, fallback = []) => {
205
+ if (redisUnavailable) return fallback;
206
+ const redis = await getReadyRedis();
207
+ if (!redis) return fallback;
208
+ try {
209
+ const rawJson = await redis.call("JSON.GET", key);
210
+ const raw = toResultString(rawJson);
211
+ if (raw == null) return fallback;
212
+ return parseJsonOrDeleteKey(redis, key, raw, fallback);
213
+ } catch (e) {
214
+ if (e instanceof Error && isRedisConnectivityError(e)) {
215
+ markRedisUnavailable(e);
216
+ return fallback;
217
+ }
218
+ logger.log(
219
+ "error",
220
+ "failed JSON.GET %s: %s (fallback to GET)",
221
+ key,
222
+ String(e)
223
+ );
224
+ }
225
+ try {
226
+ const raw = await redis.get(key);
227
+ if (raw == null) return fallback;
228
+ return parseJsonOrDeleteKey(redis, key, raw, fallback);
229
+ } catch (e) {
230
+ if (e instanceof Error && isRedisConnectivityError(e)) {
231
+ markRedisUnavailable(e);
232
+ return fallback;
233
+ }
234
+ logger.log("error", "failed GET %s: %s", key, String(e));
235
+ return fallback;
236
+ }
237
+ };
238
+ var setData = async (key, data, options = {}) => {
239
+ if (redisUnavailable) return;
240
+ const { expire } = { ...DEFAULT_OPTIONS, ...options };
241
+ const redis = await getReadyRedis();
242
+ if (!redis) return;
243
+ const value = toJson(data);
244
+ try {
245
+ await redis.call("JSON.SET", key, "$", value);
246
+ if (expire) {
247
+ await redis.expire(key, expire);
248
+ }
249
+ } catch (e) {
250
+ if (e instanceof Error && isRedisConnectivityError(e)) {
251
+ markRedisUnavailable(e);
252
+ return;
253
+ }
254
+ logger.log(
255
+ "error",
256
+ "failed JSON.SET %s: %s (fallback to SET)",
257
+ key,
258
+ String(e)
259
+ );
260
+ try {
261
+ if (expire) {
262
+ await redis.set(key, value, "EX", expire);
263
+ } else {
264
+ await redis.set(key, value);
265
+ }
266
+ } catch (e2) {
267
+ if (e2 instanceof Error && isRedisConnectivityError(e2)) {
268
+ markRedisUnavailable(e2);
269
+ return;
270
+ }
271
+ logger.log("error", "failed SET %s: %s", key, String(e2));
272
+ }
273
+ }
274
+ };
275
+ var setDataIfAbsent = async (key, data) => {
276
+ if (redisUnavailable) return false;
277
+ const redis = await getReadyRedis();
278
+ if (!redis) return false;
279
+ try {
280
+ return await redis.call("JSON.SET", key, "$", toJson(data), "NX") === "OK";
281
+ } catch (error) {
282
+ if (error instanceof Error && isRedisConnectivityError(error)) {
283
+ markRedisUnavailable(error);
284
+ return false;
285
+ }
286
+ logger.log(
287
+ "warn",
288
+ "failed JSON.SET NX %s: %s (fallback to SET NX)",
289
+ key,
290
+ String(error)
291
+ );
292
+ try {
293
+ return await redis.set(key, toJson(data), "NX") === "OK";
294
+ } catch (fallbackError) {
295
+ if (fallbackError instanceof Error && isRedisConnectivityError(fallbackError)) {
296
+ markRedisUnavailable(fallbackError);
297
+ }
298
+ logger.log("error", "failed SET NX %s: %s", key, String(fallbackError));
299
+ return false;
300
+ }
301
+ }
302
+ };
303
+ var incrementKey = async (key) => {
304
+ if (redisUnavailable) throw new Error("Redis is unavailable");
305
+ const redis = await getReadyRedis();
306
+ if (!redis) throw new Error("Redis is unavailable");
307
+ try {
308
+ return await redis.incr(key);
309
+ } catch (error) {
310
+ if (error instanceof Error && isRedisConnectivityError(error)) {
311
+ markRedisUnavailable(error);
312
+ }
313
+ throw error;
314
+ }
315
+ };
316
+ var redisKeys = {
317
+ users: () => "users:index:",
318
+ user: (userName) => `users:index:${userName}`,
319
+ tradingAccounts: (userName) => `users:${userName}:trading-accounts:`,
320
+ tradingAccount: (userName, accountId) => `users:${userName}:trading-accounts:${accountId}`,
321
+ runtimeDeployments: (userName) => `users:${userName}:runtime:deployments:`,
322
+ runtimeDeployment: (userName, deploymentId) => `users:${userName}:runtime:deployments:${deploymentId}`,
323
+ runtimeDeploymentHeartbeat: (userName, deploymentId) => `users:${userName}:runtime:deployments:${deploymentId}:heartbeat`,
324
+ bots: (userName) => `users:${userName}:bots`,
325
+ botsPrefix: () => "users:",
326
+ bot: (userName, botId) => `users:${userName}:bots:${botId}`,
327
+ backtestConfig: (userName, config) => `users:${userName}:backtests:configs:${config}`,
328
+ strategies: (userName) => `users:${userName}:strategies`,
329
+ strategyConfig: (userName, strategyName, configId = "config") => `users:${userName}:strategies:${strategyName}:${configId}`,
330
+ runtimeStrategyDraft: (userName, strategyName) => `users:${userName}:strategies:${strategyName}:draft`,
331
+ runtimeStrategyReleases: (userName, strategyName) => `users:${userName}:strategies:${strategyName}:releases:`,
332
+ runtimeStrategyRelease: (userName, strategyName, releaseVersion) => `users:${userName}:strategies:${strategyName}:releases:${releaseVersion}`,
333
+ runtimeStrategyReleaseSequence: (userName, strategyName) => `users:${userName}:strategies:${strategyName}:release-seq`,
334
+ runtimeStrategyControlEvents: (userName) => `users:${userName}:runtime:strategy-control-events:`,
335
+ runtimeStrategyControlEvent: (userName, eventId) => `users:${userName}:runtime:strategy-control-events:${eventId}`,
336
+ strategyResults: (userName, strategyName) => `users:${userName}:strategies:${strategyName}:results`,
337
+ strategyCharts: (userName, mode) => `users:${userName}:strategies:charts:${mode}`,
338
+ strategyChartCards: (userName, mode) => `users:${userName}:strategies:charts:${mode}:cards:`,
339
+ strategyChartCard: (userName, mode, cardId) => `users:${userName}:strategies:charts:${mode}:cards:${cardId}`,
340
+ tests: (userName, strategyName) => strategyName ? `users:${userName}:tests:${strategyName}` : `users:${userName}:tests:`,
341
+ testOrders: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:orders`,
342
+ testConfig: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:config`,
343
+ testStat: (userName, strategyName, testName) => `users:${userName}:tests:${strategyName}:${testName}:stat`,
344
+ testSummaries: (userName) => `users:${userName}:tests:index:summary`,
345
+ cacheChunk: (userName, chunkId) => `users:${userName}:cache:tests:chunks:${chunkId}`,
346
+ cacheOrders: (userName, orderLogId) => `users:${userName}:cache:tests:orders:${orderLogId}`,
347
+ cachePositions: (userName, orderLogId) => `users:${userName}:cache:tests:positions:${orderLogId}`,
348
+ tickerUniverse: (userName, connectorName, universe, accountId) => universe || accountId ? `users:${userName}:cache:tickers:${connectorName}:${universe ?? "crypto"}:${accountId ?? "default"}` : `users:${userName}:cache:tickers:${connectorName}`,
349
+ signal: (symbol, signalId) => `signals:${symbol}:${signalId}`,
350
+ signalsBySymbol: (symbol) => `signals:${symbol}:`,
351
+ storeSignal: (symbol, signalId) => `store:signals:${symbol}:${signalId}`,
352
+ runtimeSignals: (userName) => `users:${userName}:runtime:signals:`,
353
+ runtimeSignal: (userName, signalId) => `users:${userName}:runtime:signals:${signalId}`,
354
+ runtimeSignalBuckets: (userName) => `users:${userName}:runtime:signals:days:`,
355
+ runtimeSignalBucket: (userName, dayKey, strategyName) => `users:${userName}:runtime:signals:days:${dayKey}:${strategyName}`,
356
+ runtimeSignalEvaluations: (userName) => `users:${userName}:runtime:signal-evaluations:`,
357
+ runtimeSignalEvaluation: (userName, evaluationId) => `users:${userName}:runtime:signal-evaluations:${evaluationId}`,
358
+ runtimeSignalEvaluationBuckets: (userName) => `users:${userName}:runtime:signal-evaluations:days:`,
359
+ runtimeSignalEvaluationBucket: (userName, dayKey, strategyName) => `users:${userName}:runtime:signal-evaluations:days:${dayKey}:${strategyName}`,
360
+ runtimeSignalEvaluationStatsBuckets: (userName) => `users:${userName}:runtime:signal-evaluation-stats:days:`,
361
+ runtimeSignalEvaluationStatsBucket: (userName, dayKey, strategyName) => `users:${userName}:runtime:signal-evaluation-stats:days:${dayKey}:${strategyName}`,
362
+ runtimeLineageScopeBucket: (userName, dayKey) => `users:${userName}:runtime:lineage-scopes:days:${dayKey}`,
363
+ runtimeTrades: (userName) => `users:${userName}:runtime:trade-records:`,
364
+ runtimeTrade: (userName, orderId) => `users:${userName}:runtime:trade-records:${orderId}`,
365
+ runtimeTradeBuckets: (userName) => `users:${userName}:runtime:trade-records:days:`,
366
+ runtimeTradeBucket: (userName, dayKey) => `users:${userName}:runtime:trade-records:days:${dayKey}`,
367
+ runtimeClosedTradeBuckets: (userName) => `users:${userName}:runtime:closed-trade-records:days:`,
368
+ runtimeClosedTradeBucket: (userName, dayKey) => `users:${userName}:runtime:closed-trade-records:days:${dayKey}`,
369
+ runtimeActiveTrades: (userName) => `users:${userName}:runtime:active-trades:`,
370
+ runtimeActiveTrade: (userName, symbol, scopeId) => scopeId ? `users:${userName}:runtime:active-trades:${scopeId}:${symbol}` : `users:${userName}:runtime:active-trades:${symbol}`,
371
+ aiChatHistory: (userName, symbolKey) => `users:${userName}:ai:chats:${symbolKey}`,
372
+ analysis: (symbol, signalId) => `analysis:${symbol}:${signalId}`,
373
+ screenshotSessionToken: (token) => `auth:screenshot:${token}`,
374
+ backtestResults: (userName, config, timestamp) => `users:${userName}:backtests:results:${config}:${timestamp}`,
375
+ backtestJobs: (userName) => `users:${userName}:backtests:jobs:`,
376
+ backtestJob: (userName, jobId) => `users:${userName}:backtests:jobs:${jobId}`,
377
+ backtestRuns: (userName) => `users:${userName}:backtests:runs:`,
378
+ backtestRun: (userName, runId) => `users:${userName}:backtests:runs:${runId}`,
379
+ backtestRunResults: (userName, runId) => `users:${userName}:backtests:runs:${runId}:results`,
380
+ backtestLatestRun: (userName, config) => `users:${userName}:backtests:latest:${config}`,
381
+ researchRuns: (userName) => `users:${userName}:research:runs:`,
382
+ researchRun: (userName, runId) => `users:${userName}:research:runs:${runId}`,
383
+ researchLatestRun: (userName, strategyName) => `users:${userName}:research:latest:${strategyName}`,
384
+ mlSignalsByStrategy: (strategyName) => `ml:${strategyName}:signals:`,
385
+ mlSignals: () => "ml:",
386
+ mlSignal: (strategyName, signalId) => `ml:${strategyName}:signals:${signalId}`,
387
+ mlResultsByStrategy: (strategyName) => `ml:${strategyName}:results:`,
388
+ mlResults: () => "ml:",
389
+ mlResult: (strategyName, signalId) => `ml:${strategyName}:results:${signalId}`
390
+ };
391
+
392
+ // src/runtimeStrategyReleases.ts
393
+ var SHA256_RE = /^[a-f0-9]{64}$/;
394
+ var FORBIDDEN_RELEASE_CONFIG_KEYS = /* @__PURE__ */ new Set([
395
+ "ACCOUNT_ID",
396
+ "AI_REPLAY_ANALYSES",
397
+ "DEPLOYMENT_ID",
398
+ "ENABLE",
399
+ "ENV",
400
+ "MAKE_ORDERS",
401
+ "RECORD_RUNTIME_TRADES",
402
+ "configId"
403
+ ]);
404
+ var SECRET_CONFIG_KEY_RE = /(?:^|_)(?:API_KEY|API_SECRET|TOKEN|PASSWORD|PRIVATE_KEY)$/i;
405
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
406
+ var canonicalJson = (value) => {
407
+ const normalize = (current) => {
408
+ if (Array.isArray(current)) return current.map(normalize);
409
+ const record = asRecord(current);
410
+ if (!record) return current;
411
+ return Object.fromEntries(
412
+ Object.entries(record).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, normalize(entry)])
413
+ );
414
+ };
415
+ return JSON.stringify(normalize(value));
416
+ };
417
+ var releaseContentSha256 = (release) => (0, import_node_crypto.createHash)("sha256").update(canonicalJson(release)).digest("hex");
418
+ var assertStrategyName = (strategyName) => {
419
+ if (!/^[a-zA-Z0-9_-]+$/.test(strategyName)) {
420
+ throw new Error(`Invalid strategy name: ${strategyName}`);
421
+ }
422
+ };
423
+ var assertRuntimeStrategyReleaseConfig = (config) => {
424
+ const record = asRecord(config);
425
+ if (!record) throw new Error("Strategy release config must be an object");
426
+ for (const key of FORBIDDEN_RELEASE_CONFIG_KEYS) {
427
+ if (record[key] !== void 0) {
428
+ throw new Error(`${key} is a deployment binding, not release config`);
429
+ }
430
+ }
431
+ const inspect = (value, parentPath = "") => {
432
+ if (Array.isArray(value)) {
433
+ value.forEach(
434
+ (entry, index) => inspect(entry, `${parentPath}[${index}]`)
435
+ );
436
+ return;
437
+ }
438
+ const current = asRecord(value);
439
+ if (!current) return;
440
+ for (const [key, entry] of Object.entries(current)) {
441
+ const keyPath = parentPath ? `${parentPath}.${key}` : key;
442
+ if (SECRET_CONFIG_KEY_RE.test(key)) {
443
+ throw new Error(`${keyPath} is secret material, not release config`);
444
+ }
445
+ inspect(entry, keyPath);
446
+ }
447
+ };
448
+ inspect(record);
449
+ if (typeof record.INTERVAL !== "string" || !record.INTERVAL.trim()) {
450
+ throw new Error("Published strategy config must define INTERVAL");
451
+ }
452
+ if (record.UNIVERSE !== "crypto" && record.UNIVERSE !== "tradfi") {
453
+ throw new Error("Published strategy config must define a valid UNIVERSE");
454
+ }
455
+ };
456
+ var isRelease = (value) => {
457
+ const release = asRecord(value);
458
+ return Boolean(
459
+ 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.createdAt === "number" && typeof release.createdBy === "string" && typeof release.contentSha256 === "string" && SHA256_RE.test(release.contentSha256)
460
+ );
461
+ };
462
+ var verifyRuntimeStrategyRelease = (value) => {
463
+ if (!isRelease(value)) throw new Error("Invalid runtime strategy release");
464
+ const { contentSha256, ...content } = value;
465
+ if (releaseContentSha256(content) !== contentSha256) {
466
+ throw new Error(
467
+ `${value.strategyName} v${value.releaseVersion} content checksum mismatch`
468
+ );
469
+ }
470
+ assertRuntimeStrategyReleaseConfig(value.config);
471
+ return value;
472
+ };
473
+ var getRuntimeStrategyDraft = async (userName, strategyName) => {
474
+ const value = await getData(
475
+ redisKeys.runtimeStrategyDraft(userName, strategyName),
476
+ null
477
+ );
478
+ const draft = asRecord(value);
479
+ return draft?.schema === import_types.RUNTIME_STRATEGY_DRAFT_SCHEMA && draft.strategyName === strategyName && asRecord(draft.config) ? value : null;
480
+ };
481
+ var saveRuntimeStrategyDraft = async ({
482
+ userName,
483
+ strategyName,
484
+ config,
485
+ baseReleaseVersion = null,
486
+ updatedBy
487
+ }) => {
488
+ assertStrategyName(strategyName);
489
+ const draft = {
490
+ schema: import_types.RUNTIME_STRATEGY_DRAFT_SCHEMA,
491
+ strategyName,
492
+ baseReleaseVersion,
493
+ config,
494
+ updatedAt: Date.now(),
495
+ updatedBy
496
+ };
497
+ await setData(redisKeys.runtimeStrategyDraft(userName, strategyName), draft, {
498
+ expire: 0
499
+ });
500
+ return draft;
501
+ };
502
+ var getRuntimeStrategyRelease = async (userName, strategyName, releaseVersion) => {
503
+ const value = await getData(
504
+ redisKeys.runtimeStrategyRelease(userName, strategyName, releaseVersion),
505
+ null
506
+ );
507
+ return value == null ? null : verifyRuntimeStrategyRelease(value);
508
+ };
509
+ var listRuntimeStrategyReleases = async (userName, strategyName) => {
510
+ const keys = await getKeys(
511
+ redisKeys.runtimeStrategyReleases(userName, strategyName)
512
+ );
513
+ const releases = await Promise.all(
514
+ keys.map(async (key) => {
515
+ const value = await getData(key, null);
516
+ return value == null ? null : verifyRuntimeStrategyRelease(value);
517
+ })
518
+ );
519
+ return releases.filter((release) => release != null).sort((left, right) => right.releaseVersion - left.releaseVersion);
520
+ };
521
+ var publishRuntimeStrategyRelease = async ({
522
+ userName,
523
+ strategyName,
524
+ config,
525
+ strategyPackage = null,
526
+ strategyPackageVersion = null,
527
+ runtimePackageVersion = null,
528
+ createdBy
529
+ }) => {
530
+ assertStrategyName(strategyName);
531
+ assertRuntimeStrategyReleaseConfig(config);
532
+ const releaseVersion = await incrementKey(
533
+ redisKeys.runtimeStrategyReleaseSequence(userName, strategyName)
534
+ );
535
+ const content = {
536
+ schema: import_types.RUNTIME_STRATEGY_RELEASE_SCHEMA,
537
+ strategyName,
538
+ releaseVersion,
539
+ config,
540
+ strategyPackage,
541
+ strategyPackageVersion,
542
+ runtimePackageVersion,
543
+ createdAt: Date.now(),
544
+ createdBy
545
+ };
546
+ const release = {
547
+ ...content,
548
+ contentSha256: releaseContentSha256(content)
549
+ };
550
+ const stored = await setDataIfAbsent(
551
+ redisKeys.runtimeStrategyRelease(userName, strategyName, releaseVersion),
552
+ release
553
+ );
554
+ if (!stored) {
555
+ throw new Error(`Refused to overwrite ${strategyName} v${releaseVersion}`);
556
+ }
557
+ return release;
558
+ };
559
+ var recordRuntimeStrategyControlEvent = async ({
560
+ userName,
561
+ deploymentId,
562
+ strategyName,
563
+ releaseVersion,
564
+ previousState,
565
+ nextState,
566
+ createdBy
567
+ }) => {
568
+ const action = nextState === "entries_paused" ? "pause_entries" : "resume";
569
+ const createdAt = Date.now();
570
+ const event = {
571
+ eventId: `${createdAt}-${(0, import_node_crypto.randomUUID)()}`,
572
+ deploymentId,
573
+ strategyName,
574
+ releaseVersion,
575
+ action,
576
+ previousState,
577
+ nextState,
578
+ createdAt,
579
+ createdBy
580
+ };
581
+ await setData(
582
+ redisKeys.runtimeStrategyControlEvent(userName, event.eventId),
583
+ event,
584
+ { expire: 0 }
585
+ );
586
+ return event;
587
+ };
588
+ // Annotate the CommonJS export names for ESM import in node:
589
+ 0 && (module.exports = {
590
+ assertRuntimeStrategyReleaseConfig,
591
+ getRuntimeStrategyDraft,
592
+ getRuntimeStrategyRelease,
593
+ listRuntimeStrategyReleases,
594
+ publishRuntimeStrategyRelease,
595
+ recordRuntimeStrategyControlEvent,
596
+ saveRuntimeStrategyDraft,
597
+ verifyRuntimeStrategyRelease
598
+ });