@wokuapp/react-native 0.1.0 → 0.3.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 +103 -14
- package/dist/index.cjs +309 -93
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +96 -63
- package/dist/index.d.ts +96 -63
- package/dist/index.js +309 -93
- package/dist/index.js.map +1 -1
- package/package.json +27 -18
package/dist/index.js
CHANGED
|
@@ -4,7 +4,8 @@ var fetchHttpClient = {
|
|
|
4
4
|
const res = await fetch(req.url, {
|
|
5
5
|
method: req.method,
|
|
6
6
|
headers: req.headers,
|
|
7
|
-
body: req.body
|
|
7
|
+
body: req.body,
|
|
8
|
+
signal: req.signal
|
|
8
9
|
});
|
|
9
10
|
return {
|
|
10
11
|
status: res.status,
|
|
@@ -69,12 +70,16 @@ var WokuNetworkError = class extends WokuError {
|
|
|
69
70
|
};
|
|
70
71
|
|
|
71
72
|
// src/client.ts
|
|
72
|
-
var DEFAULT_CAPTURE_PATH = "/
|
|
73
|
+
var DEFAULT_CAPTURE_PATH = "/v1/captures";
|
|
73
74
|
var WokuClient = class {
|
|
74
75
|
constructor(config) {
|
|
75
76
|
this.config = config;
|
|
76
77
|
if (!config.apiUrl) throw new WokuConfigError("apiUrl is required");
|
|
77
78
|
if (!config.publicKey) throw new WokuConfigError("publicKey is required");
|
|
79
|
+
if (config.publicKey.startsWith("sk_"))
|
|
80
|
+
throw new WokuConfigError(
|
|
81
|
+
"Use a publishable key, never a management secret key, in a mobile app."
|
|
82
|
+
);
|
|
78
83
|
if (!config.companyId) throw new WokuConfigError("companyId is required");
|
|
79
84
|
this.http = config.http ?? fetchHttpClient;
|
|
80
85
|
this.logger = config.logger ?? noopLogger;
|
|
@@ -89,37 +94,85 @@ var WokuClient = class {
|
|
|
89
94
|
* non-2xx responses (so the caller can decide to re-queue).
|
|
90
95
|
*/
|
|
91
96
|
async send(submission) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
if (submission.companyId !== this.config.companyId)
|
|
98
|
+
throw new WokuConfigError(
|
|
99
|
+
"Cannot deliver a capture for another company."
|
|
100
|
+
);
|
|
101
|
+
const headers = {
|
|
102
|
+
"x-woku-key": this.config.publicKey,
|
|
103
|
+
"X-Woku-Company": this.config.companyId,
|
|
104
|
+
"X-Woku-Idempotency-Key": submission.id
|
|
105
|
+
};
|
|
106
|
+
let requestBody;
|
|
107
|
+
if (submission.audio) {
|
|
108
|
+
const form = new FormData();
|
|
109
|
+
form.append("file", {
|
|
110
|
+
uri: submission.audio.uri,
|
|
111
|
+
type: submission.audio.mimeType,
|
|
112
|
+
name: "capture-audio"
|
|
104
113
|
});
|
|
105
|
-
|
|
106
|
-
|
|
114
|
+
form.append("payload", JSON.stringify(submission));
|
|
115
|
+
requestBody = form;
|
|
116
|
+
} else {
|
|
117
|
+
headers["Content-Type"] = "application/json";
|
|
118
|
+
requestBody = JSON.stringify(submission);
|
|
107
119
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
120
|
+
const controller = new AbortController();
|
|
121
|
+
let timer;
|
|
122
|
+
try {
|
|
123
|
+
return await Promise.race([
|
|
124
|
+
(async () => {
|
|
125
|
+
const response = await this.http.request({
|
|
126
|
+
method: "POST",
|
|
127
|
+
url: this.endpoint,
|
|
128
|
+
headers,
|
|
129
|
+
body: requestBody,
|
|
130
|
+
signal: controller.signal
|
|
131
|
+
});
|
|
132
|
+
if (response.ok) {
|
|
133
|
+
const body2 = await this.safeJson(response.json);
|
|
134
|
+
return {
|
|
135
|
+
id: submission.id,
|
|
136
|
+
status: "sent",
|
|
137
|
+
remoteId: body2?.remoteId ?? body2?.id
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (response.status === 429) {
|
|
141
|
+
const raw = response.headers.get("Retry-After");
|
|
142
|
+
const retryAfter = raw === null ? void 0 : Number(raw);
|
|
143
|
+
throw new WokuQuarantineError(
|
|
144
|
+
"Submission blocked by quarantine",
|
|
145
|
+
retryAfter !== void 0 && Number.isFinite(retryAfter) ? Math.max(0, retryAfter) : void 0
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const body = await this.safeJson(response.json);
|
|
149
|
+
const value = body?.message;
|
|
150
|
+
const message = value && typeof value === "object" && !Array.isArray(value) ? value.message : value;
|
|
151
|
+
const detail = Array.isArray(message) ? message.filter((item) => typeof item === "string").join(", ") : message;
|
|
152
|
+
const error = typeof detail === "string" && detail ? detail : `HTTP ${response.status}`;
|
|
153
|
+
this.logger.warn(`woku capture rejected: ${error}`);
|
|
154
|
+
return {
|
|
155
|
+
id: submission.id,
|
|
156
|
+
status: "failed",
|
|
157
|
+
error,
|
|
158
|
+
retryable: response.status === 408 || response.status >= 500
|
|
159
|
+
};
|
|
160
|
+
})(),
|
|
161
|
+
new Promise((_resolve, reject) => {
|
|
162
|
+
timer = setTimeout(() => {
|
|
163
|
+
controller.abort();
|
|
164
|
+
reject(new WokuNetworkError("Capture request timed out"));
|
|
165
|
+
}, this.config.timeoutMs ?? 3e4);
|
|
166
|
+
})
|
|
167
|
+
]);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (err instanceof WokuQuarantineError) throw err;
|
|
170
|
+
throw new WokuNetworkError(
|
|
171
|
+
err instanceof Error ? err.message : "Capture request failed"
|
|
117
172
|
);
|
|
173
|
+
} finally {
|
|
174
|
+
if (timer) clearTimeout(timer);
|
|
118
175
|
}
|
|
119
|
-
const body = await this.safeJson(response.json);
|
|
120
|
-
const error = body?.message ?? `HTTP ${response.status}`;
|
|
121
|
-
this.logger.warn(`woku capture rejected: ${error}`);
|
|
122
|
-
return { id: submission.id, status: "failed", error };
|
|
123
176
|
}
|
|
124
177
|
async safeJson(json) {
|
|
125
178
|
try {
|
|
@@ -132,94 +185,193 @@ var WokuClient = class {
|
|
|
132
185
|
|
|
133
186
|
// src/queue.ts
|
|
134
187
|
var DEFAULT_KEY = "woku.sdk.queue.v1";
|
|
135
|
-
var
|
|
188
|
+
var mutations = /* @__PURE__ */ new WeakMap();
|
|
189
|
+
var flushes = /* @__PURE__ */ new WeakMap();
|
|
136
190
|
var OfflineQueue = class {
|
|
137
191
|
constructor(opts = {}) {
|
|
138
|
-
this.items = null;
|
|
139
192
|
this.storage = opts.storage ?? new InMemoryStorage();
|
|
140
193
|
this.key = opts.storageKey ?? DEFAULT_KEY;
|
|
141
|
-
this.maxAttempts = opts.maxAttempts ??
|
|
194
|
+
this.maxAttempts = opts.maxAttempts ?? 8;
|
|
195
|
+
this.companyId = opts.companyId;
|
|
196
|
+
}
|
|
197
|
+
owned(item) {
|
|
198
|
+
return !this.companyId || item.submission.companyId === this.companyId;
|
|
142
199
|
}
|
|
143
200
|
async load() {
|
|
144
|
-
if (this.items) return this.items;
|
|
145
201
|
const raw = await this.storage.getItem(this.key);
|
|
146
|
-
if (!raw)
|
|
147
|
-
this.items = [];
|
|
148
|
-
return this.items;
|
|
149
|
-
}
|
|
202
|
+
if (!raw) return [];
|
|
150
203
|
try {
|
|
151
204
|
const parsed = JSON.parse(raw);
|
|
152
|
-
|
|
205
|
+
if (Array.isArray(parsed) && parsed.every(
|
|
206
|
+
(item) => item && typeof item === "object" && item.submission && typeof item.submission.id === "string" && typeof item.submission.companyId === "string" && Number.isFinite(item.submission.createdAt) && Number.isInteger(item.attempts) && item.attempts >= 0
|
|
207
|
+
))
|
|
208
|
+
return parsed;
|
|
153
209
|
} catch {
|
|
154
|
-
this.items = [];
|
|
155
210
|
}
|
|
156
|
-
|
|
211
|
+
throw new WokuConfigError(
|
|
212
|
+
"The capture queue contains invalid data; repair it before writing."
|
|
213
|
+
);
|
|
157
214
|
}
|
|
158
|
-
async persist() {
|
|
159
|
-
await this.storage.setItem(this.key, JSON.stringify(
|
|
215
|
+
async persist(items) {
|
|
216
|
+
await this.storage.setItem(this.key, JSON.stringify(items));
|
|
217
|
+
}
|
|
218
|
+
mutation(run) {
|
|
219
|
+
let locks = mutations.get(this.storage);
|
|
220
|
+
if (!locks) {
|
|
221
|
+
locks = /* @__PURE__ */ new Map();
|
|
222
|
+
mutations.set(this.storage, locks);
|
|
223
|
+
}
|
|
224
|
+
const previous = locks.get(this.key) ?? Promise.resolve();
|
|
225
|
+
const result = previous.then(run, run);
|
|
226
|
+
const settled = result.then(
|
|
227
|
+
() => void 0,
|
|
228
|
+
() => void 0
|
|
229
|
+
);
|
|
230
|
+
locks.set(this.key, settled);
|
|
231
|
+
void settled.then(() => {
|
|
232
|
+
if (locks?.get(this.key) === settled) locks.delete(this.key);
|
|
233
|
+
});
|
|
234
|
+
return result;
|
|
160
235
|
}
|
|
161
|
-
/** Adds a submission to the tail of the queue (dedup by id). */
|
|
162
236
|
async enqueue(submission) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
237
|
+
if (this.companyId && submission.companyId !== this.companyId)
|
|
238
|
+
throw new WokuConfigError(
|
|
239
|
+
"Cannot enqueue a capture from another company."
|
|
240
|
+
);
|
|
241
|
+
const snapshot = JSON.parse(
|
|
242
|
+
JSON.stringify(submission)
|
|
243
|
+
);
|
|
244
|
+
return this.mutation(async () => {
|
|
245
|
+
const items = await this.load();
|
|
246
|
+
if (items.some(
|
|
247
|
+
(item) => item.submission.id === snapshot.id && item.submission.companyId === snapshot.companyId
|
|
248
|
+
))
|
|
249
|
+
return;
|
|
250
|
+
items.push({ submission: snapshot, attempts: 0 });
|
|
251
|
+
await this.persist(items);
|
|
252
|
+
});
|
|
167
253
|
}
|
|
168
|
-
|
|
169
|
-
return
|
|
254
|
+
size() {
|
|
255
|
+
return this.mutation(
|
|
256
|
+
async () => (await this.load()).filter((item) => this.owned(item) && !item.failure).length
|
|
257
|
+
);
|
|
170
258
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
259
|
+
pending() {
|
|
260
|
+
return this.mutation(
|
|
261
|
+
async () => (await this.load()).filter((item) => this.owned(item) && !item.failure).map((item) => item.submission)
|
|
262
|
+
);
|
|
174
263
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
264
|
+
failures() {
|
|
265
|
+
return this.mutation(
|
|
266
|
+
async () => (await this.load()).filter((item) => this.owned(item) && item.failure).map((item) => ({
|
|
267
|
+
submission: item.submission,
|
|
268
|
+
reason: item.failure
|
|
269
|
+
}))
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
acknowledge(submission) {
|
|
273
|
+
return this.mutation(async () => {
|
|
274
|
+
const items = await this.load();
|
|
275
|
+
await this.persist(
|
|
276
|
+
items.filter(
|
|
277
|
+
(item) => !(item.submission.id === submission.id && item.submission.companyId === submission.companyId && this.owned(item))
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
reject(submission, reason) {
|
|
283
|
+
return this.mutation(async () => {
|
|
284
|
+
const items = await this.load();
|
|
285
|
+
const item = items.find(
|
|
286
|
+
(row) => row.submission.id === submission.id && row.submission.companyId === submission.companyId && this.owned(row)
|
|
287
|
+
);
|
|
288
|
+
if (item) {
|
|
289
|
+
item.failure = reason;
|
|
290
|
+
await this.persist(items);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
flush(send) {
|
|
295
|
+
let jobs = flushes.get(this.storage);
|
|
296
|
+
if (!jobs) {
|
|
297
|
+
jobs = /* @__PURE__ */ new Map();
|
|
298
|
+
flushes.set(this.storage, jobs);
|
|
299
|
+
}
|
|
300
|
+
const scope = JSON.stringify([this.key, this.companyId ?? "*"]);
|
|
301
|
+
const existing = jobs.get(scope);
|
|
302
|
+
if (existing) return existing;
|
|
303
|
+
const result = this.runFlush(send).finally(() => {
|
|
304
|
+
if (jobs?.get(scope) === result) jobs.delete(scope);
|
|
305
|
+
});
|
|
306
|
+
jobs.set(scope, result);
|
|
307
|
+
return result;
|
|
308
|
+
}
|
|
309
|
+
async runFlush(send) {
|
|
310
|
+
const batch = await this.pending();
|
|
311
|
+
let sent = 0, failed = 0, quarantined = false;
|
|
312
|
+
for (const candidate of batch) {
|
|
313
|
+
const current = await this.mutation(
|
|
314
|
+
async () => (await this.load()).find(
|
|
315
|
+
(item) => this.owned(item) && !item.failure && item.submission.id === candidate.id && item.submission.companyId === candidate.companyId
|
|
316
|
+
)
|
|
317
|
+
);
|
|
318
|
+
if (!current) continue;
|
|
319
|
+
const submission = current.submission;
|
|
320
|
+
if (submission.createdAt > 0 && Date.now() - submission.createdAt >= 24 * 60 * 60 * 1e3) {
|
|
321
|
+
await this.reject(
|
|
322
|
+
submission,
|
|
323
|
+
"Retry window expired; inspect delivery before resubmitting."
|
|
324
|
+
);
|
|
325
|
+
failed++;
|
|
190
326
|
continue;
|
|
191
327
|
}
|
|
328
|
+
let permanent = false;
|
|
329
|
+
let reason;
|
|
330
|
+
let accepted = false;
|
|
192
331
|
try {
|
|
193
|
-
const result = await send(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
else survivors.push(item);
|
|
200
|
-
}
|
|
201
|
-
} catch (err) {
|
|
202
|
-
if (err instanceof WokuQuarantineError) {
|
|
332
|
+
const result = await send(submission);
|
|
333
|
+
accepted = result.status === "sent";
|
|
334
|
+
reason = result.error;
|
|
335
|
+
permanent = result.retryable === false;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
if (error instanceof WokuQuarantineError) {
|
|
203
338
|
quarantined = true;
|
|
204
|
-
|
|
205
|
-
} else if (err instanceof WokuNetworkError) {
|
|
206
|
-
item.attempts++;
|
|
207
|
-
survivors.push(item);
|
|
208
|
-
} else {
|
|
209
|
-
item.attempts++;
|
|
210
|
-
if (item.attempts >= this.maxAttempts) failed++;
|
|
211
|
-
else survivors.push(item);
|
|
339
|
+
break;
|
|
212
340
|
}
|
|
341
|
+
reason = error instanceof Error ? error.message : "Delivery failed";
|
|
213
342
|
}
|
|
343
|
+
const outcome = await this.mutation(async () => {
|
|
344
|
+
const items = await this.load();
|
|
345
|
+
const index = items.findIndex(
|
|
346
|
+
(item) => item.submission.id === submission.id && item.submission.companyId === submission.companyId && !item.failure
|
|
347
|
+
);
|
|
348
|
+
if (index < 0) return "removed";
|
|
349
|
+
if (accepted) items.splice(index, 1);
|
|
350
|
+
else {
|
|
351
|
+
const item = items[index];
|
|
352
|
+
item.attempts++;
|
|
353
|
+
if (permanent || item.attempts >= this.maxAttempts)
|
|
354
|
+
item.failure = reason ?? "Attempt budget exhausted";
|
|
355
|
+
}
|
|
356
|
+
const exhausted = !accepted && Boolean(items[index]?.failure);
|
|
357
|
+
await this.persist(items);
|
|
358
|
+
return accepted ? "sent" : exhausted ? "failed" : "pending";
|
|
359
|
+
});
|
|
360
|
+
if (outcome === "sent") sent++;
|
|
361
|
+
if (outcome === "failed") failed++;
|
|
214
362
|
}
|
|
215
|
-
this.
|
|
216
|
-
await this.persist();
|
|
217
|
-
return { sent, failed, remaining: survivors.length, quarantined };
|
|
363
|
+
return { sent, failed, remaining: await this.size(), quarantined };
|
|
218
364
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
365
|
+
clear() {
|
|
366
|
+
return this.mutation(async () => {
|
|
367
|
+
if (!this.companyId) {
|
|
368
|
+
await this.storage.removeItem(this.key);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const remaining = (await this.load()).filter((item) => !this.owned(item));
|
|
372
|
+
if (remaining.length) await this.persist(remaining);
|
|
373
|
+
else await this.storage.removeItem(this.key);
|
|
374
|
+
});
|
|
223
375
|
}
|
|
224
376
|
};
|
|
225
377
|
|
|
@@ -241,10 +393,12 @@ var WokuSdk = class {
|
|
|
241
393
|
this.client = new WokuClient(config);
|
|
242
394
|
this.queue = new OfflineQueue({
|
|
243
395
|
storage: config.storage,
|
|
396
|
+
companyId: config.companyId,
|
|
244
397
|
maxAttempts: config.maxQueueAttempts
|
|
245
398
|
});
|
|
246
399
|
this.logger = config.logger ?? noopLogger;
|
|
247
400
|
this.companyId = config.companyId;
|
|
401
|
+
this.language = config.language ?? "es";
|
|
248
402
|
}
|
|
249
403
|
/** Captures a Woku rating (1..5) with optional text/audio comment. */
|
|
250
404
|
async captureWoku(input) {
|
|
@@ -259,6 +413,10 @@ var WokuSdk = class {
|
|
|
259
413
|
audio: input.audio,
|
|
260
414
|
respondent: input.respondent,
|
|
261
415
|
metadata: input.metadata,
|
|
416
|
+
dispatchToken: input.dispatchToken,
|
|
417
|
+
collectionKind: input.collectionKind ?? "standalone",
|
|
418
|
+
flowId: input.flowId,
|
|
419
|
+
language: input.language ?? (input.audio ? this.language : void 0),
|
|
262
420
|
createdAt: Date.now()
|
|
263
421
|
};
|
|
264
422
|
return this.deliver(submission);
|
|
@@ -276,10 +434,48 @@ var WokuSdk = class {
|
|
|
276
434
|
audio: input.audio,
|
|
277
435
|
respondent: input.respondent,
|
|
278
436
|
metadata: input.metadata,
|
|
437
|
+
dispatchToken: input.dispatchToken,
|
|
438
|
+
collectionKind: input.collectionKind ?? "standalone",
|
|
439
|
+
flowId: input.flowId,
|
|
440
|
+
language: input.language ?? (input.audio ? this.language : void 0),
|
|
279
441
|
createdAt: Date.now()
|
|
280
442
|
};
|
|
281
443
|
return this.deliver(submission);
|
|
282
444
|
}
|
|
445
|
+
async captureCsat(input) {
|
|
446
|
+
assertInt(input.score, 1, 5, "score");
|
|
447
|
+
return this.deliver({
|
|
448
|
+
id: generateId(),
|
|
449
|
+
kind: "csat",
|
|
450
|
+
companyId: this.companyId,
|
|
451
|
+
targetId: input.csatId,
|
|
452
|
+
score: input.score,
|
|
453
|
+
comment: input.comment,
|
|
454
|
+
respondent: input.respondent,
|
|
455
|
+
metadata: input.metadata,
|
|
456
|
+
dispatchToken: input.dispatchToken,
|
|
457
|
+
collectionKind: input.collectionKind ?? "standalone",
|
|
458
|
+
flowId: input.flowId,
|
|
459
|
+
createdAt: Date.now()
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async captureCes(input) {
|
|
463
|
+
assertInt(input.score, 1, 5, "score");
|
|
464
|
+
return this.deliver({
|
|
465
|
+
id: generateId(),
|
|
466
|
+
kind: "ces",
|
|
467
|
+
companyId: this.companyId,
|
|
468
|
+
targetId: input.cesId,
|
|
469
|
+
score: input.score,
|
|
470
|
+
comment: input.comment,
|
|
471
|
+
respondent: input.respondent,
|
|
472
|
+
metadata: input.metadata,
|
|
473
|
+
dispatchToken: input.dispatchToken,
|
|
474
|
+
collectionKind: input.collectionKind ?? "standalone",
|
|
475
|
+
flowId: input.flowId,
|
|
476
|
+
createdAt: Date.now()
|
|
477
|
+
});
|
|
478
|
+
}
|
|
283
479
|
/** Retries every queued submission. Safe to call on app foreground or
|
|
284
480
|
* when connectivity is regained. */
|
|
285
481
|
flush() {
|
|
@@ -289,6 +485,10 @@ var WokuSdk = class {
|
|
|
289
485
|
pendingCount() {
|
|
290
486
|
return this.queue.size();
|
|
291
487
|
}
|
|
488
|
+
/** Inspect retained captures whose automatic attempt budget was exhausted. */
|
|
489
|
+
failedCaptures() {
|
|
490
|
+
return this.queue.failures();
|
|
491
|
+
}
|
|
292
492
|
/** Clears the offline queue (e.g. on user logout). */
|
|
293
493
|
clearQueue() {
|
|
294
494
|
return this.queue.clear();
|
|
@@ -298,9 +498,25 @@ var WokuSdk = class {
|
|
|
298
498
|
* submission is queued for a later flush rather than thrown away.
|
|
299
499
|
*/
|
|
300
500
|
async deliver(submission) {
|
|
501
|
+
submission = JSON.parse(JSON.stringify(submission));
|
|
502
|
+
await this.queue.enqueue(submission);
|
|
301
503
|
try {
|
|
302
504
|
const result = await this.client.send(submission);
|
|
303
|
-
if (result.status === "sent")
|
|
505
|
+
if (result.status === "sent") {
|
|
506
|
+
try {
|
|
507
|
+
await this.queue.acknowledge(submission);
|
|
508
|
+
} catch {
|
|
509
|
+
this.logger.warn(
|
|
510
|
+
"Capture accepted; local acknowledgement is pending",
|
|
511
|
+
{ id: submission.id }
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
if (result.retryable === false) {
|
|
517
|
+
await this.queue.reject(submission, result.error ?? "Capture rejected");
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
304
520
|
await this.queue.enqueue(submission);
|
|
305
521
|
return { ...result, status: "queued" };
|
|
306
522
|
} catch (err) {
|