procedere-mq-sdk 0.2.0

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 (4) hide show
  1. package/README.md +106 -0
  2. package/index.d.ts +62 -0
  3. package/index.js +229 -0
  4. package/package.json +48 -0
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # procedere-mq-sdk
2
+
3
+ SDK oficial em Node.js para consumir a API HTTP do ProcedereMQ.
4
+
5
+ ## Requisitos
6
+
7
+ - Node.js 18 ou superior
8
+
9
+ ## Instalacao
10
+
11
+ ```bash
12
+ npm install procedere-mq-sdk
13
+ ```
14
+
15
+ ## Uso
16
+
17
+ ### Producer
18
+
19
+ ```js
20
+ const { LiteMQClient } = require("procedere-mq-sdk");
21
+
22
+ async function main() {
23
+ const client = new LiteMQClient("http://127.0.0.1:65090");
24
+ const producer = client.producer("emails");
25
+
26
+ const job = await producer.publish(
27
+ { to: "alice@example.com", template: "welcome" },
28
+ { maxRetry: 3 }
29
+ );
30
+
31
+ console.log("job criado:", job.id);
32
+ }
33
+
34
+ main().catch(console.error);
35
+ ```
36
+
37
+ ### Consumer
38
+
39
+ ```js
40
+ const { LiteMQClient, QueueEmptyError } = require("procedere-mq-sdk");
41
+
42
+ async function main() {
43
+ const client = new LiteMQClient("http://127.0.0.1:65090");
44
+ const consumer = client.consumer("emails");
45
+
46
+ try {
47
+ const job = await consumer.receive();
48
+ await consumer.ack(job);
49
+ } catch (error) {
50
+ if (error instanceof QueueEmptyError) {
51
+ return;
52
+ }
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ main().catch(console.error);
58
+ ```
59
+
60
+ ## API
61
+
62
+ ### `new LiteMQClient(baseUrl, options?)`
63
+
64
+ - `baseUrl`: endereco HTTP do ProcedereMQ
65
+ - `options.timeout`: timeout em milissegundos
66
+ - `options.fetchImpl`: implementacao customizada de `fetch`
67
+
68
+ ### `client.producer(queue)`
69
+
70
+ - Retorna um `Producer` vinculado a uma fila
71
+
72
+ ### `client.consumer(queue)`
73
+
74
+ - Retorna um `Consumer` vinculado a uma fila
75
+
76
+ ### `producer.publish(payload?, options?)`
77
+
78
+ - `options.jobId`: id customizado do job
79
+ - `options.maxRetry`: quantidade maxima de retries
80
+ - `options.runAt`: `Date`, string ISO ou timestamp
81
+
82
+ ### `consumer.receive()`
83
+
84
+ - Faz `dequeue` da fila configurada
85
+
86
+ ### `consumer.ack(jobOrId)`
87
+
88
+ - Confirma um job por objeto ou id
89
+
90
+ ### `consumer.fail(jobOrId)`
91
+
92
+ - Marca falha de um job por objeto ou id
93
+
94
+ ## Erros
95
+
96
+ - `LiteMQError`
97
+ - `QueueEmptyError`
98
+ - `NotFoundError`
99
+
100
+ ## Publicacao
101
+
102
+ ```bash
103
+ npm test
104
+ npm pack --dry-run
105
+ npm publish --access public
106
+ ```
package/index.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ export class LiteMQError extends Error {}
2
+
3
+ export class QueueEmptyError extends LiteMQError {}
4
+
5
+ export class NotFoundError extends LiteMQError {}
6
+
7
+ export interface LiteMQClientOptions {
8
+ timeout?: number;
9
+ fetchImpl?: typeof fetch;
10
+ }
11
+
12
+ export interface PublishOptions {
13
+ jobId?: string;
14
+ maxRetry?: number;
15
+ runAt?: Date | string | number;
16
+ }
17
+
18
+ export interface JobRecord {
19
+ id?: string;
20
+ queue?: string;
21
+ payload?: unknown;
22
+ status?: string;
23
+ last_error?: string;
24
+ attempts?: number;
25
+ max_retry?: number;
26
+ run_at?: string;
27
+ locked_at?: string;
28
+ created_at?: string;
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ export class Producer {
33
+ constructor(client: LiteMQClient, queue: string);
34
+ client: LiteMQClient;
35
+ queue: string;
36
+ publish(payload?: unknown, options?: PublishOptions): Promise<Record<string, unknown>>;
37
+ }
38
+
39
+ export class Consumer {
40
+ constructor(client: LiteMQClient, queue: string);
41
+ client: LiteMQClient;
42
+ queue: string;
43
+ receive(): Promise<Record<string, unknown>>;
44
+ ack(jobOrId: string | JobRecord): Promise<Record<string, unknown>>;
45
+ fail(jobOrId: string | JobRecord): Promise<Record<string, unknown>>;
46
+ }
47
+
48
+ export class LiteMQClient {
49
+ constructor(baseUrl: string, options?: LiteMQClientOptions);
50
+ baseUrl: string;
51
+ timeout: number;
52
+ producer(queue: string): Producer;
53
+ consumer(queue: string): Consumer;
54
+ enqueue(
55
+ queue: string,
56
+ payload?: unknown,
57
+ options?: PublishOptions
58
+ ): Promise<Record<string, unknown>>;
59
+ dequeue(queue: string): Promise<Record<string, unknown>>;
60
+ ack(jobId: string): Promise<Record<string, unknown>>;
61
+ fail(jobId: string): Promise<Record<string, unknown>>;
62
+ }
package/index.js ADDED
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+
3
+ class LiteMQError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "LiteMQError";
7
+ }
8
+ }
9
+
10
+ class QueueEmptyError extends LiteMQError {
11
+ constructor(message = "queue empty") {
12
+ super(message);
13
+ this.name = "QueueEmptyError";
14
+ }
15
+ }
16
+
17
+ class NotFoundError extends LiteMQError {
18
+ constructor(message = "not found") {
19
+ super(message);
20
+ this.name = "NotFoundError";
21
+ }
22
+ }
23
+
24
+ class Producer {
25
+ constructor(client, queue) {
26
+ this.client = client;
27
+ this.queue = normalizeRequiredString(queue, "queue");
28
+ }
29
+
30
+ async publish(payload = null, options = {}) {
31
+ const { jobId, maxRetry, runAt } = options;
32
+ return this.client.enqueue(this.queue, payload, {
33
+ jobId,
34
+ maxRetry,
35
+ runAt,
36
+ });
37
+ }
38
+ }
39
+
40
+ class Consumer {
41
+ constructor(client, queue) {
42
+ this.client = client;
43
+ this.queue = normalizeRequiredString(queue, "queue");
44
+ }
45
+
46
+ async receive() {
47
+ return this.client.dequeue(this.queue);
48
+ }
49
+
50
+ async ack(jobOrId) {
51
+ return this.client.ack(extractJobId(jobOrId));
52
+ }
53
+
54
+ async fail(jobOrId) {
55
+ return this.client.fail(extractJobId(jobOrId));
56
+ }
57
+ }
58
+
59
+ class LiteMQClient {
60
+ constructor(baseUrl, options = {}) {
61
+ this.baseUrl = normalizeBaseUrl(baseUrl);
62
+ this.timeout = options.timeout ?? 10000;
63
+ this.fetchImpl = options.fetchImpl ?? globalThis.fetch;
64
+
65
+ if (typeof this.fetchImpl !== "function") {
66
+ throw new LiteMQError("fetch API is not available in this runtime");
67
+ }
68
+ }
69
+
70
+ producer(queue) {
71
+ return new Producer(this, queue);
72
+ }
73
+
74
+ consumer(queue) {
75
+ return new Consumer(this, queue);
76
+ }
77
+
78
+ async enqueue(queue, payload = null, options = {}) {
79
+ const normalizedQueue = normalizeRequiredString(queue, "queue");
80
+ const body = { queue: normalizedQueue };
81
+
82
+ if (payload !== null) {
83
+ body.payload = payload;
84
+ }
85
+ if (isNonEmptyString(options.jobId)) {
86
+ body.id = options.jobId.trim();
87
+ }
88
+ if (options.maxRetry !== undefined) {
89
+ body.max_retry = options.maxRetry;
90
+ }
91
+ if (options.runAt !== undefined && options.runAt !== null) {
92
+ body.run_at = formatDateTime(options.runAt);
93
+ }
94
+
95
+ return this._request("POST", "/enqueue", 201, { body });
96
+ }
97
+
98
+ async dequeue(queue) {
99
+ const normalizedQueue = normalizeRequiredString(queue, "queue");
100
+ const params = new URLSearchParams({ queue: normalizedQueue });
101
+ return this._request("GET", `/dequeue?${params.toString()}`, 200);
102
+ }
103
+
104
+ async ack(jobId) {
105
+ const normalizedJobId = normalizeRequiredString(jobId, "job_id");
106
+ return this._request("POST", "/ack", 200, {
107
+ body: { id: normalizedJobId },
108
+ });
109
+ }
110
+
111
+ async fail(jobId) {
112
+ const normalizedJobId = normalizeRequiredString(jobId, "job_id");
113
+ return this._request("POST", "/fail", 200, {
114
+ body: { id: normalizedJobId },
115
+ });
116
+ }
117
+
118
+ async _request(method, path, expectedStatus, options = {}) {
119
+ const controller = new AbortController();
120
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
121
+ const init = {
122
+ method,
123
+ signal: controller.signal,
124
+ headers: {},
125
+ };
126
+
127
+ if (options.body !== undefined) {
128
+ init.headers["Content-Type"] = "application/json";
129
+ init.body = JSON.stringify(options.body);
130
+ }
131
+
132
+ let response;
133
+ try {
134
+ response = await this.fetchImpl(`${this.baseUrl}${path}`, init);
135
+ } catch (error) {
136
+ if (error && error.name === "AbortError") {
137
+ throw new LiteMQError(`request timed out after ${this.timeout}ms`);
138
+ }
139
+ throw error;
140
+ } finally {
141
+ clearTimeout(timeoutId);
142
+ }
143
+
144
+ if (response.status === 204) {
145
+ throw new QueueEmptyError("queue empty");
146
+ }
147
+ if (response.status === 404) {
148
+ throw new NotFoundError(await extractError(response));
149
+ }
150
+ if (response.status !== expectedStatus) {
151
+ throw new LiteMQError(await extractError(response));
152
+ }
153
+
154
+ const text = await response.text();
155
+ if (!text) {
156
+ return {};
157
+ }
158
+
159
+ const data = JSON.parse(text);
160
+ if (isPlainObject(data)) {
161
+ return data;
162
+ }
163
+ return { data };
164
+ }
165
+ }
166
+
167
+ async function extractError(response) {
168
+ const text = await response.text();
169
+ if (!text) {
170
+ return response.statusText || `HTTP ${response.status}`;
171
+ }
172
+
173
+ try {
174
+ const payload = JSON.parse(text);
175
+ if (isPlainObject(payload) && isNonEmptyString(payload.error)) {
176
+ return payload.error.trim();
177
+ }
178
+ } catch (_) {
179
+ return text;
180
+ }
181
+
182
+ return text;
183
+ }
184
+
185
+ function normalizeBaseUrl(baseUrl) {
186
+ return normalizeRequiredString(baseUrl, "base_url").replace(/\/+$/, "");
187
+ }
188
+
189
+ function normalizeRequiredString(value, fieldName) {
190
+ if (!isNonEmptyString(value)) {
191
+ throw new TypeError(`${fieldName} is required`);
192
+ }
193
+ return value.trim();
194
+ }
195
+
196
+ function isNonEmptyString(value) {
197
+ return typeof value === "string" && value.trim() !== "";
198
+ }
199
+
200
+ function formatDateTime(value) {
201
+ const date = value instanceof Date ? value : new Date(value);
202
+ if (Number.isNaN(date.getTime())) {
203
+ throw new TypeError("runAt must be a valid Date");
204
+ }
205
+ return date.toISOString();
206
+ }
207
+
208
+ function extractJobId(jobOrId) {
209
+ if (isNonEmptyString(jobOrId)) {
210
+ return jobOrId.trim();
211
+ }
212
+ if (isPlainObject(jobOrId) && isNonEmptyString(jobOrId.id)) {
213
+ return jobOrId.id.trim();
214
+ }
215
+ throw new TypeError("job id is required");
216
+ }
217
+
218
+ function isPlainObject(value) {
219
+ return value !== null && typeof value === "object" && !Array.isArray(value);
220
+ }
221
+
222
+ module.exports = {
223
+ Consumer,
224
+ LiteMQClient,
225
+ LiteMQError,
226
+ NotFoundError,
227
+ Producer,
228
+ QueueEmptyError,
229
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "procedere-mq-sdk",
3
+ "version": "0.2.0",
4
+ "description": "Node.js SDK para ProcedereMQ",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "type": "commonjs",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "require": "./index.js",
12
+ "default": "./index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "index.js",
17
+ "index.d.ts",
18
+ "README.md"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "scripts": {
24
+ "test": "node --test",
25
+ "prepublishOnly": "npm test"
26
+ },
27
+ "keywords": [
28
+ "queue",
29
+ "mq",
30
+ "sdk",
31
+ "nodejs",
32
+ "procedere"
33
+ ],
34
+ "author": "Jefferson Tadeu Leite",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/jeffersontadeuleite/procedereMQ.git",
39
+ "directory": "node_sdk"
40
+ },
41
+ "homepage": "https://github.com/jeffersontadeuleite/procedereMQ",
42
+ "bugs": {
43
+ "url": "https://github.com/jeffersontadeuleite/procedereMQ/issues"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }