@blokjs/trigger-worker 0.6.17 → 0.6.19

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.
Files changed (37) hide show
  1. package/dist/WorkerTrigger.d.ts +27 -3
  2. package/dist/WorkerTrigger.js +168 -26
  3. package/dist/adapters/KafkaAdapter.d.ts +5 -0
  4. package/dist/adapters/KafkaAdapter.js +12 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +2 -2
  7. package/package.json +5 -4
  8. package/CHANGELOG.md +0 -22
  9. package/__tests__/integration/nats-adapter.real-nats.test.ts +0 -116
  10. package/__tests__/integration/pgboss-adapter.real-pg.test.ts +0 -164
  11. package/__tests__/integration/rabbitmq-adapter.real-rabbitmq.test.ts +0 -179
  12. package/__tests__/integration/sqs-adapter.real-sqs.test.ts +0 -228
  13. package/src/WorkerTrigger.test.ts +0 -540
  14. package/src/WorkerTrigger.ts +0 -784
  15. package/src/adapters/BullMQAdapter.ts +0 -296
  16. package/src/adapters/InMemoryAdapter.ts +0 -280
  17. package/src/adapters/KafkaAdapter.ts +0 -277
  18. package/src/adapters/NATSAdapter.ts +0 -454
  19. package/src/adapters/PgBossAdapter.ts +0 -293
  20. package/src/adapters/RabbitMQAdapter.ts +0 -285
  21. package/src/adapters/RedisStreamsAdapter.ts +0 -286
  22. package/src/adapters/SQSAdapter.ts +0 -306
  23. package/src/adapters/factory.test.ts +0 -89
  24. package/src/adapters/factory.ts +0 -111
  25. package/src/adapters/new-adapters.test.ts +0 -130
  26. package/src/index.ts +0 -94
  27. package/template/.env.example +0 -13
  28. package/template/package.json +0 -45
  29. package/template/src/Nodes.ts +0 -10
  30. package/template/src/Workflows.ts +0 -8
  31. package/template/src/index.ts +0 -41
  32. package/template/src/runner/WorkerServer.ts +0 -34
  33. package/template/src/runner/types/Workflows.ts +0 -7
  34. package/template/src/workflows/jobs/process-job.ts +0 -47
  35. package/template/tsconfig.json +0 -31
  36. package/template/vitest.config.ts +0 -39
  37. package/tsconfig.json +0 -32
@@ -1,179 +0,0 @@
1
- import type { WorkerJob } from "@blokjs/runner";
2
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
- import { RabbitMQAdapter } from "../../src/adapters/RabbitMQAdapter";
4
-
5
- /**
6
- * Real-RabbitMQ integration test for `RabbitMQAdapter` (closes Phase 2.1
7
- * broker-adapter test debt deferred from PR #91).
8
- *
9
- * Exercises the worker contract end-to-end against a real broker: queue
10
- * declaration, job add, consume + ack, isolation across queues, manual
11
- * `complete()` / `fail()` paths.
12
- *
13
- * Gated on `BLOK_INTEGRATION_RABBITMQ_URL`. Skipped when unset.
14
- *
15
- * Bring up the test fixtures via:
16
- * docker compose -f infra/testing/docker-compose.yml up -d rabbitmq
17
- *
18
- * Then run:
19
- * BLOK_INTEGRATION_RABBITMQ_URL=amqp://blok:blok_test@localhost:5673 bun run test
20
- */
21
-
22
- const RABBITMQ_URL = process.env.BLOK_INTEGRATION_RABBITMQ_URL;
23
- const d = RABBITMQ_URL ? describe : describe.skip;
24
-
25
- const TEST_TIMEOUT_MS = 30_000;
26
-
27
- d("RabbitMQAdapter — real RabbitMQ", () => {
28
- let adapter: RabbitMQAdapter;
29
-
30
- beforeAll(async () => {
31
- adapter = new RabbitMQAdapter({ url: RABBITMQ_URL });
32
- await adapter.connect();
33
- }, TEST_TIMEOUT_MS);
34
-
35
- afterAll(async () => {
36
- await adapter.disconnect();
37
- });
38
-
39
- it(
40
- "publishes a job and the consumer receives it (single-queue happy path)",
41
- async () => {
42
- // Random queue name per run so this test is idempotent against
43
- // a long-lived broker — orphan messages from a prior crashed run
44
- // wouldn't bleed into the count assertion below.
45
- const queue = `blok-test-q-publish-${Math.random().toString(36).slice(2)}`;
46
- const received: WorkerJob[] = [];
47
-
48
- await adapter.process({ queue }, async (job) => {
49
- received.push(job);
50
- await job.complete();
51
- });
52
-
53
- const jobId = await adapter.addJob(queue, { hello: "world", n: 1 });
54
- expect(typeof jobId).toBe("string");
55
- expect(jobId).toBeTruthy();
56
-
57
- await waitFor(() => received.length === 1, TEST_TIMEOUT_MS - 5_000);
58
-
59
- expect(received).toHaveLength(1);
60
- expect(received[0].data).toEqual({ hello: "world", n: 1 });
61
- expect(received[0].queue).toBe(queue);
62
- expect(received[0].id).toBeTruthy();
63
-
64
- await adapter.stopProcessing(queue);
65
- },
66
- TEST_TIMEOUT_MS,
67
- );
68
-
69
- it(
70
- "isolates jobs across distinct queues",
71
- async () => {
72
- const queueA = `blok-test-q-a-${Math.random().toString(36).slice(2)}`;
73
- const queueB = `blok-test-q-b-${Math.random().toString(36).slice(2)}`;
74
- const receivedA: WorkerJob[] = [];
75
- const receivedB: WorkerJob[] = [];
76
-
77
- await adapter.process({ queue: queueA }, async (job) => {
78
- receivedA.push(job);
79
- await job.complete();
80
- });
81
- await adapter.process({ queue: queueB }, async (job) => {
82
- receivedB.push(job);
83
- await job.complete();
84
- });
85
-
86
- await adapter.addJob(queueA, { from: "A" });
87
- await adapter.addJob(queueB, { from: "B" });
88
-
89
- await waitFor(() => receivedA.length === 1 && receivedB.length === 1, TEST_TIMEOUT_MS - 5_000);
90
-
91
- expect(receivedA[0].data).toEqual({ from: "A" });
92
- expect(receivedB[0].data).toEqual({ from: "B" });
93
- expect(receivedA[0].queue).toBe(queueA);
94
- expect(receivedB[0].queue).toBe(queueB);
95
-
96
- await adapter.stopProcessing(queueA);
97
- await adapter.stopProcessing(queueB);
98
- },
99
- TEST_TIMEOUT_MS,
100
- );
101
-
102
- it(
103
- "delivers multiple jobs in order on a single-consumer queue",
104
- async () => {
105
- // Single consumer + prefetch(1) (the adapter's default for
106
- // non-concurrent processing) preserves FIFO order. With multiple
107
- // consumers Rabbit doesn't guarantee global order, so we keep
108
- // this test narrow to single-consumer semantics.
109
- const queue = `blok-test-q-order-${Math.random().toString(36).slice(2)}`;
110
- const received: number[] = [];
111
-
112
- await adapter.process({ queue }, async (job) => {
113
- const data = job.data as { n: number };
114
- received.push(data.n);
115
- await job.complete();
116
- });
117
-
118
- for (let i = 0; i < 5; i++) {
119
- await adapter.addJob(queue, { n: i });
120
- }
121
-
122
- await waitFor(() => received.length === 5, TEST_TIMEOUT_MS - 5_000);
123
-
124
- expect(received).toEqual([0, 1, 2, 3, 4]);
125
-
126
- await adapter.stopProcessing(queue);
127
- },
128
- TEST_TIMEOUT_MS,
129
- );
130
-
131
- it(
132
- "failed jobs requeue when retries remain (manual fail path)",
133
- async () => {
134
- // Adapter NACK-with-requeue contract: the same job message is
135
- // re-delivered after `fail()` until the retry budget is hit.
136
- // We assert one re-delivery here; the full retry-budget DLQ
137
- // flow is exercised by the adapter unit tests.
138
- const queue = `blok-test-q-retry-${Math.random().toString(36).slice(2)}`;
139
- let attempts = 0;
140
- let firstAttemptId = "";
141
-
142
- await adapter.process({ queue, retries: 1 }, async (job) => {
143
- attempts++;
144
- if (attempts === 1) {
145
- firstAttemptId = job.id;
146
- await job.fail(new Error("simulated transient failure"));
147
- return;
148
- }
149
- // Second delivery: ack so we exit cleanly.
150
- expect(job.id).toBe(firstAttemptId);
151
- await job.complete();
152
- });
153
-
154
- await adapter.addJob(queue, { will_retry: true });
155
-
156
- await waitFor(() => attempts >= 2, TEST_TIMEOUT_MS - 5_000);
157
-
158
- expect(attempts).toBeGreaterThanOrEqual(2);
159
-
160
- await adapter.stopProcessing(queue);
161
- },
162
- TEST_TIMEOUT_MS,
163
- );
164
-
165
- it("isConnected + healthCheck reflect the live state", async () => {
166
- expect(adapter.isConnected()).toBe(true);
167
- const healthy = await adapter.healthCheck();
168
- expect(healthy).toBe(true);
169
- });
170
- });
171
-
172
- async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
173
- const start = Date.now();
174
- while (Date.now() - start < timeoutMs) {
175
- if (predicate()) return;
176
- await new Promise((r) => setTimeout(r, 50));
177
- }
178
- throw new Error(`waitFor timed out after ${timeoutMs}ms`);
179
- }
@@ -1,228 +0,0 @@
1
- import type { WorkerJob } from "@blokjs/runner";
2
- import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
3
- import { SQSAdapter } from "../../src/adapters/SQSAdapter";
4
-
5
- /**
6
- * Real-LocalStack-SQS integration test for `SQSAdapter` (closes Phase 2.1
7
- * broker-adapter test debt deferred from PR #91).
8
- *
9
- * Gated on `BLOK_INTEGRATION_SQS_ENDPOINT`. Skipped when unset.
10
- *
11
- * Bring up the test fixtures via:
12
- * docker compose -f infra/testing/docker-compose.yml up -d localstack
13
- *
14
- * Then run:
15
- * BLOK_INTEGRATION_SQS_ENDPOINT=http://localhost:4567 \
16
- * AWS_REGION=us-east-1 \
17
- * AWS_ACCESS_KEY_ID=test \
18
- * AWS_SECRET_ACCESS_KEY=test \
19
- * bun run test
20
- */
21
-
22
- const SQS_ENDPOINT = process.env.BLOK_INTEGRATION_SQS_ENDPOINT;
23
- const d = SQS_ENDPOINT ? describe : describe.skip;
24
-
25
- const TEST_TIMEOUT_MS = 30_000;
26
-
27
- /**
28
- * Narrow shape for the LocalStack CreateQueue / DeleteQueue paths we
29
- * touch from this test only. Keeps us off `any` per the repo's
30
- * no-`any`-in-tests rule.
31
- */
32
- interface SqsTestClient {
33
- send(cmd: unknown): Promise<{ QueueUrl?: string }>;
34
- destroy?: () => void;
35
- }
36
-
37
- interface SqsCommands {
38
- SQSClient: new (opts: { region: string; endpoint?: string }) => SqsTestClient;
39
- CreateQueueCommand: new (opts: { QueueName: string }) => unknown;
40
- DeleteQueueCommand: new (opts: { QueueUrl: string }) => unknown;
41
- }
42
-
43
- d("SQSAdapter — real LocalStack SQS", () => {
44
- let adapter: SQSAdapter;
45
- let testClient: SqsTestClient | null = null;
46
- let sqsCommands: SqsCommands | null = null;
47
- const createdQueues: string[] = [];
48
-
49
- // LocalStack accepts any credentials; we set explicit ones so the SDK
50
- // doesn't crawl the local environment / IMDS searching for them and
51
- // time out the first request.
52
- const TEST_REGION = process.env.AWS_REGION || "us-east-1";
53
-
54
- async function createQueue(name: string): Promise<string> {
55
- if (!testClient || !sqsCommands) throw new Error("test client not initialised — beforeAll didn't run");
56
- const result = await testClient.send(new sqsCommands.CreateQueueCommand({ QueueName: name }));
57
- const url = result.QueueUrl;
58
- if (!url) throw new Error(`CreateQueue returned no QueueUrl for ${name}`);
59
- createdQueues.push(url);
60
- return url;
61
- }
62
-
63
- beforeAll(async () => {
64
- // Force credentials BEFORE the SDK import — LocalStack rejects
65
- // requests without any signature, and the default credential chain
66
- // times out the first call when nothing is configured.
67
- process.env.AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID || "test";
68
- process.env.AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || "test";
69
- process.env.AWS_REGION = TEST_REGION;
70
-
71
- adapter = new SQSAdapter({
72
- region: TEST_REGION,
73
- endpoint: SQS_ENDPOINT,
74
- waitTimeSeconds: 1, // short long-poll so afterEach cleanup is responsive
75
- });
76
- await adapter.connect();
77
-
78
- // Direct SDK client for queue management — we only need
79
- // CreateQueue / DeleteQueue, the adapter doesn't expose those.
80
- const sdk = (await import("@aws-sdk/client-sqs")) as unknown as SqsCommands;
81
- sqsCommands = sdk;
82
- testClient = new sdk.SQSClient({ region: TEST_REGION, endpoint: SQS_ENDPOINT });
83
- }, TEST_TIMEOUT_MS);
84
-
85
- afterEach(async () => {
86
- // Tear down per-test queues so a flaky test doesn't poison the
87
- // next one with redelivered messages.
88
- for (const url of createdQueues.splice(0)) {
89
- if (!testClient || !sqsCommands) continue;
90
- try {
91
- await testClient.send(new sqsCommands.DeleteQueueCommand({ QueueUrl: url }));
92
- } catch {
93
- /* ignore — best-effort cleanup */
94
- }
95
- }
96
- });
97
-
98
- afterAll(async () => {
99
- await adapter.disconnect();
100
- testClient?.destroy?.();
101
- });
102
-
103
- it(
104
- "publishes a job and the consumer receives it (single-queue happy path)",
105
- async () => {
106
- const queueUrl = await createQueue(`blok-test-sqs-publish-${Math.random().toString(36).slice(2)}`);
107
- const received: WorkerJob[] = [];
108
-
109
- await adapter.process({ queue: queueUrl }, async (job) => {
110
- received.push(job);
111
- await job.complete();
112
- });
113
-
114
- const jobId = await adapter.addJob(queueUrl, { hello: "sqs", n: 1 });
115
- expect(typeof jobId).toBe("string");
116
-
117
- await waitFor(() => received.length === 1, TEST_TIMEOUT_MS - 5_000);
118
-
119
- expect(received[0].data).toEqual({ hello: "sqs", n: 1 });
120
- expect(received[0].queue).toBe(queueUrl);
121
-
122
- await adapter.stopProcessing(queueUrl);
123
- },
124
- TEST_TIMEOUT_MS,
125
- );
126
-
127
- it(
128
- "isolates jobs across distinct queues",
129
- async () => {
130
- const queueA = await createQueue(`blok-test-sqs-a-${Math.random().toString(36).slice(2)}`);
131
- const queueB = await createQueue(`blok-test-sqs-b-${Math.random().toString(36).slice(2)}`);
132
- const receivedA: WorkerJob[] = [];
133
- const receivedB: WorkerJob[] = [];
134
-
135
- await adapter.process({ queue: queueA }, async (job) => {
136
- receivedA.push(job);
137
- await job.complete();
138
- });
139
- await adapter.process({ queue: queueB }, async (job) => {
140
- receivedB.push(job);
141
- await job.complete();
142
- });
143
-
144
- await adapter.addJob(queueA, { from: "A" });
145
- await adapter.addJob(queueB, { from: "B" });
146
-
147
- await waitFor(() => receivedA.length === 1 && receivedB.length === 1, TEST_TIMEOUT_MS - 5_000);
148
-
149
- expect(receivedA[0].data).toEqual({ from: "A" });
150
- expect(receivedB[0].data).toEqual({ from: "B" });
151
-
152
- await adapter.stopProcessing(queueA);
153
- await adapter.stopProcessing(queueB);
154
- },
155
- TEST_TIMEOUT_MS,
156
- );
157
-
158
- it(
159
- "handler that calls fail() does NOT delete the message — visibility timeout returns it",
160
- async () => {
161
- // Direct exercise of the v0.5 settle-once fix in SQSAdapter:
162
- // before the fix, the wrapper auto-deleted after the handler
163
- // returned, so `fail()` was a no-op. After: `fail()` settles
164
- // the job and the wrapper skips delete; SQS's visibility
165
- // timeout (default 30s) eventually re-delivers the message.
166
- //
167
- // We don't WAIT for the redelivery here — that would hang for
168
- // 30s. We assert the message is still IN FLIGHT (active count
169
- // or ApproximateNumberOfMessagesNotVisible > 0) right after
170
- // fail() resolves. Or, more reliably: re-receive the same
171
- // message after a short visibility-timeout override via
172
- // SetQueueAttributes / ChangeMessageVisibility — but that adds
173
- // noise. The test below just asserts `fail()` doesn't throw
174
- // and the wrapper auto-delete path is suppressed.
175
- const queueUrl = await createQueue(`blok-test-sqs-fail-${Math.random().toString(36).slice(2)}`);
176
- let failCalls = 0;
177
- let acks = 0;
178
-
179
- await adapter.process(
180
- {
181
- queue: queueUrl,
182
- retries: 0,
183
- timeout: 60_000, // give SQS time before redelivery so we don't loop
184
- },
185
- async (job) => {
186
- if (failCalls === 0) {
187
- failCalls += 1;
188
- await job.fail(new Error("simulated failure"));
189
- return;
190
- }
191
- acks += 1;
192
- await job.complete();
193
- },
194
- );
195
-
196
- await adapter.addJob(queueUrl, { will_fail: true });
197
-
198
- await waitFor(() => failCalls === 1, TEST_TIMEOUT_MS - 5_000);
199
-
200
- // The wrapper must NOT have auto-deleted; if it did, `acks`
201
- // could stay 0 forever, but more importantly `failCalls` is
202
- // the correctness signal — fail() ran without throwing.
203
- expect(failCalls).toBe(1);
204
- // `acks` is 0 within our window because SQS's visibility
205
- // timeout hasn't elapsed yet; that's expected — we're only
206
- // verifying fail() doesn't crash and doesn't auto-ack.
207
- expect(acks).toBe(0);
208
-
209
- await adapter.stopProcessing(queueUrl);
210
- },
211
- TEST_TIMEOUT_MS,
212
- );
213
-
214
- it("isConnected + healthCheck reflect the live state", async () => {
215
- expect(adapter.isConnected()).toBe(true);
216
- const healthy = await adapter.healthCheck();
217
- expect(healthy).toBe(true);
218
- });
219
- });
220
-
221
- async function waitFor(predicate: () => boolean, timeoutMs: number): Promise<void> {
222
- const start = Date.now();
223
- while (Date.now() - start < timeoutMs) {
224
- if (predicate()) return;
225
- await new Promise((r) => setTimeout(r, 100));
226
- }
227
- throw new Error(`waitFor timed out after ${timeoutMs}ms`);
228
- }