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.
- package/README.md +106 -0
- package/index.d.ts +62 -0
- package/index.js +229 -0
- 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
|
+
}
|