vidspotai-shared 1.0.78 → 1.0.80

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.
@@ -1 +1 @@
1
- {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAoIlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAO/C;;;;OAIG;YACW,kBAAkB;IA+B1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqF3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0F3C,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAsGjC;;;;;;OAMG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqEjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,EAAE,iBAAiB,GAAG,MAAM;CA8BvI"}
1
+ {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAoJlB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAO/C;;;;OAIG;YACW,kBAAkB;IA+B1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyG3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0F3C,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAsGjC;;;;;;OAMG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAqEjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,EAAE,iBAAiB,GAAG,MAAM;CA8BvI"}
@@ -127,6 +127,20 @@ function classifyGoogleApiError(err) {
127
127
  if (httpCode === 14 || /high demand/i.test(msg)) {
128
128
  return new errors_1.UserFacingError(msg, errors_1.USER_FACING_ERROR_CODES.VIDEO_PROVIDER_HIGH_DEMAND);
129
129
  }
130
+ // INVALID_ARGUMENT 400 — narrow match: only the specific "use case is
131
+ // currently not supported" string, which Veo returns when our request
132
+ // structure doesn't match the chosen model variant's capabilities.
133
+ // The pre-call guards above (duration=8 for lastFrame/refs) should
134
+ // prevent the known cases; if we still hit this it's a NEW combo we
135
+ // haven't profiled — surface as CAPABILITY_MISMATCH so the user gets a
136
+ // useful message, AND keep the raw provider text in the error so the
137
+ // next entry in PROD_FIX_LOG can identify which combo broke. Generic
138
+ // 400s (other INVALID_ARGUMENT variants) still surface as `error` so
139
+ // a real platform bug isn't muted.
140
+ if ((status === "INVALID_ARGUMENT" || httpCode === 400) &&
141
+ /use case is currently not supported/i.test(msg)) {
142
+ return new errors_1.UserFacingError(msg, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
143
+ }
130
144
  }
131
145
  catch {
132
146
  // Not JSON — fall through to non-JSON checks.
@@ -183,6 +197,21 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
183
197
  const modelConfig = aiModels_1.aiModelConfigs[params.modelKey];
184
198
  const modelId = modelConfig.modelId;
185
199
  const isVeo3_1 = VEO_3_1_MODELS.has(params.modelKey);
200
+ // Gemini Veo cross-constraint: first+last-frame interpolation AND
201
+ // reference images BOTH require durationSeconds=8. Sending any other
202
+ // duration returns INVALID_ARGUMENT 400 "Your use case is currently not
203
+ // supported." with no hint about which param caused it. Surface a
204
+ // typed UserFacingError so the user/frontend can correct the input
205
+ // instead of burning a provider call + opaque rejection.
206
+ const needsDuration8 = !!params.lastFrameImageUrl ||
207
+ (isVeo3_1 && (params.referenceImageUrls?.length ?? 0) > 0);
208
+ if (needsDuration8 && params.duration !== undefined && params.duration !== 8) {
209
+ const constraint = params.lastFrameImageUrl
210
+ ? "first-frame + last-frame interpolation"
211
+ : "reference images";
212
+ throw new errors_1.UserFacingError(`Google Veo requires an 8-second duration when using ${constraint}. ` +
213
+ `Please select 8s or remove the ${params.lastFrameImageUrl ? "last-frame image" : "reference images"}.`, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
214
+ }
186
215
  const request = {
187
216
  model: modelId,
188
217
  prompt: params.prompt,
@@ -1 +1 @@
1
- {"version":3,"file":"openai.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/openai/openai.service.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAElB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,MAAM,CAAS;;IAQjB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAuC3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAsC3C,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IA2ChC;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA6HjC,aAAa,CAAC,EACZ,QAAQ,EACR,UAAuB,EACvB,QAAY,EACZ,SAAiB,EACjB,SAAa,EACb,OAAO,GACR,EAAE,iBAAiB,GAAG,MAAM;CAoB9B"}
1
+ {"version":3,"file":"openai.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/openai/openai.service.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAElB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,MAAM,CAAS;;IAQjB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA0E3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAsC3C,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IA2ChC;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA6HjC,aAAa,CAAC,EACZ,QAAQ,EACR,UAAuB,EACvB,QAAY,EACZ,SAAiB,EACjB,SAAa,EACb,OAAO,GACR,EAAE,iBAAiB,GAAG,MAAM;CAoB9B"}
@@ -61,19 +61,57 @@ class OpenaiService extends baseAiGenProvider_service_1.BaseAiGenProviderService
61
61
  };
62
62
  // First-frame image-to-video. The Sora API requires the reference image
63
63
  // dimensions to match `size`; the caller is responsible for that.
64
- // Note: openai SDK v6.3.0 does NOT yet expose extensions / characters /
65
- // edits endpoints — those exist in the REST API but are not surfaced here.
64
+ //
65
+ // We use the JSON variant `input_reference: { image_url }` (OpenAI
66
+ // fetches the image themselves) rather than the multipart `Uploadable`
67
+ // shape. Reason: as of late 2026 sora-2-pro started returning
68
+ // 400 Invalid type for 'input_reference': expected an object, but
69
+ // got a file instead.
70
+ // for multipart uploads that worked on sora-2. The JSON form is
71
+ // documented for both models and avoids the size mismatch entirely.
72
+ //
73
+ // openai SDK 6.3.0's typed shape for input_reference is `Uploadable`
74
+ // (no JSON-object overload yet), so we bypass the type with a cast.
75
+ // The HTTP layer serializes plain objects as JSON automatically when
76
+ // no Uploadable is present in the body, which is exactly what we want.
77
+ //
78
+ // Note: openai SDK v6.3.0 does NOT yet expose extensions / characters
79
+ // / edits endpoints — those exist in the REST API but are not surfaced.
66
80
  if (params.inputImageUrl) {
67
- const resp = await fetch(params.inputImageUrl);
68
- // fetch() does not throw on 4xx/5xx surface the HTTP error so we
69
- // don't ship an HTML error page to Sora as if it were image bytes.
70
- if (!resp.ok) {
71
- throw new errors_1.UserFacingError(`Input image could not be downloaded (HTTP ${resp.status}). The image URL may have expired or been deleted.`);
81
+ // SDK 6.3.0 types input_reference as Uploadable only; the JSON
82
+ // object form is a runtime-supported overload that the types
83
+ // haven't caught up to yet. Bypass via unknown cast.
84
+ request.input_reference = {
85
+ image_url: params.inputImageUrl,
86
+ };
87
+ }
88
+ let job;
89
+ try {
90
+ job = await this.client.videos.create(request);
91
+ }
92
+ catch (err) {
93
+ // OpenAI client surfaces 400s via APIError with `status`/`message`.
94
+ // The most common case on sora-2-pro right now:
95
+ // 400 Invalid type for 'input_reference': expected an object, but got a file instead.
96
+ // Either the API now requires `input_reference` to be an uploaded file
97
+ // reference object (file_id) rather than a multipart Uploadable, or it's
98
+ // a transient SDK<>endpoint mismatch. Either way, this is not an
99
+ // ops-actionable bug — surface as a translatable capability mismatch
100
+ // so the user can switch model / disable image-to-video instead of
101
+ // firing Slack on every attempt.
102
+ const status = err?.status ?? err?.response?.status;
103
+ const message = err?.message ?? String(err);
104
+ if (status === 400) {
105
+ throw new errors_1.UserFacingError(message, errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
106
+ }
107
+ if (status === 401 || status === 403) {
108
+ throw new errors_1.UserFacingError(message, errors_1.USER_FACING_ERROR_CODES.PROVIDER_AUTH_ERROR);
109
+ }
110
+ if (status === 429) {
111
+ throw new errors_1.UserFacingError(message, errors_1.USER_FACING_ERROR_CODES.VIDEO_PROVIDER_RATE_LIMITED);
72
112
  }
73
- const filename = (params.inputImageUrl.split("?")[0] ?? "reference").split("/").pop() || "reference.png";
74
- request.input_reference = await (0, openai_1.toFile)(resp, filename);
113
+ throw err;
75
114
  }
76
- const job = await this.client.videos.create(request);
77
115
  if (job.status === "failed") {
78
116
  throw new Error(`OpenAI video generation failed: ${JSON.stringify(job.error)}`);
79
117
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bullmq.service.d.ts","sourceRoot":"","sources":["../../src/services/bullmq.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAgB,EAAwB,YAAY,EAAE,MAAM,SAAS,CAAC;AAItE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE1C,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,cAAM,aAAa;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAgB;IACvC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAgB;IAIpC,OAAO,CAAC,UAAU,CAAC,CAAc;IACjC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAE7B,OAAO;WAQO,WAAW,CAAC,OAAO,EAAE,oBAAoB;IAOvD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,aAAa;IAOrB,sDAAsD;IACzC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBlC,gBAAgB;IACH,MAAM,CACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAUpC,YAAY,CACjB,UAAU,EAAE,MAAM,EAAE,EACpB,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,EACtC,eAAe,CAAC,EAAE,MAAM,CACtB,MAAM,EACN;QACE,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CACF;CAwGJ;AASD,eAAO,MAAM,MAAM,eAGjB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC"}
1
+ {"version":3,"file":"bullmq.service.d.ts","sourceRoot":"","sources":["../../src/services/bullmq.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAiB,GAAG,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAgB,EAAwB,YAAY,EAAE,MAAM,SAAS,CAAC;AAItE,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAc1C,UAAU,oBAAoB;IAC5B,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,cAAM,aAAa;IACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAgB;IACvC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,YAAY,CAAgB;IAIpC,OAAO,CAAC,UAAU,CAAC,CAAc;IACjC,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAAS;IAE7B,OAAO;WAQO,WAAW,CAAC,OAAO,EAAE,oBAAoB;IAOvD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,aAAa;IAOrB,sDAAsD;IACzC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBlC,gBAAgB;IACH,MAAM,CACjB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,OAAO,EACb,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE;IAUpC,YAAY,CACjB,UAAU,EAAE,MAAM,EAAE,EACpB,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,EACtC,eAAe,CAAC,EAAE,MAAM,CACtB,MAAM,EACN;QACE,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CACF;CAkHJ;AASD,eAAO,MAAM,MAAM,eAGjB,CAAC;AAEH,MAAM,MAAM,SAAS,GAAG,GAAG,CAAC"}
@@ -9,6 +9,17 @@ const bullmq_1 = require("bullmq");
9
9
  const ioredis_1 = __importDefault(require("ioredis"));
10
10
  const logger_1 = require("../utils/logger");
11
11
  const redisOptions_1 = require("./redisOptions");
12
+ // Transient ioredis socket errors that BullMQ's worker `error` event surfaces
13
+ // verbatim. ioredis reconnects from these on its own — they're noise, not
14
+ // outages. Kept in sync with TRANSIENT_REDIS_ERROR_CODES in redisOptions.ts.
15
+ const TRANSIENT_WORKER_ERROR_CODES = new Set([
16
+ "ECONNRESET",
17
+ "ETIMEDOUT",
18
+ "ECONNREFUSED",
19
+ "EPIPE",
20
+ "ENOTFOUND",
21
+ "EAI_AGAIN",
22
+ ]);
12
23
  class BullMQService {
13
24
  constructor({ redisOptions, concurrency = 10, }) {
14
25
  this.queues = new Map();
@@ -132,9 +143,18 @@ class BullMQService {
132
143
  });
133
144
  // Worker-shell failure (Redis lost, malformed payload, processor
134
145
  // import error, etc.). Without this handler these crashes are silent.
146
+ // Transient ioredis network blips (ECONNRESET/ETIMEDOUT/EPIPE) bubble
147
+ // through this same listener — ioredis recovers them automatically,
148
+ // so demote to `warn` to keep Slack quiet. Real shell failures (auth,
149
+ // parser, malformed payload) still surface at `error` → Slack.
135
150
  worker.on("error", (err) => {
136
- logger_1.logger.error(`BullMQ worker error`, {
151
+ const code = err?.code;
152
+ const isTransient = !!code && TRANSIENT_WORKER_ERROR_CODES.has(code);
153
+ const logFn = isTransient ? logger_1.logger.warn.bind(logger_1.logger) : logger_1.logger.error.bind(logger_1.logger);
154
+ logFn(`BullMQ worker error`, {
137
155
  queueName,
156
+ code,
157
+ transient: isTransient,
138
158
  err: err?.stack ?? err?.message ?? String(err),
139
159
  });
140
160
  });
@@ -9,6 +9,18 @@ import type { RedisOptions } from "ioredis";
9
9
  * - `retryStrategy` — exponential 200ms → 10s cap; infinite retries.
10
10
  * - `reconnectOnError` — reconnect on READONLY (failover to a replica
11
11
  * that has since become primary).
12
+ * - `keepAlive: 30_000` — send a TCP keep-alive probe every 30s. Without
13
+ * this, BullMQ's blocking worker connections (BRPOPLPUSH idle while
14
+ * waiting for jobs) get silently dropped by intermediate NATs / Redis
15
+ * Cloud's idle-connection killer after ~60-300s. The next read returns
16
+ * ECONNRESET / ETIMEDOUT and ioredis reconnects, but each drop fires
17
+ * `error` events on every queue's worker. Keep-alive prevents the
18
+ * silent-drop in the first place, which is the actual root cause of
19
+ * the recurring ECONNRESET / ETIMEDOUT flood we were seeing in Slack.
20
+ * - `connectTimeout: 15_000` — bounds new-connection establishment so a
21
+ * slow DNS / handshake fails fast and retryStrategy kicks in cleanly.
22
+ * - `noDelay: true` — disable Nagle so small commands (PING, BRPOPLPUSH
23
+ * wakeups) don't pile up waiting for an MTU's worth of data.
12
24
  */
13
25
  export declare const sharedRedisOptions: RedisOptions;
14
26
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"redisOptions.d.ts","sourceRoot":"","sources":["../../src/services/redisOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,EAAE,YAMhC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE;IAAE,EAAE,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,IAAI,CAAA;CAAE,EAC/D,KAAK,EAAE,MAAM,GACZ,IAAI,CAwBN"}
1
+ {"version":3,"file":"redisOptions.d.ts","sourceRoot":"","sources":["../../src/services/redisOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG5C;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,EAAE,YAShC,CAAC;AAkBF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE;IAAE,EAAE,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,KAAK,IAAI,KAAK,IAAI,CAAA;CAAE,EAC/D,KAAK,EAAE,MAAM,GACZ,IAAI,CA2BN"}
@@ -13,14 +13,44 @@ const logger_1 = require("../utils/logger");
13
13
  * - `retryStrategy` — exponential 200ms → 10s cap; infinite retries.
14
14
  * - `reconnectOnError` — reconnect on READONLY (failover to a replica
15
15
  * that has since become primary).
16
+ * - `keepAlive: 30_000` — send a TCP keep-alive probe every 30s. Without
17
+ * this, BullMQ's blocking worker connections (BRPOPLPUSH idle while
18
+ * waiting for jobs) get silently dropped by intermediate NATs / Redis
19
+ * Cloud's idle-connection killer after ~60-300s. The next read returns
20
+ * ECONNRESET / ETIMEDOUT and ioredis reconnects, but each drop fires
21
+ * `error` events on every queue's worker. Keep-alive prevents the
22
+ * silent-drop in the first place, which is the actual root cause of
23
+ * the recurring ECONNRESET / ETIMEDOUT flood we were seeing in Slack.
24
+ * - `connectTimeout: 15_000` — bounds new-connection establishment so a
25
+ * slow DNS / handshake fails fast and retryStrategy kicks in cleanly.
26
+ * - `noDelay: true` — disable Nagle so small commands (PING, BRPOPLPUSH
27
+ * wakeups) don't pile up waiting for an MTU's worth of data.
16
28
  */
17
29
  exports.sharedRedisOptions = {
18
30
  maxRetriesPerRequest: null,
19
31
  enableReadyCheck: true,
20
32
  enableOfflineQueue: true,
33
+ keepAlive: 30000,
34
+ connectTimeout: 15000,
35
+ noDelay: true,
21
36
  retryStrategy: (times) => Math.min(200 * Math.pow(1.5, times), 10000),
22
37
  reconnectOnError: (err) => err.message.includes("READONLY"),
23
38
  };
39
+ /**
40
+ * ioredis auto-reconnects from these — they are operational noise during
41
+ * a Redis Cloud failover, a brief TCP reset, or a sleeping connection
42
+ * timing out. Logging them at `error` pages Slack every time the window
43
+ * expires; demote to `warn` so the Slack transport (error-only) ignores
44
+ * them. Real persistent outages still surface via BullMQ job timeouts.
45
+ */
46
+ const TRANSIENT_REDIS_ERROR_CODES = new Set([
47
+ "ECONNRESET",
48
+ "ETIMEDOUT",
49
+ "ECONNREFUSED",
50
+ "EPIPE",
51
+ "ENOTFOUND",
52
+ "EAI_AGAIN",
53
+ ]);
24
54
  /**
25
55
  * Attach observability + error-dedupe listeners to an ioredis client.
26
56
  * Reduces log spam during prolonged outages (e.g. DNS NXDOMAIN, network
@@ -42,9 +72,12 @@ function attachRedisListeners(client, label) {
42
72
  return;
43
73
  lastErrCode = code;
44
74
  lastErrAt = now;
45
- logger_1.logger.error("redis: connection error", {
75
+ const isTransient = !!err.code && TRANSIENT_REDIS_ERROR_CODES.has(err.code);
76
+ const logFn = isTransient ? logger_1.logger.warn.bind(logger_1.logger) : logger_1.logger.error.bind(logger_1.logger);
77
+ logFn("redis: connection error", {
46
78
  label,
47
79
  code,
80
+ transient: isTransient,
48
81
  err: err?.stack ?? err?.message ?? String(err),
49
82
  });
50
83
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidspotai-shared",
3
- "version": "1.0.78",
3
+ "version": "1.0.80",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "exports": {