@teincfood/core 0.9.15 → 0.9.16
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/dist/adapters/sync-failure.d.ts +33 -0
- package/dist/adapters/sync-failure.js +23 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/sync/command-queue.service.d.ts +2 -0
- package/dist/sync/command-queue.service.js +48 -30
- package/dist/sync/sync-jobs.service.js +59 -48
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync-failure reporter adapter.
|
|
3
|
+
*
|
|
4
|
+
* Core never depends on the observability SDK. The host app registers a
|
|
5
|
+
* reporter at boot (built on `@teincfood/observability`) and core forwards
|
|
6
|
+
* every failed command attempt so operators can see which device/app/user is
|
|
7
|
+
* struggling and go fix it.
|
|
8
|
+
*/
|
|
9
|
+
export type SyncFailurePhase = "outbox" | "sync_jobs";
|
|
10
|
+
export interface SyncFailureReport {
|
|
11
|
+
/** Which queue produced the failure. */
|
|
12
|
+
phase: SyncFailurePhase;
|
|
13
|
+
business_id: string;
|
|
14
|
+
user_id?: string;
|
|
15
|
+
/** Stable per-install device id (matches the telemetry device). */
|
|
16
|
+
device_id?: string;
|
|
17
|
+
command_type: string;
|
|
18
|
+
command_id?: string;
|
|
19
|
+
entity_id?: string;
|
|
20
|
+
/** HTTP status when the failure came from a response. */
|
|
21
|
+
status?: number;
|
|
22
|
+
/** Backend/guard error code. */
|
|
23
|
+
code?: string;
|
|
24
|
+
message?: string;
|
|
25
|
+
attempts?: number;
|
|
26
|
+
/** True when the command was dead-lettered (no further auto-retry). */
|
|
27
|
+
terminal: boolean;
|
|
28
|
+
transport?: string;
|
|
29
|
+
}
|
|
30
|
+
export type SyncFailureReporter = (report: SyncFailureReport) => void;
|
|
31
|
+
export declare function setSyncFailureReporter(next: SyncFailureReporter | null): void;
|
|
32
|
+
/** Fire-and-forget; never throws, safe before a reporter is registered. */
|
|
33
|
+
export declare function reportSyncFailure(report: SyncFailureReport): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync-failure reporter adapter.
|
|
3
|
+
*
|
|
4
|
+
* Core never depends on the observability SDK. The host app registers a
|
|
5
|
+
* reporter at boot (built on `@teincfood/observability`) and core forwards
|
|
6
|
+
* every failed command attempt so operators can see which device/app/user is
|
|
7
|
+
* struggling and go fix it.
|
|
8
|
+
*/
|
|
9
|
+
let reporter = null;
|
|
10
|
+
export function setSyncFailureReporter(next) {
|
|
11
|
+
reporter = next;
|
|
12
|
+
}
|
|
13
|
+
/** Fire-and-forget; never throws, safe before a reporter is registered. */
|
|
14
|
+
export function reportSyncFailure(report) {
|
|
15
|
+
if (!reporter)
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
reporter(report);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
/* telemetry must never break sync */
|
|
22
|
+
}
|
|
23
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export * from "./utils/customer-display";
|
|
|
29
29
|
export * from "./utils/order-filter";
|
|
30
30
|
export { useReferenceData } from "./reference/hooks";
|
|
31
31
|
export { setNotifyHandlers } from "./adapters/notify";
|
|
32
|
+
export { setSyncFailureReporter, reportSyncFailure, type SyncFailureReport, type SyncFailureReporter, type SyncFailurePhase, } from "./adapters/sync-failure";
|
|
32
33
|
export { walletService, riderService, fleetService, deliveryService, dashboardService, analyticsService, placesService } from "./adapters/services";
|
|
33
34
|
export { buildMenuItemEnvelope, buildMenuCategoryEnvelope } from "./reference/menu-record";
|
|
34
35
|
export { validateMenuItemInput, validateMenuCategoryInput, validateOrderCreateInput, validateInviteInput } from "./validation/offline";
|
package/dist/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export * from "./utils/customer-display";
|
|
|
30
30
|
export * from "./utils/order-filter";
|
|
31
31
|
export { useReferenceData } from "./reference/hooks";
|
|
32
32
|
export { setNotifyHandlers } from "./adapters/notify";
|
|
33
|
+
export { setSyncFailureReporter, reportSyncFailure, } from "./adapters/sync-failure";
|
|
33
34
|
export { walletService, riderService, fleetService, deliveryService, dashboardService, analyticsService, placesService } from "./adapters/services";
|
|
34
35
|
export { buildMenuItemEnvelope, buildMenuCategoryEnvelope } from "./reference/menu-record";
|
|
35
36
|
export { validateMenuItemInput, validateMenuCategoryInput, validateOrderCreateInput, validateInviteInput } from "./validation/offline";
|
|
@@ -24,6 +24,8 @@ declare class CommandQueueService {
|
|
|
24
24
|
* Try to execute a single pending command through the current transport.
|
|
25
25
|
*/
|
|
26
26
|
processCommand(command: PersistedCommand): Promise<void>;
|
|
27
|
+
/** Forward a failed attempt to the host's observability reporter. */
|
|
28
|
+
private reportFailure;
|
|
27
29
|
/**
|
|
28
30
|
* Mark a command permanently failed: loud toast + optimistic revert, then
|
|
29
31
|
* replay continues to the next command. Shared by the exhausted-retry and
|
|
@@ -21,7 +21,8 @@ import { generateLocalCalloutNumber, generateOrderNumberForBusiness, generateOrd
|
|
|
21
21
|
import { getConnectivityState, subscribeConnectivity } from "../adapters/connectivity";
|
|
22
22
|
import { canBusinessSync } from "./sync-guard";
|
|
23
23
|
import { backoffDelayMs } from "./backoff";
|
|
24
|
-
|
|
24
|
+
import { reportSyncFailure } from "../adapters/sync-failure";
|
|
25
|
+
const MAX_ATTEMPTS = 8;
|
|
25
26
|
/**
|
|
26
27
|
* Stop a replay pass after this many consecutive transport-level failures
|
|
27
28
|
* (network/timeout/5xx) so a dead server is not hammered once per queued
|
|
@@ -30,14 +31,8 @@ const MAX_RETRIES = 5;
|
|
|
30
31
|
*/
|
|
31
32
|
const TRANSPORT_FAILURE_BREAKER = 3;
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
-
* permanent verdicts. Anything else 4xx (400 validation, 401 session,
|
|
35
|
-
* 402 plan, 403 forbidden) fails fast with a toast. 409 ORDER_NUMBER_TAKEN
|
|
36
|
-
* has its own regen path below and is not capped (every attempt mints a
|
|
37
|
-
* new order number, so it is never a blind retry).
|
|
34
|
+
* In-memory next-eligible time per command (shared 2s→5min curve).
|
|
38
35
|
*/
|
|
39
|
-
const RETRYABLE_4XX = new Set([404, 408, 409, 422, 429]);
|
|
40
|
-
/** In-memory next-eligible time per command (shared 2s→5min curve). */
|
|
41
36
|
const retryNotBefore = new Map();
|
|
42
37
|
const QUEUED_STATUS_BY_COMMAND = {
|
|
43
38
|
"order:accept": "accepted",
|
|
@@ -267,7 +262,7 @@ class CommandQueueService {
|
|
|
267
262
|
timestamp: command.timestamp,
|
|
268
263
|
payload: command.payload,
|
|
269
264
|
};
|
|
270
|
-
if (command.retry_count >=
|
|
265
|
+
if (command.retry_count >= MAX_ATTEMPTS) {
|
|
271
266
|
await updateCommandStatus(db, command.command_id, "failed", "Max retry count exceeded");
|
|
272
267
|
return;
|
|
273
268
|
}
|
|
@@ -336,6 +331,13 @@ class CommandQueueService {
|
|
|
336
331
|
catch { }
|
|
337
332
|
}
|
|
338
333
|
await updateCommandStatus(db, command.command_id, "failed", `order_number_taken:${message}`);
|
|
334
|
+
this.reportFailure(command, {
|
|
335
|
+
status: 409,
|
|
336
|
+
code: "ORDER_NUMBER_TAKEN",
|
|
337
|
+
message,
|
|
338
|
+
attempts: command.retry_count + 1,
|
|
339
|
+
terminal: true,
|
|
340
|
+
});
|
|
339
341
|
throw error;
|
|
340
342
|
}
|
|
341
343
|
const status = typeof apiErr?.status === "number" ? apiErr.status : undefined;
|
|
@@ -352,19 +354,28 @@ class CommandQueueService {
|
|
|
352
354
|
}
|
|
353
355
|
catch { }
|
|
354
356
|
}
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
357
|
+
// Every other failure is retryable on the shared 2s→5min backoff.
|
|
358
|
+
//
|
|
359
|
+
// 4xx (validation/permission/plan races) is bounded: a device stuck in
|
|
360
|
+
// a bad local state keeps retrying, then dead-letters so an operator is
|
|
361
|
+
// alerted and can visit the device. 5xx/network is an outage, not a
|
|
362
|
+
// device problem — it keeps retrying indefinitely so a long outage can
|
|
363
|
+
// never lose good orders. Every attempt is reported to observability.
|
|
364
|
+
const is4xxResponse = status !== undefined && status >= 400 && status < 500;
|
|
365
|
+
if (is4xxResponse) {
|
|
360
366
|
const attempts = command.retry_count + 1;
|
|
361
|
-
if (attempts >=
|
|
362
|
-
await this.failPermanently(db, command, message, `
|
|
367
|
+
if (attempts >= MAX_ATTEMPTS) {
|
|
368
|
+
await this.failPermanently(db, command, message, `4xx exhausted after ${attempts} attempts`, {
|
|
369
|
+
status,
|
|
370
|
+
code,
|
|
371
|
+
attempts,
|
|
372
|
+
});
|
|
363
373
|
return;
|
|
364
374
|
}
|
|
375
|
+
this.reportFailure(command, { status, code, message, attempts, terminal: false });
|
|
365
376
|
retryNotBefore.set(command.command_id, Date.now() + backoffDelayMs(command.retry_count));
|
|
366
377
|
await updateCommandStatus(db, command.command_id, "pending", `retry_4xx:${message}`, true);
|
|
367
|
-
syncLogger.warn("CommandQueueService", "
|
|
378
|
+
syncLogger.warn("CommandQueueService", "4xx — backing off", {
|
|
368
379
|
command_id: command.command_id,
|
|
369
380
|
command_type: command.command_type,
|
|
370
381
|
status,
|
|
@@ -372,30 +383,37 @@ class CommandQueueService {
|
|
|
372
383
|
});
|
|
373
384
|
throw new TransitionError(TRANSITION_ERROR_CODES.REPLAY_DEFERRED, `Retryable ${status} on ${command.command_type}, backing off`);
|
|
374
385
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
// Do not throw — allow replay to continue to next command.
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
// Network/timeout/5xx: NOT a rejection — keep the command retryable
|
|
384
|
-
// indefinitely (status back to pending, retry_count untouched) so a
|
|
385
|
-
// long outage never dead-letters the outbox via MAX_RETRIES. Replay
|
|
386
|
-
// stops here to preserve ordering; the next trigger retries.
|
|
386
|
+
// 5xx / network / timeout: keep the command retryable indefinitely
|
|
387
|
+
// (status back to pending, retry_count untouched) so a long outage never
|
|
388
|
+
// dead-letters the outbox. Replay defers this entity; the next trigger
|
|
389
|
+
// retries.
|
|
390
|
+
this.reportFailure(command, { status, code, message, attempts: command.retry_count, terminal: false });
|
|
387
391
|
await updateCommandStatus(db, command.command_id, "pending", `network:${message}`);
|
|
388
392
|
syncLogger.command(parsedCommand, "queued", { error: message, deferred: true });
|
|
389
393
|
throw error;
|
|
390
394
|
}
|
|
391
395
|
}
|
|
396
|
+
/** Forward a failed attempt to the host's observability reporter. */
|
|
397
|
+
reportFailure(command, extra) {
|
|
398
|
+
reportSyncFailure({
|
|
399
|
+
phase: "outbox",
|
|
400
|
+
business_id: command.business_id,
|
|
401
|
+
user_id: command.user_id,
|
|
402
|
+
device_id: command.device_id,
|
|
403
|
+
command_type: command.command_type,
|
|
404
|
+
command_id: command.command_id,
|
|
405
|
+
entity_id: command.entity_id,
|
|
406
|
+
...extra,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
392
409
|
/**
|
|
393
410
|
* Mark a command permanently failed: loud toast + optimistic revert, then
|
|
394
411
|
* replay continues to the next command. Shared by the exhausted-retry and
|
|
395
412
|
* permanent-rejection paths.
|
|
396
413
|
*/
|
|
397
|
-
async failPermanently(db, command, message, reason) {
|
|
414
|
+
async failPermanently(db, command, message, reason, extra) {
|
|
398
415
|
await updateCommandStatus(db, command.command_id, "failed", `permanent:${message}`);
|
|
416
|
+
this.reportFailure(command, { ...extra, message, terminal: true });
|
|
399
417
|
syncLogger.warn("CommandQueueService", `Queued command failed (${reason})`, {
|
|
400
418
|
command_id: command.command_id,
|
|
401
419
|
command_type: command.command_type,
|
|
@@ -19,11 +19,13 @@ import { getTransitionPath, DELIVERY_COMMANDS } from "./transition-endpoints";
|
|
|
19
19
|
import { getQueuedStatus } from "../utils/order-transitions";
|
|
20
20
|
import { extractApiErrorCode } from "../utils/error";
|
|
21
21
|
import { canBusinessSync, isAuthenticated } from "./sync-guard";
|
|
22
|
+
import { reportSyncFailure } from "../adapters/sync-failure";
|
|
23
|
+
import { getAppInstanceId } from "../adapters/devices";
|
|
22
24
|
const inflight = new Map();
|
|
23
25
|
const retryTimers = new Map();
|
|
24
26
|
const backoffTracker = new BackoffTracker();
|
|
25
|
-
/**
|
|
26
|
-
const MAX_SYNC_JOB_ATTEMPTS =
|
|
27
|
+
/** Bounded 4xx retries stop here, loudly. 5xx/network retry indefinitely. */
|
|
28
|
+
const MAX_SYNC_JOB_ATTEMPTS = 8;
|
|
27
29
|
function isOnline() {
|
|
28
30
|
const s = getConnectivityState();
|
|
29
31
|
return s.isOnline && s.isCloudReachable !== "unreachable";
|
|
@@ -65,6 +67,36 @@ function clearRetry(businessId) {
|
|
|
65
67
|
}
|
|
66
68
|
backoffTracker.reset(businessId);
|
|
67
69
|
}
|
|
70
|
+
/** Best-effort device id for failure reports (never throws). */
|
|
71
|
+
function safeDeviceId() {
|
|
72
|
+
try {
|
|
73
|
+
return getAppInstanceId();
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Forward a failed job attempt to the host's observability reporter. */
|
|
80
|
+
function reportJobFailure(businessId, row, extra) {
|
|
81
|
+
let userId;
|
|
82
|
+
try {
|
|
83
|
+
const p = JSON.parse(row.payload);
|
|
84
|
+
const candidate = p.user_id ?? p.user?.id;
|
|
85
|
+
if (typeof candidate === "string")
|
|
86
|
+
userId = candidate;
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
reportSyncFailure({
|
|
90
|
+
phase: "sync_jobs",
|
|
91
|
+
business_id: businessId,
|
|
92
|
+
user_id: userId,
|
|
93
|
+
device_id: safeDeviceId(),
|
|
94
|
+
command_type: row.command_type,
|
|
95
|
+
command_id: row.id,
|
|
96
|
+
entity_id: row.entity_id,
|
|
97
|
+
...extra,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
68
100
|
async function tryPhxPush(businessId, payload) {
|
|
69
101
|
try {
|
|
70
102
|
const socket = phoenixSocket.getChannel?.(`business:${businessId}`);
|
|
@@ -385,38 +417,13 @@ export async function drainOne(businessId, entityId) {
|
|
|
385
417
|
syncLogger.debug("SyncJobs", "401 deferred until login", { businessId });
|
|
386
418
|
return;
|
|
387
419
|
}
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// validation, 402 plan, 403 forbidden) fails fast. Backend codes are
|
|
420
|
+
// Any 4xx is retryable with backoff (bounded); 5xx/network keeps retrying
|
|
421
|
+
// indefinitely. Every failed attempt is reported to observability so an
|
|
422
|
+
// operator can see the device/app/user and visit it. BUSINESS_CLOSED is a
|
|
423
|
+
// legitimate closed state (keep pending, re-pull hours). Backend codes are
|
|
393
424
|
// exact (no message matching).
|
|
394
425
|
const isBusinessClosed = rest.code === "BUSINESS_CLOSED" || rest.code === "HOURS_NOT_SYNCED";
|
|
395
|
-
const is4xx = rest.status && rest.status >= 400 && rest.status < 500;
|
|
396
|
-
const isRetryable4xx = rest.status === 401 || rest.status === 408 || rest.status === 409
|
|
397
|
-
|| rest.status === 422 || rest.status === 429 || rest.status === 404
|
|
398
|
-
|| isBusinessClosed;
|
|
399
|
-
if (is4xx && !isRetryable4xx) {
|
|
400
|
-
syncLogger.warn("SyncJobs", "Permanent 4xx — marking job failed to stop retry loop", { businessId, entityId, status: rest.status, code: rest.code, message: rest.message });
|
|
401
|
-
try {
|
|
402
|
-
await db.runAsync(`UPDATE sync_jobs SET status='failed', last_error=?, updated_at=? WHERE id=?;`, `${rest.status} ${rest.code ?? ""} ${rest.message ?? ""}`.trim(), new Date().toISOString(), row.id);
|
|
403
|
-
}
|
|
404
|
-
catch { }
|
|
405
|
-
clearRetry(businessId);
|
|
406
|
-
try {
|
|
407
|
-
const { notifyError } = await import("../adapters/notify");
|
|
408
|
-
notifyError(`Server rejected ${row.command_type}: ${rest.message ?? rest.code ?? rest.status}`);
|
|
409
|
-
}
|
|
410
|
-
catch { }
|
|
411
|
-
// Retain optimistic row — pull will converge for transitions, create stays for manual retry.
|
|
412
|
-
if (row.command_type !== "order:create") {
|
|
413
|
-
try {
|
|
414
|
-
void pullRemoteOrders(businessId).catch(() => { });
|
|
415
|
-
}
|
|
416
|
-
catch { }
|
|
417
|
-
}
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
426
|
+
const is4xx = !!rest.status && rest.status >= 400 && rest.status < 500;
|
|
420
427
|
// Already-applied absorb for transitions: the server already moved the
|
|
421
428
|
// order (duplicate tap, earlier drain won the race) — complete the job
|
|
422
429
|
// instead of failing it. Server truth only (hlc-guarded pull), never the
|
|
@@ -432,39 +439,43 @@ export async function drainOne(businessId, entityId) {
|
|
|
432
439
|
}
|
|
433
440
|
catch { }
|
|
434
441
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
syncLogger.warn("SyncJobs", "Retryable 4xx exhausted — marking job failed", { businessId, entityId, status: rest.status, code: rest.code, attempts: row.attempts });
|
|
442
|
+
if (isBusinessClosed) {
|
|
443
|
+
syncLogger.warn("SyncJobs", "Business closed — keeping pending for retry after hours sync", { businessId, entityId, code: rest.code });
|
|
444
|
+
reportJobFailure(businessId, row, { status: rest.status, code: rest.code, message: rest.message, attempts: row.attempts, terminal: false });
|
|
439
445
|
try {
|
|
440
|
-
await db.runAsync(`UPDATE sync_jobs SET
|
|
446
|
+
await db.runAsync(`UPDATE sync_jobs SET last_error=?, updated_at=? WHERE id=?;`, `${rest.status} ${rest.code ?? ""} ${rest.message ?? ""}`.trim(), new Date().toISOString(), row.id);
|
|
441
447
|
}
|
|
442
448
|
catch { }
|
|
443
|
-
|
|
449
|
+
// Re-pull reference so hours merge converges, then retry with backoff.
|
|
444
450
|
try {
|
|
445
|
-
const {
|
|
446
|
-
|
|
451
|
+
const { referenceSyncService } = await import("../reference/service");
|
|
452
|
+
void referenceSyncService.sync({ businessId, keys: ["operating-hours", "business:profile"] }).catch(() => { });
|
|
447
453
|
}
|
|
448
454
|
catch { }
|
|
455
|
+
scheduleRetry(businessId);
|
|
449
456
|
return;
|
|
450
457
|
}
|
|
451
|
-
|
|
452
|
-
|
|
458
|
+
const attempts = (row.attempts ?? 0) + 1;
|
|
459
|
+
// Bounded 4xx: after MAX_SYNC_JOB_ATTEMPTS, dead-letter (retained) with a
|
|
460
|
+
// loud toast + observability report so an operator can visit the device.
|
|
461
|
+
if (is4xx && attempts >= MAX_SYNC_JOB_ATTEMPTS) {
|
|
462
|
+
syncLogger.warn("SyncJobs", "4xx exhausted — marking job failed", { businessId, entityId, status: rest.status, code: rest.code, attempts: row.attempts });
|
|
453
463
|
try {
|
|
454
|
-
await db.runAsync(`UPDATE sync_jobs SET last_error=?, updated_at=? WHERE id=?;`, `${rest.status} ${rest.code ?? ""} ${rest.message ?? ""}`.trim(), new Date().toISOString(), row.id);
|
|
464
|
+
await db.runAsync(`UPDATE sync_jobs SET status='failed', last_error=?, updated_at=? WHERE id=?;`, `${rest.status} ${rest.code ?? ""} ${rest.message ?? ""} (after ${MAX_SYNC_JOB_ATTEMPTS} attempts)`.trim(), new Date().toISOString(), row.id);
|
|
455
465
|
}
|
|
456
466
|
catch { }
|
|
457
|
-
|
|
467
|
+
reportJobFailure(businessId, row, { status: rest.status, code: rest.code, message: rest.message, attempts, terminal: true });
|
|
468
|
+
clearRetry(businessId);
|
|
458
469
|
try {
|
|
459
|
-
const {
|
|
460
|
-
|
|
470
|
+
const { notifyError } = await import("../adapters/notify");
|
|
471
|
+
notifyError(`Server rejected ${row.command_type}: ${rest.message ?? rest.code ?? rest.status} (after retries)`);
|
|
461
472
|
}
|
|
462
473
|
catch { }
|
|
463
|
-
scheduleRetry(businessId);
|
|
464
474
|
return;
|
|
465
475
|
}
|
|
466
|
-
//
|
|
476
|
+
// 4xx (bounded) or 5xx/network (indefinite): keep pending and back off.
|
|
467
477
|
syncLogger.debug("SyncJobs", "drainOne kept pending for retry", { businessId, entityId, status: rest.status, code: rest.code });
|
|
478
|
+
reportJobFailure(businessId, row, { status: rest.status, code: rest.code, message: rest.message, attempts, terminal: false });
|
|
468
479
|
try {
|
|
469
480
|
await db.runAsync(`UPDATE sync_jobs SET attempts = attempts + 1, last_error = ?, updated_at = ? WHERE id = ?;`, `${rest.status ?? "network"} ${rest.code ?? ""} ${rest.message ?? ""}`.trim(), new Date().toISOString(), row.id);
|
|
470
481
|
}
|
package/package.json
CHANGED