@sockethub/data-layer 1.0.0-alpha.10 → 1.0.0-alpha.12

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/src/job-queue.ts CHANGED
@@ -1,13 +1,18 @@
1
+ import {
2
+ createLogger,
3
+ getLoggerNamespace,
4
+ type Logger,
5
+ } from "@sockethub/logger";
6
+ import { type ActivityStream, resolvePlatformId } from "@sockethub/schemas";
1
7
  import { type Job, Queue, QueueEvents, Worker } from "bullmq";
2
- import debug, { type Debugger } from "debug";
3
8
 
4
- import type { ActivityStream } from "@sockethub/schemas";
5
-
6
- import { JobBase, createIORedisConnection } from "./job-base.js";
9
+ import { JobBase } from "./job-base.js";
10
+ import { buildQueueId } from "./queue-id.js";
7
11
  import type { JobDataEncrypted, JobDecrypted, RedisConfig } from "./types.js";
8
12
 
9
13
  export async function verifyJobQueue(config: RedisConfig): Promise<void> {
10
- const log = debug("sockethub:data-layer:queue");
14
+ const log = createLogger("data-layer:verify-job-queue");
15
+
11
16
  return new Promise((resolve, reject) => {
12
17
  const worker = new Worker(
13
18
  "connectiontest",
@@ -20,7 +25,7 @@ export async function verifyJobQueue(config: RedisConfig): Promise<void> {
20
25
  job.data.test = "touched by worker";
21
26
  },
22
27
  {
23
- connection: createIORedisConnection(config),
28
+ connection: config,
24
29
  },
25
30
  );
26
31
  worker.on("completed", async (job: Job) => {
@@ -29,22 +34,22 @@ export async function verifyJobQueue(config: RedisConfig): Promise<void> {
29
34
  "Worker job completed unsuccessfully during JobQueue connection test",
30
35
  );
31
36
  }
32
- log("connection verified");
37
+ log.info("job queue connection verified");
33
38
  await queue.close();
34
39
  await worker.close();
35
40
  resolve();
36
41
  });
37
42
  worker.on("error", (err) => {
38
- log(
43
+ log.warn(
39
44
  `connection verification worker error received ${err.toString()}`,
40
45
  );
41
46
  reject(err);
42
47
  });
43
48
  const queue = new Queue("connectiontest", {
44
- connection: createIORedisConnection(config),
49
+ connection: config,
45
50
  });
46
51
  queue.on("error", (err) => {
47
- log(
52
+ log.warn(
48
53
  `connection verification queue error received ${err.toString()}`,
49
54
  );
50
55
  reject(err);
@@ -71,43 +76,49 @@ export async function verifyJobQueue(config: RedisConfig): Promise<void> {
71
76
  * ```
72
77
  */
73
78
  export class JobQueue extends JobBase {
74
- readonly uid: string;
79
+ private readonly connectionName: string;
80
+ protected readonly queueId: string;
75
81
  protected queue: Queue;
76
82
  protected events: QueueEvents;
77
- private readonly debug: Debugger;
83
+ private readonly log: Logger;
78
84
  private counter = 0;
79
85
  private initialized = false;
80
86
 
81
87
  /**
82
88
  * Creates a new JobQueue instance.
83
89
  *
90
+ * @param parentId - Sockethub instance identifier for queue isolation
84
91
  * @param instanceId - Unique identifier for the platform instance
85
- * @param sessionId - Client session identifier for queue isolation
86
92
  * @param secret - 32-character encryption secret for message security
87
93
  * @param redisConfig - Redis connection configuration
88
94
  */
89
95
  constructor(
96
+ parentId: string,
90
97
  instanceId: string,
91
- sessionId: string,
92
98
  secret: string,
93
99
  redisConfig: RedisConfig,
94
100
  ) {
95
101
  super(secret);
96
- this.uid = `sockethub:data-layer:queue:${instanceId}:${sessionId}`;
97
- this.debug = debug(this.uid);
102
+ // Create logger with full namespace (context will be prepended automatically)
103
+ this.log = createLogger(`data-layer:queue:${parentId}:${instanceId}`);
104
+
105
+ this.queueId = buildQueueId(parentId, instanceId);
106
+ // Use logger's full namespace (includes context) for Redis connection name
107
+ this.connectionName = getLoggerNamespace(this.log);
108
+ redisConfig.connectionName = this.connectionName;
98
109
  this.init(redisConfig);
99
110
  }
100
111
 
101
112
  protected init(redisConfig: RedisConfig) {
102
113
  if (this.initialized) {
103
- throw new Error(`JobQueue already initialized for ${this.uid}`);
114
+ throw new Error(`JobQueue already initialized for ${this.queueId}`);
104
115
  }
105
116
  this.initialized = true;
106
117
 
107
- // BullMQ v5+ prohibits colons in queue names, so replace with dashes
108
- // while keeping uid with colons for debug namespace convention
109
- const queueName = this.uid.replace(/:/g, "-");
110
- // Let BullMQ create its own connections for better lifecycle management
118
+ // BullMQ v5+ prohibits colons in queue names; derive the queue name
119
+ // from the canonical queue id by replacing ':' with '-'.
120
+ const queueName = this.queueId.replace(/:/g, "-");
121
+ // Let BullMQ create its own connections (it duplicates them internally anyway)
111
122
  this.queue = new Queue(queueName, {
112
123
  connection: redisConfig,
113
124
  });
@@ -115,30 +126,39 @@ export class JobQueue extends JobBase {
115
126
  connection: redisConfig,
116
127
  });
117
128
 
129
+ // Handle Redis contention errors (e.g., BUSY from Lua scripts)
130
+ this.queue.on("error", (err) => {
131
+ this.log.warn(`queue error: ${err.message}`);
132
+ });
133
+
134
+ this.events.on("error", (err) => {
135
+ this.log.warn(`events error: ${err.message}`);
136
+ });
137
+
118
138
  this.events.on("completed", async ({ jobId, returnvalue }) => {
119
139
  const job = await this.getJob(jobId);
120
140
  if (!job) {
121
- this.debug(`completed job ${jobId} (already removed)`);
141
+ this.log.debug(`completed job ${jobId} (already removed)`);
122
142
  return;
123
143
  }
124
- this.debug(`completed ${job.data.title} ${job.data.msg.type}`);
144
+ this.log.debug(`completed ${job.data.title} ${job.data.msg.type}`);
125
145
  this.emit("completed", job.data, returnvalue);
126
146
  });
127
147
 
128
148
  this.events.on("failed", async ({ jobId, failedReason }) => {
129
149
  const job = await this.getJob(jobId);
130
150
  if (!job) {
131
- this.debug(
151
+ this.log.debug(
132
152
  `failed job ${jobId} (already removed): ${failedReason}`,
133
153
  );
134
154
  return;
135
155
  }
136
- this.debug(
156
+ this.log.warn(
137
157
  `failed ${job.data.title} ${job.data.msg.type}: ${failedReason}`,
138
158
  );
139
159
  this.emit("failed", job.data, failedReason);
140
160
  });
141
- this.debug("initialized");
161
+ this.log.info("initialized");
142
162
  }
143
163
 
144
164
  /**
@@ -156,7 +176,7 @@ export class JobQueue extends JobBase {
156
176
  const job = this.createJob(socketId, msg);
157
177
  if (await this.queue.isPaused()) {
158
178
  // this.queue.emit("error", new Error("queue closed"));
159
- this.debug(
179
+ this.log.debug(
160
180
  `failed to add ${job.title} ${msg.type} to queue: queue closed`,
161
181
  );
162
182
  throw new Error("queue closed");
@@ -168,7 +188,7 @@ export class JobQueue extends JobBase {
168
188
  removeOnComplete: { age: 300 }, // 5 minutes in seconds
169
189
  removeOnFail: { age: 300 },
170
190
  });
171
- this.debug(`added ${job.title} ${msg.type} to queue`);
191
+ this.log.debug(`added ${job.title} ${msg.type} to queue`);
172
192
  return job;
173
193
  }
174
194
 
@@ -177,7 +197,7 @@ export class JobQueue extends JobBase {
177
197
  */
178
198
  async pause() {
179
199
  await this.queue.pause();
180
- this.debug("paused");
200
+ this.log.debug("paused");
181
201
  }
182
202
 
183
203
  /**
@@ -185,7 +205,7 @@ export class JobQueue extends JobBase {
185
205
  */
186
206
  async resume() {
187
207
  await this.queue.resume();
188
- this.debug("resumed");
208
+ this.log.debug("resumed");
189
209
  }
190
210
 
191
211
  /**
@@ -207,9 +227,8 @@ export class JobQueue extends JobBase {
207
227
  if (job) {
208
228
  job.data = this.decryptJobData(job);
209
229
  try {
210
- // biome-ignore lint/performance/noDelete: <explanation>
211
230
  delete job.data.msg.sessionSecret;
212
- } catch (e) {
231
+ } catch (_e) {
213
232
  // this property should never be exposed externally
214
233
  }
215
234
  }
@@ -217,7 +236,8 @@ export class JobQueue extends JobBase {
217
236
  }
218
237
 
219
238
  private createJob(socketId: string, msg: ActivityStream): JobDataEncrypted {
220
- const title = `${msg.context}-${msg.id ? msg.id : this.counter++}`;
239
+ const platformId = resolvePlatformId(msg) || "unknown";
240
+ const title = `${platformId}-${msg.id ? msg.id : this.counter++}`;
221
241
  return {
222
242
  title: title,
223
243
  sessionId: socketId,
@@ -1,8 +1,17 @@
1
1
  import { expect } from "chai";
2
2
  import * as sinon from "sinon";
3
3
 
4
+ import type { Logger } from "@sockethub/schemas";
5
+
4
6
  import { JobWorker } from "./index";
5
7
 
8
+ const mockLogger: Logger = {
9
+ error: () => {},
10
+ warn: () => {},
11
+ info: () => {},
12
+ debug: () => {},
13
+ };
14
+
6
15
  describe("JobWorker", () => {
7
16
  let MockBull, jobWorker, cryptoMocks, sandbox;
8
17
 
@@ -32,8 +41,9 @@ describe("JobWorker", () => {
32
41
  class TestJobWorker extends JobWorker {
33
42
  init() {
34
43
  this.redisConnection = MockBull();
44
+ const queueName = this.queueId.replace(/:/g, "-");
35
45
  this.worker = MockBull(
36
- this.uid,
46
+ queueName,
37
47
  this.jobHandler.bind(this),
38
48
  this.redisConnection,
39
49
  );
@@ -41,6 +51,9 @@ describe("JobWorker", () => {
41
51
  initCrypto() {
42
52
  this.crypto = cryptoMocks;
43
53
  }
54
+ getQueueId() {
55
+ return this.queueId;
56
+ }
44
57
  }
45
58
  jobWorker = new TestJobWorker(
46
59
  "a parent id",
@@ -49,6 +62,7 @@ describe("JobWorker", () => {
49
62
  {
50
63
  url: "redis config",
51
64
  },
65
+ mockLogger,
52
66
  );
53
67
  jobWorker.emit = sandbox.stub();
54
68
  });
@@ -60,8 +74,8 @@ describe("JobWorker", () => {
60
74
 
61
75
  it("returns a valid JobWorker object", () => {
62
76
  expect(typeof jobWorker).to.equal("object");
63
- expect(jobWorker.uid).to.equal(
64
- `sockethub:data-layer:worker:a parent id:a session id`,
77
+ expect(jobWorker.getQueueId()).to.equal(
78
+ `sockethub:a parent id:data-layer:queue:a session id`,
65
79
  );
66
80
  expect(typeof jobWorker.onJob).to.equal("function");
67
81
  expect(typeof jobWorker.shutdown).to.equal("function");
package/src/job-worker.ts CHANGED
@@ -1,7 +1,12 @@
1
+ import {
2
+ createLogger,
3
+ getLoggerNamespace,
4
+ type Logger,
5
+ } from "@sockethub/logger";
1
6
  import { Worker } from "bullmq";
2
- import debug, { type Debugger } from "debug";
3
7
 
4
- import { JobBase, createIORedisConnection } from "./job-base.js";
8
+ import { JobBase } from "./job-base.js";
9
+ import { buildQueueId } from "./queue-id.js";
5
10
  import type { JobEncrypted, JobHandler, RedisConfig } from "./types.js";
6
11
 
7
12
  /**
@@ -21,44 +26,52 @@ import type { JobEncrypted, JobHandler, RedisConfig } from "./types.js";
21
26
  * ```
22
27
  */
23
28
  export class JobWorker extends JobBase {
24
- readonly uid: string;
29
+ private readonly connectionName: string;
25
30
  protected worker: Worker;
26
31
  protected handler: JobHandler;
27
- private readonly debug: Debugger;
32
+ private readonly log: Logger;
28
33
  private readonly redisConfig: RedisConfig;
29
- private readonly queueId: string;
34
+ protected readonly queueId: string;
30
35
  private initialized = false;
31
36
 
32
37
  /**
33
38
  * Creates a new JobWorker instance.
34
39
  *
40
+ * @param parentId - Must match the parentId of the corresponding JobQueue
35
41
  * @param instanceId - Must match the instanceId of the corresponding JobQueue
36
- * @param sessionId - Must match the sessionId of the corresponding JobQueue
37
42
  * @param secret - 32-character encryption secret, must match JobQueue secret
38
43
  * @param redisConfig - Redis connection configuration
39
44
  */
40
45
  constructor(
46
+ parentId: string,
41
47
  instanceId: string,
42
- sessionId: string,
43
48
  secret: string,
44
49
  redisConfig: RedisConfig,
45
50
  ) {
46
51
  super(secret);
47
- this.uid = `sockethub:data-layer:worker:${instanceId}:${sessionId}`;
48
- this.queueId = `sockethub:data-layer:queue:${instanceId}:${sessionId}`;
49
- this.debug = debug(this.uid);
52
+ // Create logger with full namespace (context will be prepended automatically)
53
+ this.log = createLogger(`data-layer:worker:${parentId}:${instanceId}`);
54
+
55
+ // Use logger's full namespace (includes context) for Redis connection name
56
+ this.connectionName = getLoggerNamespace(this.log);
57
+ redisConfig.connectionName = this.connectionName;
58
+
59
+ // Queue ID must match JobQueue's namespace (context-free) for cross-process connection
60
+ this.queueId = buildQueueId(parentId, instanceId);
50
61
  this.redisConfig = redisConfig;
51
62
  }
52
63
 
53
64
  protected init() {
54
65
  if (this.initialized) {
55
- throw new Error(`JobWorker already initialized for ${this.uid}`);
66
+ throw new Error(
67
+ `JobWorker already initialized for ${this.queueId}`,
68
+ );
56
69
  }
57
70
  this.initialized = true;
58
- // BullMQ v5+ prohibits colons in queue names, so replace with dashes
59
- // while keeping queueId with colons for debug namespace convention
71
+ // BullMQ v5+ prohibits colons in queue names; derive the queue name
72
+ // from the canonical queue id by replacing ':' with '-'.
60
73
  const queueName = this.queueId.replace(/:/g, "-");
61
- // Let BullMQ create its own connection for better lifecycle management
74
+ // Let BullMQ create its own connection (it duplicates them internally anyway)
62
75
  this.worker = new Worker(queueName, this.jobHandler.bind(this), {
63
76
  connection: this.redisConfig,
64
77
  // Prevent infinite retry loops when platform child process crashes mid-job.
@@ -66,7 +79,13 @@ export class JobWorker extends JobBase {
66
79
  // up to maxStalledCount times (with default 30s interval) before failing permanently.
67
80
  maxStalledCount: 3,
68
81
  });
69
- this.debug("initialized");
82
+
83
+ // Handle Redis contention errors (e.g., BUSY from Lua scripts)
84
+ this.worker.on("error", (err) => {
85
+ this.log.warn(`worker error: ${err.message}`);
86
+ });
87
+
88
+ this.log.info("initialized");
70
89
  }
71
90
 
72
91
  /**
@@ -91,7 +110,7 @@ export class JobWorker extends JobBase {
91
110
 
92
111
  protected async jobHandler(encryptedJob: JobEncrypted) {
93
112
  const job = this.decryptJobData(encryptedJob);
94
- this.debug(`handling ${job.title} ${job.msg.type}`);
113
+ this.log.debug(`handling ${job.title} ${job.msg.type}`);
95
114
  return await this.handler(job);
96
115
  }
97
116
  }
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { buildCredentialsStoreId, buildQueueId } from "./queue-id";
4
+
5
+ describe("queue-id helpers", () => {
6
+ it("buildQueueId uses canonical namespace", () => {
7
+ expect(buildQueueId("parent", "platform")).toBe(
8
+ "sockethub:parent:data-layer:queue:platform",
9
+ );
10
+ });
11
+
12
+ it("buildCredentialsStoreId uses canonical namespace", () => {
13
+ expect(buildCredentialsStoreId("parent", "session")).toBe(
14
+ "sockethub:parent:data-layer:credentials-store:session",
15
+ );
16
+ });
17
+ });
@@ -0,0 +1,14 @@
1
+ const REDIS_QUEUE_PREFIX = "sockethub";
2
+
3
+ // Canonical ids use ':' separators; BullMQ queue names are derived by replacing
4
+ // ':' with '-' when constructing the queue name.
5
+ export function buildQueueId(parentId: string, instanceId: string): string {
6
+ return `${REDIS_QUEUE_PREFIX}:${parentId}:data-layer:queue:${instanceId}`;
7
+ }
8
+
9
+ export function buildCredentialsStoreId(
10
+ parentId: string,
11
+ sessionId: string,
12
+ ): string {
13
+ return `${REDIS_QUEUE_PREFIX}:${parentId}:data-layer:credentials-store:${sessionId}`;
14
+ }
package/src/types.ts CHANGED
@@ -5,6 +5,10 @@ import type {
5
5
 
6
6
  export type RedisConfig = {
7
7
  url: string;
8
+ connectTimeout?: number;
9
+ disconnectTimeout?: number;
10
+ maxRetriesPerRequest?: number | null;
11
+ connectionName?: string;
8
12
  };
9
13
 
10
14
  export interface JobDataEncrypted {