@open-mercato/core 0.6.7-develop.6726.1.983ae8a07e → 0.6.7-develop.6749.1.6b54c56dfe
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/resources_resource/index.js +2 -0
- package/dist/generated/entities/resources_resource/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +1 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/salesFixtures.js +31 -2
- package/dist/helpers/integration/salesFixtures.js.map +2 -2
- package/dist/helpers/integration/salesUi.js +5 -0
- package/dist/helpers/integration/salesUi.js.map +2 -2
- package/dist/modules/auth/api/profile/route.js +8 -2
- package/dist/modules/auth/api/profile/route.js.map +2 -2
- package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
- package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
- package/dist/modules/communication_channels/data/enrichers.js +4 -4
- package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
- package/dist/modules/customers/analytics.js +1 -0
- package/dist/modules/customers/analytics.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +2 -1
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +1 -1
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +52 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +42 -26
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +2 -2
- package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +2 -2
- package/dist/modules/data_sync/lib/queue-policy.js +15 -0
- package/dist/modules/data_sync/lib/queue-policy.js.map +7 -0
- package/dist/modules/data_sync/lib/queue.js +16 -1
- package/dist/modules/data_sync/lib/queue.js.map +2 -2
- package/dist/modules/data_sync/lib/start-run.js +2 -1
- package/dist/modules/data_sync/lib/start-run.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +35 -2
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +70 -2
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-export.js +9 -2
- package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-import.js +9 -2
- package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +34 -12
- package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
- package/dist/modules/dictionaries/api/openapi.js +15 -2
- package/dist/modules/dictionaries/api/openapi.js.map +2 -2
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js +30 -29
- package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js.map +2 -2
- package/dist/modules/dictionaries/data/validators.js +7 -0
- package/dist/modules/dictionaries/data/validators.js.map +2 -2
- package/dist/modules/dictionaries/lib/clientEntries.js +4 -3
- package/dist/modules/dictionaries/lib/clientEntries.js.map +2 -2
- package/dist/modules/dictionaries/lib/fetchAllEntries.js +43 -0
- package/dist/modules/dictionaries/lib/fetchAllEntries.js.map +7 -0
- package/dist/modules/entities/api/definitions.js +51 -3
- package/dist/modules/entities/api/definitions.js.map +2 -2
- package/dist/modules/entities/api/entities.js +21 -2
- package/dist/modules/entities/api/entities.js.map +2 -2
- package/dist/modules/entities/api/records.js +3 -24
- package/dist/modules/entities/api/records.js.map +2 -2
- package/dist/modules/entities/api/sidebar-entities.js +34 -12
- package/dist/modules/entities/api/sidebar-entities.js.map +2 -2
- package/dist/modules/entities/lib/entityAcl.js +46 -0
- package/dist/modules/entities/lib/entityAcl.js.map +2 -2
- package/dist/modules/messages/api/route.js +2 -5
- package/dist/modules/messages/api/route.js.map +2 -2
- package/dist/modules/messages/lib/participantScope.js +15 -0
- package/dist/modules/messages/lib/participantScope.js.map +7 -0
- package/dist/modules/notifications/api/route.js +3 -1
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/api/unread-count/route.js +25 -6
- package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
- package/dist/modules/notifications/lib/notificationScope.js +26 -0
- package/dist/modules/notifications/lib/notificationScope.js.map +7 -0
- package/dist/modules/notifications/lib/notificationService.js +7 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +16 -2
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/planner/components/unavailabilityReasons.js +3 -3
- package/dist/modules/planner/components/unavailabilityReasons.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js +2 -0
- package/dist/modules/progress/lib/progressService.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +322 -104
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/resources/api/resources.js +2 -0
- package/dist/modules/resources/api/resources.js.map +2 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -2
- package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
- package/dist/modules/resources/commands/resources.js +9 -0
- package/dist/modules/resources/commands/resources.js.map +2 -2
- package/dist/modules/resources/components/detail/dictionaries.js +3 -3
- package/dist/modules/resources/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/resources/data/entities.js +3 -0
- package/dist/modules/resources/data/entities.js.map +2 -2
- package/dist/modules/resources/data/validators.js +6 -2
- package/dist/modules/resources/data/validators.js.map +2 -2
- package/dist/modules/resources/lib/seeds.js +16 -9
- package/dist/modules/resources/lib/seeds.js.map +2 -2
- package/dist/modules/resources/migrations/Migration20260608231000.js +13 -0
- package/dist/modules/resources/migrations/Migration20260608231000.js.map +7 -0
- package/dist/modules/sales/backend/sales/documents/create/page.js +11 -14
- package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +8 -0
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +18 -9
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +12 -5
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +48 -2
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +167 -0
- package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +7 -0
- package/dist/modules/sales/data/validators.js +5 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/staff/components/detail/dictionaries.js +3 -3
- package/dist/modules/staff/components/detail/dictionaries.js.map +2 -2
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +11 -4
- package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
- package/generated/entities/resources_resource/index.ts +1 -0
- package/generated/entity-fields-registry.ts +1 -0
- package/package.json +7 -7
- package/src/helpers/integration/salesFixtures.ts +34 -2
- package/src/helpers/integration/salesUi.ts +8 -0
- package/src/modules/auth/api/profile/route.ts +8 -2
- package/src/modules/auth/backend/auth/profile/page.tsx +1 -0
- package/src/modules/auth/backend/profile/change-password/page.tsx +1 -0
- package/src/modules/communication_channels/data/enrichers.ts +6 -26
- package/src/modules/customers/analytics.ts +1 -0
- package/src/modules/customers/components/detail/DealForm.tsx +5 -1
- package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
- package/src/modules/customers/data/validators.ts +5 -1
- package/src/modules/dashboards/i18n/de.json +3 -0
- package/src/modules/dashboards/i18n/en.json +3 -0
- package/src/modules/dashboards/i18n/es.json +3 -0
- package/src/modules/dashboards/i18n/pl.json +3 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +86 -0
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +33 -18
- package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +2 -2
- package/src/modules/data_sync/lib/adapter.ts +18 -0
- package/src/modules/data_sync/lib/queue-policy.ts +30 -0
- package/src/modules/data_sync/lib/queue.ts +19 -1
- package/src/modules/data_sync/lib/start-run.ts +2 -1
- package/src/modules/data_sync/lib/sync-engine.ts +46 -0
- package/src/modules/data_sync/lib/sync-run-service.ts +82 -1
- package/src/modules/data_sync/workers/sync-export.ts +8 -1
- package/src/modules/data_sync/workers/sync-import.ts +8 -1
- package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +39 -11
- package/src/modules/dictionaries/api/openapi.ts +19 -0
- package/src/modules/dictionaries/components/hooks/useDictionaryEntries.ts +35 -36
- package/src/modules/dictionaries/data/validators.ts +14 -0
- package/src/modules/dictionaries/lib/clientEntries.ts +4 -3
- package/src/modules/dictionaries/lib/fetchAllEntries.ts +72 -0
- package/src/modules/entities/api/definitions.ts +51 -3
- package/src/modules/entities/api/entities.ts +30 -2
- package/src/modules/entities/api/records.ts +3 -27
- package/src/modules/entities/api/sidebar-entities.ts +43 -14
- package/src/modules/entities/lib/entityAcl.ts +58 -0
- package/src/modules/messages/api/route.ts +4 -5
- package/src/modules/messages/lib/participantScope.ts +64 -0
- package/src/modules/notifications/api/route.ts +2 -0
- package/src/modules/notifications/api/unread-count/route.ts +42 -5
- package/src/modules/notifications/lib/notificationScope.ts +40 -0
- package/src/modules/notifications/lib/notificationService.ts +5 -0
- package/src/modules/notifications/lib/routeHelpers.ts +19 -2
- package/src/modules/planner/components/unavailabilityReasons.ts +3 -3
- package/src/modules/progress/AGENTS.md +11 -1
- package/src/modules/progress/lib/progressService.ts +1 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +385 -117
- package/src/modules/resources/api/resources.ts +2 -0
- package/src/modules/resources/backend/resources/resources/[id]/page.tsx +9 -3
- package/src/modules/resources/commands/resources.ts +10 -0
- package/src/modules/resources/components/detail/dictionaries.ts +3 -3
- package/src/modules/resources/data/entities.ts +3 -0
- package/src/modules/resources/data/validators.ts +5 -0
- package/src/modules/resources/lib/seeds.ts +16 -9
- package/src/modules/resources/migrations/.snapshot-open-mercato.json +17 -1
- package/src/modules/resources/migrations/Migration20260608231000.ts +13 -0
- package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -4
- package/src/modules/sales/commands/documents.ts +8 -0
- package/src/modules/sales/components/documents/ItemsSection.tsx +19 -10
- package/src/modules/sales/components/documents/LineItemDialog.tsx +13 -5
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +48 -6
- package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +198 -0
- package/src/modules/sales/data/validators.ts +7 -1
- package/src/modules/sales/i18n/de.json +2 -0
- package/src/modules/sales/i18n/en.json +2 -0
- package/src/modules/sales/i18n/es.json +2 -0
- package/src/modules/sales/i18n/pl.json +2 -0
- package/src/modules/staff/components/detail/dictionaries.ts +3 -3
- package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +12 -5
|
@@ -1,15 +1,28 @@
|
|
|
1
|
+
import { raw } from "@mikro-orm/core";
|
|
1
2
|
import { ProgressJob } from "../data/entities.js";
|
|
2
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
calculateEta,
|
|
5
|
+
calculateProgressPercent,
|
|
6
|
+
HEARTBEAT_INTERVAL_MS,
|
|
7
|
+
STALE_JOB_TIMEOUT_SECONDS,
|
|
8
|
+
STALE_PENDING_TIMEOUT_SECONDS
|
|
9
|
+
} from "./progressService.js";
|
|
3
10
|
import { PROGRESS_EVENTS } from "./events.js";
|
|
4
11
|
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
5
12
|
const DEFAULT_BROADCAST_MIN_INTERVAL_MS = 250;
|
|
6
13
|
function resolveBroadcastMinIntervalMs() {
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
const parsed = Number.parseInt(
|
|
14
|
+
const rawValue = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS;
|
|
15
|
+
if (rawValue == null || rawValue.trim() === "") return DEFAULT_BROADCAST_MIN_INTERVAL_MS;
|
|
16
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
10
17
|
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_BROADCAST_MIN_INTERVAL_MS;
|
|
11
18
|
return parsed;
|
|
12
19
|
}
|
|
20
|
+
const TERMINAL_STATUSES = ["completed", "failed", "cancelled"];
|
|
21
|
+
const PROGRESS_WRITABLE_STATUSES = ["pending", "running"];
|
|
22
|
+
const START_FROM_STATUSES = ["pending", "failed"];
|
|
23
|
+
const COMPLETE_FROM_STATUSES = ["pending", "running", "failed"];
|
|
24
|
+
const FAIL_FROM_STATUSES = ["pending", "running"];
|
|
25
|
+
const CANCEL_FROM_STATUSES = ["pending", "running", "failed"];
|
|
13
26
|
function buildJobPayload(job) {
|
|
14
27
|
return {
|
|
15
28
|
jobId: job.id,
|
|
@@ -37,34 +50,86 @@ function createProgressService(em, eventBus) {
|
|
|
37
50
|
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
38
51
|
};
|
|
39
52
|
}
|
|
40
|
-
async function
|
|
53
|
+
async function loadFreshJob(jobId, ctx) {
|
|
54
|
+
return em.findOne(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true });
|
|
55
|
+
}
|
|
56
|
+
async function ensureThrottleEntry(jobId, ctx) {
|
|
41
57
|
const cached = jobUpdateThrottle.get(jobId);
|
|
42
|
-
if (cached) return cached
|
|
43
|
-
|
|
58
|
+
if (cached) return cached;
|
|
59
|
+
const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true });
|
|
60
|
+
const entry = {
|
|
61
|
+
job,
|
|
62
|
+
lastBroadcastAt: 0,
|
|
63
|
+
lastPersistedAt: 0,
|
|
64
|
+
lastBroadcastPercent: job.progressPercent,
|
|
65
|
+
pendingDelta: 0,
|
|
66
|
+
absoluteCountsPending: false
|
|
67
|
+
};
|
|
68
|
+
jobUpdateThrottle.set(jobId, entry);
|
|
69
|
+
return entry;
|
|
70
|
+
}
|
|
71
|
+
function forgetJobThrottle(jobId) {
|
|
72
|
+
jobUpdateThrottle.delete(jobId);
|
|
44
73
|
}
|
|
45
|
-
|
|
74
|
+
function buildBufferedCountData(entry) {
|
|
75
|
+
if (entry.absoluteCountsPending || !Number.isSafeInteger(entry.pendingDelta)) {
|
|
76
|
+
return { processedCount: entry.job.processedCount };
|
|
77
|
+
}
|
|
78
|
+
if (entry.pendingDelta !== 0) {
|
|
79
|
+
const processedCount = raw(`processed_count + ${entry.pendingDelta}`);
|
|
80
|
+
const totalCount = entry.job.totalCount;
|
|
81
|
+
if (Number.isSafeInteger(totalCount) && totalCount != null && totalCount > 0) {
|
|
82
|
+
return {
|
|
83
|
+
processedCount,
|
|
84
|
+
progressPercent: raw(
|
|
85
|
+
`least(100, round(((processed_count + ${entry.pendingDelta})::numeric / ${totalCount}) * 100))`
|
|
86
|
+
)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return { processedCount };
|
|
90
|
+
}
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
async function persistAndMaybeBroadcast(entry, ctx) {
|
|
94
|
+
const job = entry.job;
|
|
46
95
|
const now = Date.now();
|
|
47
|
-
const
|
|
48
|
-
const
|
|
96
|
+
const shouldBroadcast = broadcastMinIntervalMs <= 0 || now - entry.lastBroadcastAt >= broadcastMinIntervalMs || Math.abs(job.progressPercent - entry.lastBroadcastPercent) >= 1;
|
|
97
|
+
const shouldPersist = shouldBroadcast || now - entry.lastPersistedAt >= HEARTBEAT_INTERVAL_MS;
|
|
98
|
+
if (!shouldPersist) return job;
|
|
99
|
+
const usesAtomicIncrement = !entry.absoluteCountsPending && Number.isSafeInteger(entry.pendingDelta) && entry.pendingDelta !== 0;
|
|
100
|
+
const data = {
|
|
101
|
+
progressPercent: job.progressPercent,
|
|
102
|
+
totalCount: job.totalCount ?? null,
|
|
103
|
+
etaSeconds: job.etaSeconds ?? null,
|
|
104
|
+
meta: job.meta ?? null,
|
|
105
|
+
heartbeatAt: job.heartbeatAt ?? /* @__PURE__ */ new Date(),
|
|
106
|
+
updatedAt: /* @__PURE__ */ new Date(),
|
|
107
|
+
...buildBufferedCountData(entry)
|
|
108
|
+
};
|
|
109
|
+
const affected = await em.nativeUpdate(ProgressJob, {
|
|
110
|
+
...jobScopeFilter(job.id, ctx),
|
|
111
|
+
status: { $in: PROGRESS_WRITABLE_STATUSES }
|
|
112
|
+
}, data);
|
|
113
|
+
if (affected === 0) {
|
|
114
|
+
forgetJobThrottle(job.id);
|
|
115
|
+
const fresh = await loadFreshJob(job.id, ctx);
|
|
116
|
+
return fresh ?? job;
|
|
117
|
+
}
|
|
118
|
+
const persistedJob = usesAtomicIncrement ? await loadFreshJob(job.id, ctx) ?? job : job;
|
|
119
|
+
entry.job = persistedJob;
|
|
120
|
+
entry.pendingDelta = 0;
|
|
121
|
+
entry.absoluteCountsPending = false;
|
|
122
|
+
entry.lastPersistedAt = now;
|
|
49
123
|
if (shouldBroadcast) {
|
|
50
|
-
await em.flush();
|
|
51
124
|
await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {
|
|
52
|
-
...buildJobPayload(
|
|
125
|
+
...buildJobPayload(persistedJob),
|
|
53
126
|
tenantId: ctx.tenantId,
|
|
54
|
-
organizationId:
|
|
55
|
-
});
|
|
56
|
-
jobUpdateThrottle.set(job.id, { job, lastBroadcastAt: now, lastBroadcastPercent: job.progressPercent });
|
|
57
|
-
} else {
|
|
58
|
-
jobUpdateThrottle.set(job.id, {
|
|
59
|
-
job,
|
|
60
|
-
lastBroadcastAt: cached.lastBroadcastAt,
|
|
61
|
-
lastBroadcastPercent: cached.lastBroadcastPercent
|
|
127
|
+
organizationId: persistedJob.organizationId ?? null
|
|
62
128
|
});
|
|
129
|
+
entry.lastBroadcastAt = now;
|
|
130
|
+
entry.lastBroadcastPercent = persistedJob.progressPercent;
|
|
63
131
|
}
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
function forgetJobThrottle(jobId) {
|
|
67
|
-
jobUpdateThrottle.delete(jobId);
|
|
132
|
+
return persistedJob;
|
|
68
133
|
}
|
|
69
134
|
return {
|
|
70
135
|
async createJob(input, ctx) {
|
|
@@ -92,18 +157,33 @@ function createProgressService(em, eventBus) {
|
|
|
92
157
|
return job;
|
|
93
158
|
},
|
|
94
159
|
async startJob(jobId, ctx) {
|
|
95
|
-
const job = await em.findOneOrFail(ProgressJob, {
|
|
96
|
-
|
|
97
|
-
tenantId: ctx.tenantId,
|
|
98
|
-
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
99
|
-
});
|
|
100
|
-
if (job.status === "cancelled") {
|
|
160
|
+
const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true });
|
|
161
|
+
if (job.status === "running" || job.status === "cancelled" || job.status === "completed") {
|
|
101
162
|
return job;
|
|
102
163
|
}
|
|
164
|
+
const now = /* @__PURE__ */ new Date();
|
|
165
|
+
const affected = await em.nativeUpdate(ProgressJob, {
|
|
166
|
+
...jobScopeFilter(jobId, ctx),
|
|
167
|
+
status: { $in: START_FROM_STATUSES }
|
|
168
|
+
}, {
|
|
169
|
+
status: "running",
|
|
170
|
+
startedAt: now,
|
|
171
|
+
heartbeatAt: now,
|
|
172
|
+
finishedAt: null,
|
|
173
|
+
errorMessage: null,
|
|
174
|
+
errorStack: null,
|
|
175
|
+
updatedAt: now
|
|
176
|
+
});
|
|
177
|
+
if (affected === 0) {
|
|
178
|
+
const fresh = await loadFreshJob(jobId, ctx);
|
|
179
|
+
return fresh ?? job;
|
|
180
|
+
}
|
|
103
181
|
job.status = "running";
|
|
104
|
-
job.startedAt =
|
|
105
|
-
job.heartbeatAt =
|
|
106
|
-
|
|
182
|
+
job.startedAt = now;
|
|
183
|
+
job.heartbeatAt = now;
|
|
184
|
+
job.finishedAt = null;
|
|
185
|
+
job.errorMessage = null;
|
|
186
|
+
job.errorStack = null;
|
|
107
187
|
await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {
|
|
108
188
|
...buildJobPayload(job),
|
|
109
189
|
tenantId: ctx.tenantId,
|
|
@@ -112,13 +192,16 @@ function createProgressService(em, eventBus) {
|
|
|
112
192
|
return job;
|
|
113
193
|
},
|
|
114
194
|
async updateProgress(jobId, input, ctx) {
|
|
115
|
-
const
|
|
116
|
-
|
|
195
|
+
const entry = await ensureThrottleEntry(jobId, ctx);
|
|
196
|
+
const job = entry.job;
|
|
197
|
+
if (TERMINAL_STATUSES.includes(job.status)) {
|
|
117
198
|
forgetJobThrottle(jobId);
|
|
118
199
|
return job;
|
|
119
200
|
}
|
|
120
201
|
if (input.processedCount !== void 0) {
|
|
121
202
|
job.processedCount = input.processedCount;
|
|
203
|
+
entry.absoluteCountsPending = true;
|
|
204
|
+
entry.pendingDelta = 0;
|
|
122
205
|
}
|
|
123
206
|
if (input.totalCount !== void 0) {
|
|
124
207
|
job.totalCount = input.totalCount;
|
|
@@ -137,15 +220,17 @@ function createProgressService(em, eventBus) {
|
|
|
137
220
|
job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt);
|
|
138
221
|
}
|
|
139
222
|
job.heartbeatAt = /* @__PURE__ */ new Date();
|
|
140
|
-
return
|
|
223
|
+
return persistAndMaybeBroadcast(entry, ctx);
|
|
141
224
|
},
|
|
142
225
|
async incrementProgress(jobId, delta, ctx) {
|
|
143
|
-
const
|
|
144
|
-
|
|
226
|
+
const entry = await ensureThrottleEntry(jobId, ctx);
|
|
227
|
+
const job = entry.job;
|
|
228
|
+
if (TERMINAL_STATUSES.includes(job.status)) {
|
|
145
229
|
forgetJobThrottle(jobId);
|
|
146
230
|
return job;
|
|
147
231
|
}
|
|
148
232
|
job.processedCount += delta;
|
|
233
|
+
entry.pendingDelta += delta;
|
|
149
234
|
job.heartbeatAt = /* @__PURE__ */ new Date();
|
|
150
235
|
if (job.totalCount) {
|
|
151
236
|
job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount);
|
|
@@ -153,104 +238,189 @@ function createProgressService(em, eventBus) {
|
|
|
153
238
|
job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt);
|
|
154
239
|
}
|
|
155
240
|
}
|
|
156
|
-
return
|
|
241
|
+
return persistAndMaybeBroadcast(entry, ctx);
|
|
157
242
|
},
|
|
158
243
|
async completeJob(jobId, input, ctx) {
|
|
159
|
-
const job = await
|
|
160
|
-
id: jobId,
|
|
161
|
-
tenantId: ctx.tenantId,
|
|
162
|
-
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
163
|
-
});
|
|
244
|
+
const job = await loadFreshJob(jobId, ctx);
|
|
164
245
|
if (!job) throw new Error(`Job ${jobId} not found`);
|
|
165
|
-
if (job.status === "cancelled") {
|
|
246
|
+
if (job.status === "cancelled" || job.status === "completed") {
|
|
247
|
+
forgetJobThrottle(jobId);
|
|
166
248
|
return job;
|
|
167
249
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
250
|
+
const entry = jobUpdateThrottle.get(jobId);
|
|
251
|
+
const snapshot = entry?.job ?? job;
|
|
252
|
+
const now = /* @__PURE__ */ new Date();
|
|
253
|
+
const data = {
|
|
254
|
+
status: "completed",
|
|
255
|
+
finishedAt: now,
|
|
256
|
+
etaSeconds: 0,
|
|
257
|
+
updatedAt: now,
|
|
258
|
+
...input?.resultSummary ? { resultSummary: input.resultSummary } : {},
|
|
259
|
+
...entry ? {
|
|
260
|
+
totalCount: snapshot.totalCount ?? null,
|
|
261
|
+
meta: snapshot.meta ?? null,
|
|
262
|
+
...buildBufferedCountData(entry)
|
|
263
|
+
} : {},
|
|
264
|
+
progressPercent: 100
|
|
265
|
+
};
|
|
266
|
+
const affected = await em.nativeUpdate(ProgressJob, {
|
|
267
|
+
...jobScopeFilter(jobId, ctx),
|
|
268
|
+
status: { $in: COMPLETE_FROM_STATUSES }
|
|
269
|
+
}, data);
|
|
270
|
+
forgetJobThrottle(jobId);
|
|
271
|
+
if (affected === 0) {
|
|
272
|
+
const fresh = await loadFreshJob(jobId, ctx);
|
|
273
|
+
return fresh ?? job;
|
|
274
|
+
}
|
|
275
|
+
snapshot.status = "completed";
|
|
276
|
+
snapshot.finishedAt = now;
|
|
277
|
+
snapshot.progressPercent = 100;
|
|
278
|
+
snapshot.etaSeconds = 0;
|
|
172
279
|
if (input?.resultSummary) {
|
|
173
|
-
|
|
280
|
+
snapshot.resultSummary = input.resultSummary;
|
|
174
281
|
}
|
|
175
|
-
await
|
|
282
|
+
const persistedJob = await loadFreshJob(jobId, ctx) ?? snapshot;
|
|
176
283
|
await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {
|
|
177
|
-
...buildJobPayload(
|
|
178
|
-
resultSummary:
|
|
284
|
+
...buildJobPayload(persistedJob),
|
|
285
|
+
resultSummary: persistedJob.resultSummary,
|
|
179
286
|
tenantId: ctx.tenantId,
|
|
180
|
-
organizationId:
|
|
287
|
+
organizationId: persistedJob.organizationId ?? null
|
|
181
288
|
});
|
|
182
|
-
|
|
183
|
-
return job;
|
|
289
|
+
return persistedJob;
|
|
184
290
|
},
|
|
185
291
|
async failJob(jobId, input, ctx) {
|
|
186
|
-
const job = await
|
|
187
|
-
id: jobId,
|
|
188
|
-
tenantId: ctx.tenantId,
|
|
189
|
-
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
190
|
-
});
|
|
292
|
+
const job = await loadFreshJob(jobId, ctx);
|
|
191
293
|
if (!job) throw new Error(`Job ${jobId} not found`);
|
|
192
|
-
if (job.status
|
|
294
|
+
if (TERMINAL_STATUSES.includes(job.status)) {
|
|
295
|
+
forgetJobThrottle(jobId);
|
|
193
296
|
return job;
|
|
194
297
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
298
|
+
const entry = jobUpdateThrottle.get(jobId);
|
|
299
|
+
const snapshot = entry?.job ?? job;
|
|
300
|
+
const now = /* @__PURE__ */ new Date();
|
|
301
|
+
const data = {
|
|
302
|
+
status: "failed",
|
|
303
|
+
finishedAt: now,
|
|
304
|
+
errorMessage: input.errorMessage,
|
|
305
|
+
errorStack: input.errorStack,
|
|
306
|
+
updatedAt: now,
|
|
307
|
+
...entry ? {
|
|
308
|
+
totalCount: snapshot.totalCount ?? null,
|
|
309
|
+
meta: snapshot.meta ?? null,
|
|
310
|
+
...buildBufferedCountData(entry)
|
|
311
|
+
} : {}
|
|
312
|
+
};
|
|
313
|
+
const affected = await em.nativeUpdate(ProgressJob, {
|
|
314
|
+
...jobScopeFilter(jobId, ctx),
|
|
315
|
+
status: { $in: FAIL_FROM_STATUSES }
|
|
316
|
+
}, data);
|
|
317
|
+
forgetJobThrottle(jobId);
|
|
318
|
+
if (affected === 0) {
|
|
319
|
+
const fresh = await loadFreshJob(jobId, ctx);
|
|
320
|
+
return fresh ?? job;
|
|
321
|
+
}
|
|
322
|
+
snapshot.status = "failed";
|
|
323
|
+
snapshot.finishedAt = now;
|
|
324
|
+
snapshot.errorMessage = input.errorMessage;
|
|
325
|
+
snapshot.errorStack = input.errorStack;
|
|
326
|
+
const persistedJob = await loadFreshJob(jobId, ctx) ?? snapshot;
|
|
200
327
|
await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {
|
|
201
|
-
...buildJobPayload(
|
|
202
|
-
errorMessage:
|
|
328
|
+
...buildJobPayload(persistedJob),
|
|
329
|
+
errorMessage: persistedJob.errorMessage,
|
|
203
330
|
tenantId: ctx.tenantId,
|
|
204
|
-
organizationId:
|
|
331
|
+
organizationId: persistedJob.organizationId ?? null
|
|
205
332
|
});
|
|
206
|
-
|
|
207
|
-
return job;
|
|
333
|
+
return persistedJob;
|
|
208
334
|
},
|
|
209
335
|
async cancelJob(jobId, ctx) {
|
|
210
|
-
const job = await em.findOneOrFail(ProgressJob, {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
336
|
+
const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true });
|
|
337
|
+
if (TERMINAL_STATUSES.includes(job.status)) {
|
|
338
|
+
return job;
|
|
339
|
+
}
|
|
340
|
+
if (!job.cancellable) {
|
|
341
|
+
throw new Error(`Job ${jobId} is not cancellable`);
|
|
342
|
+
}
|
|
343
|
+
const now = /* @__PURE__ */ new Date();
|
|
344
|
+
const cancelledNow = await em.nativeUpdate(ProgressJob, {
|
|
345
|
+
...jobScopeFilter(jobId, ctx),
|
|
214
346
|
cancellable: true,
|
|
215
|
-
status:
|
|
347
|
+
status: "pending"
|
|
348
|
+
}, {
|
|
349
|
+
status: "cancelled",
|
|
350
|
+
cancelRequestedAt: now,
|
|
351
|
+
cancelledByUserId: ctx.userId ?? null,
|
|
352
|
+
finishedAt: now,
|
|
353
|
+
etaSeconds: 0,
|
|
354
|
+
updatedAt: now
|
|
216
355
|
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
356
|
+
let requested = 0;
|
|
357
|
+
if (cancelledNow === 0) {
|
|
358
|
+
requested = await em.nativeUpdate(ProgressJob, {
|
|
359
|
+
...jobScopeFilter(jobId, ctx),
|
|
360
|
+
cancellable: true,
|
|
361
|
+
status: "running"
|
|
362
|
+
}, {
|
|
363
|
+
cancelRequestedAt: now,
|
|
364
|
+
cancelledByUserId: ctx.userId ?? null,
|
|
365
|
+
updatedAt: now
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (cancelledNow === 0 && requested === 0) {
|
|
369
|
+
const fresh = await loadFreshJob(jobId, ctx);
|
|
370
|
+
return fresh ?? job;
|
|
371
|
+
}
|
|
372
|
+
forgetJobThrottle(jobId);
|
|
373
|
+
if (cancelledNow > 0) {
|
|
220
374
|
job.status = "cancelled";
|
|
221
|
-
job.finishedAt =
|
|
375
|
+
job.finishedAt = now;
|
|
376
|
+
job.etaSeconds = 0;
|
|
222
377
|
}
|
|
223
|
-
|
|
378
|
+
job.cancelRequestedAt = now;
|
|
379
|
+
job.cancelledByUserId = ctx.userId ?? null;
|
|
224
380
|
await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {
|
|
225
381
|
...buildJobPayload(job),
|
|
226
382
|
tenantId: ctx.tenantId,
|
|
227
383
|
organizationId: job.organizationId ?? null
|
|
228
384
|
});
|
|
229
|
-
forgetJobThrottle(jobId);
|
|
230
385
|
return job;
|
|
231
386
|
},
|
|
232
387
|
async markCancelled(jobId, ctx) {
|
|
233
|
-
const job = await
|
|
234
|
-
id: jobId,
|
|
235
|
-
tenantId: ctx.tenantId,
|
|
236
|
-
...ctx.organizationId ? { organizationId: ctx.organizationId } : {}
|
|
237
|
-
});
|
|
388
|
+
const job = await loadFreshJob(jobId, ctx);
|
|
238
389
|
if (!job) throw new Error(`Job ${jobId} not found`);
|
|
239
|
-
if (job.status === "cancelled") {
|
|
390
|
+
if (job.status === "cancelled" || job.status === "completed") {
|
|
391
|
+
forgetJobThrottle(jobId);
|
|
240
392
|
return job;
|
|
241
393
|
}
|
|
242
|
-
|
|
243
|
-
job.
|
|
394
|
+
const now = /* @__PURE__ */ new Date();
|
|
395
|
+
const cancelRequestedAt = job.cancelRequestedAt ?? now;
|
|
396
|
+
const cancelledByUserId = job.cancelledByUserId ?? ctx.userId ?? null;
|
|
397
|
+
const finishedAt = job.finishedAt ?? now;
|
|
398
|
+
const affected = await em.nativeUpdate(ProgressJob, {
|
|
399
|
+
...jobScopeFilter(jobId, ctx),
|
|
400
|
+
status: { $in: CANCEL_FROM_STATUSES }
|
|
401
|
+
}, {
|
|
402
|
+
status: "cancelled",
|
|
403
|
+
cancelRequestedAt,
|
|
404
|
+
cancelledByUserId,
|
|
405
|
+
finishedAt,
|
|
406
|
+
etaSeconds: 0,
|
|
407
|
+
updatedAt: now
|
|
408
|
+
});
|
|
409
|
+
forgetJobThrottle(jobId);
|
|
410
|
+
if (affected === 0) {
|
|
411
|
+
const fresh = await loadFreshJob(jobId, ctx);
|
|
412
|
+
return fresh ?? job;
|
|
413
|
+
}
|
|
244
414
|
job.status = "cancelled";
|
|
245
|
-
job.
|
|
415
|
+
job.cancelRequestedAt = cancelRequestedAt;
|
|
416
|
+
job.cancelledByUserId = cancelledByUserId;
|
|
417
|
+
job.finishedAt = finishedAt;
|
|
246
418
|
job.etaSeconds = 0;
|
|
247
|
-
await em.flush();
|
|
248
419
|
await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {
|
|
249
420
|
...buildJobPayload(job),
|
|
250
421
|
tenantId: ctx.tenantId,
|
|
251
422
|
organizationId: job.organizationId ?? null
|
|
252
423
|
});
|
|
253
|
-
forgetJobThrottle(jobId);
|
|
254
424
|
return job;
|
|
255
425
|
},
|
|
256
426
|
async isCancellationRequested(jobId, tenantId, organizationId) {
|
|
@@ -258,7 +428,7 @@ function createProgressService(em, eventBus) {
|
|
|
258
428
|
id: jobId,
|
|
259
429
|
tenantId,
|
|
260
430
|
...organizationId ? { organizationId } : {}
|
|
261
|
-
});
|
|
431
|
+
}, { disableIdentityMap: true });
|
|
262
432
|
return job?.cancelRequestedAt != null;
|
|
263
433
|
},
|
|
264
434
|
async getActiveJobs(ctx) {
|
|
@@ -293,10 +463,14 @@ function createProgressService(em, eventBus) {
|
|
|
293
463
|
});
|
|
294
464
|
},
|
|
295
465
|
async markStaleJobsFailed(tenantId, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS, organizationId) {
|
|
296
|
-
const
|
|
297
|
-
const
|
|
466
|
+
const now = /* @__PURE__ */ new Date();
|
|
467
|
+
const cutoff = new Date(now.getTime() - timeoutSeconds * 1e3);
|
|
468
|
+
const scope = {
|
|
298
469
|
tenantId,
|
|
299
|
-
...organizationId ? { organizationId } : {}
|
|
470
|
+
...organizationId ? { organizationId } : {}
|
|
471
|
+
};
|
|
472
|
+
let failedCount = 0;
|
|
473
|
+
const staleFilter = {
|
|
300
474
|
status: "running",
|
|
301
475
|
$or: [
|
|
302
476
|
{ heartbeatAt: { $lt: cutoff } },
|
|
@@ -305,11 +479,56 @@ function createProgressService(em, eventBus) {
|
|
|
305
479
|
startedAt: { $lt: cutoff }
|
|
306
480
|
}
|
|
307
481
|
]
|
|
308
|
-
}
|
|
309
|
-
|
|
482
|
+
};
|
|
483
|
+
const staleRunning = await em.find(ProgressJob, { ...scope, ...staleFilter }, { disableIdentityMap: true });
|
|
484
|
+
for (const job of staleRunning) {
|
|
485
|
+
const errorMessage = `Job stale: no heartbeat for ${timeoutSeconds} seconds`;
|
|
486
|
+
const affected = await em.nativeUpdate(ProgressJob, {
|
|
487
|
+
id: job.id,
|
|
488
|
+
tenantId: job.tenantId,
|
|
489
|
+
...staleFilter
|
|
490
|
+
}, {
|
|
491
|
+
status: "failed",
|
|
492
|
+
finishedAt: now,
|
|
493
|
+
errorMessage,
|
|
494
|
+
updatedAt: now
|
|
495
|
+
});
|
|
496
|
+
if (affected === 0) continue;
|
|
497
|
+
failedCount += 1;
|
|
498
|
+
job.status = "failed";
|
|
499
|
+
job.finishedAt = now;
|
|
500
|
+
job.errorMessage = errorMessage;
|
|
501
|
+
await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {
|
|
502
|
+
...buildJobPayload(job),
|
|
503
|
+
errorMessage: job.errorMessage,
|
|
504
|
+
tenantId: job.tenantId,
|
|
505
|
+
stale: true,
|
|
506
|
+
organizationId: job.organizationId ?? null
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
const pendingCutoff = new Date(now.getTime() - STALE_PENDING_TIMEOUT_SECONDS * 1e3);
|
|
510
|
+
const stalePendingFilter = {
|
|
511
|
+
status: "pending",
|
|
512
|
+
createdAt: { $lt: pendingCutoff }
|
|
513
|
+
};
|
|
514
|
+
const stalePending = await em.find(ProgressJob, { ...scope, ...stalePendingFilter }, { disableIdentityMap: true });
|
|
515
|
+
for (const job of stalePending) {
|
|
516
|
+
const errorMessage = `Job stale: never started within ${STALE_PENDING_TIMEOUT_SECONDS} seconds`;
|
|
517
|
+
const affected = await em.nativeUpdate(ProgressJob, {
|
|
518
|
+
id: job.id,
|
|
519
|
+
tenantId: job.tenantId,
|
|
520
|
+
...stalePendingFilter
|
|
521
|
+
}, {
|
|
522
|
+
status: "failed",
|
|
523
|
+
finishedAt: now,
|
|
524
|
+
errorMessage,
|
|
525
|
+
updatedAt: now
|
|
526
|
+
});
|
|
527
|
+
if (affected === 0) continue;
|
|
528
|
+
failedCount += 1;
|
|
310
529
|
job.status = "failed";
|
|
311
|
-
job.finishedAt =
|
|
312
|
-
job.errorMessage =
|
|
530
|
+
job.finishedAt = now;
|
|
531
|
+
job.errorMessage = errorMessage;
|
|
313
532
|
await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {
|
|
314
533
|
...buildJobPayload(job),
|
|
315
534
|
errorMessage: job.errorMessage,
|
|
@@ -318,8 +537,7 @@ function createProgressService(em, eventBus) {
|
|
|
318
537
|
organizationId: job.organizationId ?? null
|
|
319
538
|
});
|
|
320
539
|
}
|
|
321
|
-
|
|
322
|
-
return staleJobs.length;
|
|
540
|
+
return failedCount;
|
|
323
541
|
}
|
|
324
542
|
};
|
|
325
543
|
}
|