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

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,142 +0,0 @@
1
- import { expect } from "chai";
2
- import * as sinon from "sinon";
3
-
4
- import type { Logger } from "@sockethub/schemas";
5
-
6
- import { JobWorker } from "./index";
7
-
8
- const mockLogger: Logger = {
9
- error: () => {},
10
- warn: () => {},
11
- info: () => {},
12
- debug: () => {},
13
- };
14
-
15
- describe("JobWorker", () => {
16
- let MockBull, jobWorker, cryptoMocks, sandbox;
17
-
18
- beforeEach(() => {
19
- sandbox = sinon.createSandbox();
20
- cryptoMocks = {
21
- objectHash: sandbox.stub(),
22
- decrypt: sandbox.stub(),
23
- encrypt: sandbox.stub(),
24
- hash: sandbox.stub(),
25
- };
26
- MockBull = sandbox.stub().returns({
27
- add: sandbox.stub(),
28
- getJob: sandbox.stub(),
29
- removeAllListeners: sandbox.stub(),
30
- pause: sandbox.stub(),
31
- resume: sandbox.stub(),
32
- isPaused: sandbox.stub(),
33
- obliterate: sandbox.stub(),
34
- isRunning: sandbox.stub(),
35
- close: sandbox.stub(),
36
- emit: sandbox.stub(),
37
- disconnect: sandbox.stub(),
38
- on: sandbox.stub().callsArgWith(1, "a job id", "a result string"),
39
- });
40
-
41
- class TestJobWorker extends JobWorker {
42
- init() {
43
- this.redisConnection = MockBull();
44
- const queueName = this.queueId.replace(/:/g, "-");
45
- this.worker = MockBull(
46
- queueName,
47
- this.jobHandler.bind(this),
48
- this.redisConnection,
49
- );
50
- }
51
- initCrypto() {
52
- this.crypto = cryptoMocks;
53
- }
54
- getQueueId() {
55
- return this.queueId;
56
- }
57
- }
58
- jobWorker = new TestJobWorker(
59
- "a parent id",
60
- "a session id",
61
- "secret is 32 char long like this",
62
- {
63
- url: "redis config",
64
- },
65
- mockLogger,
66
- );
67
- jobWorker.emit = sandbox.stub();
68
- });
69
-
70
- afterEach(() => {
71
- sinon.restore();
72
- sandbox.reset();
73
- });
74
-
75
- it("returns a valid JobWorker object", () => {
76
- expect(typeof jobWorker).to.equal("object");
77
- expect(jobWorker.getQueueId()).to.equal(
78
- `sockethub:a parent id:data-layer:queue:a session id`,
79
- );
80
- expect(typeof jobWorker.onJob).to.equal("function");
81
- expect(typeof jobWorker.shutdown).to.equal("function");
82
- });
83
-
84
- describe("onJob", () => {
85
- it("queues the handler", () => {
86
- jobWorker.onJob(() => {
87
- throw new Error("This handler should never be called");
88
- });
89
- sinon.assert.notCalled(jobWorker.worker.on);
90
- });
91
- });
92
-
93
- describe("jobHandler", () => {
94
- it("calls handler as expected", async () => {
95
- cryptoMocks.decrypt.returns("an unencrypted message");
96
- const encryptedJob = {
97
- data: {
98
- title: "a title",
99
- msg: "an encrypted message",
100
- sessionId: "a socket id",
101
- },
102
- };
103
- jobWorker.onJob((job) => {
104
- const decryptedData = encryptedJob.data;
105
- decryptedData.msg = "an unencrypted message";
106
- expect(job).to.eql(decryptedData);
107
- decryptedData.msg += " handled";
108
- return decryptedData;
109
- });
110
- const result = await jobWorker.jobHandler(encryptedJob);
111
- expect(result.msg).to.eql("an unencrypted message handled");
112
- });
113
- });
114
-
115
- describe("decryptJobData", () => {
116
- it("decrypts and returns expected object", () => {
117
- cryptoMocks.decrypt.returnsArg(0);
118
- const jobData = {
119
- data: {
120
- title: "foo",
121
- msg: "encryptedjobdata",
122
- sessionId: "foobar",
123
- },
124
- };
125
- const secret = "secretstring";
126
- expect(jobWorker.decryptJobData(jobData, secret)).to.be.eql(
127
- jobData.data,
128
- );
129
- });
130
- });
131
-
132
- describe("decryptActivityStream", () => {
133
- it("decrypts and returns expected object", () => {
134
- cryptoMocks.decrypt.returnsArg(0);
135
- const jobData = "encryptedjobdata";
136
- const secret = "secretstring";
137
- expect(jobWorker.decryptActivityStream(jobData, secret)).to.be.eql(
138
- jobData,
139
- );
140
- });
141
- });
142
- });
package/src/job-worker.ts DELETED
@@ -1,116 +0,0 @@
1
- import {
2
- createLogger,
3
- getLoggerNamespace,
4
- type Logger,
5
- } from "@sockethub/logger";
6
- import { Worker } from "bullmq";
7
-
8
- import { JobBase } from "./job-base.js";
9
- import { buildQueueId } from "./queue-id.js";
10
- import type { JobEncrypted, JobHandler, RedisConfig } from "./types.js";
11
-
12
- /**
13
- * Worker for processing jobs from a Redis queue within platform child processes.
14
- *
15
- * Connects to the same queue as its corresponding JobQueue instance and processes
16
- * jobs using a platform-specific handler function. Provides automatic decryption
17
- * of job data and error handling.
18
- *
19
- * @example
20
- * ```typescript
21
- * const worker = new JobWorker('irc-platform', 'session123', secret, redisConfig);
22
- * worker.onJob(async (job) => {
23
- * // Process the decrypted ActivityStreams message
24
- * return await processMessage(job.msg);
25
- * });
26
- * ```
27
- */
28
- export class JobWorker extends JobBase {
29
- private readonly connectionName: string;
30
- protected worker: Worker;
31
- protected handler: JobHandler;
32
- private readonly log: Logger;
33
- private readonly redisConfig: RedisConfig;
34
- protected readonly queueId: string;
35
- private initialized = false;
36
-
37
- /**
38
- * Creates a new JobWorker instance.
39
- *
40
- * @param parentId - Must match the parentId of the corresponding JobQueue
41
- * @param instanceId - Must match the instanceId of the corresponding JobQueue
42
- * @param secret - 32-character encryption secret, must match JobQueue secret
43
- * @param redisConfig - Redis connection configuration
44
- */
45
- constructor(
46
- parentId: string,
47
- instanceId: string,
48
- secret: string,
49
- redisConfig: RedisConfig,
50
- ) {
51
- super(secret);
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);
61
- this.redisConfig = redisConfig;
62
- }
63
-
64
- protected init() {
65
- if (this.initialized) {
66
- throw new Error(
67
- `JobWorker already initialized for ${this.queueId}`,
68
- );
69
- }
70
- this.initialized = true;
71
- // BullMQ v5+ prohibits colons in queue names; derive the queue name
72
- // from the canonical queue id by replacing ':' with '-'.
73
- const queueName = this.queueId.replace(/:/g, "-");
74
- // Let BullMQ create its own connection (it duplicates them internally anyway)
75
- this.worker = new Worker(queueName, this.jobHandler.bind(this), {
76
- connection: this.redisConfig,
77
- // Prevent infinite retry loops when platform child process crashes mid-job.
78
- // If worker disappears (crash/disconnect), job becomes "stalled" and retries
79
- // up to maxStalledCount times (with default 30s interval) before failing permanently.
80
- maxStalledCount: 3,
81
- });
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");
89
- }
90
-
91
- /**
92
- * Registers a job handler function and starts processing jobs from the queue.
93
- *
94
- * @param handler - Function that processes decrypted job data and returns results
95
- */
96
- onJob(handler: JobHandler): void {
97
- this.handler = handler;
98
- this.init();
99
- }
100
-
101
- /**
102
- * Gracefully shuts down the worker, stopping job processing and cleaning up connections.
103
- */
104
- async shutdown() {
105
- await this.worker.pause();
106
- this.removeAllListeners();
107
- this.worker.removeAllListeners();
108
- await this.worker.close();
109
- }
110
-
111
- protected async jobHandler(encryptedJob: JobEncrypted) {
112
- const job = this.decryptJobData(encryptedJob);
113
- this.log.debug(`handling ${job.title} ${job.msg.type}`);
114
- return await this.handler(job);
115
- }
116
- }
@@ -1,17 +0,0 @@
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
- });
package/src/queue-id.ts DELETED
@@ -1,14 +0,0 @@
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
- }