@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.
Files changed (191) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/resources_resource/index.js +2 -0
  3. package/dist/generated/entities/resources_resource/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/helpers/integration/salesFixtures.js +31 -2
  7. package/dist/helpers/integration/salesFixtures.js.map +2 -2
  8. package/dist/helpers/integration/salesUi.js +5 -0
  9. package/dist/helpers/integration/salesUi.js.map +2 -2
  10. package/dist/modules/auth/api/profile/route.js +8 -2
  11. package/dist/modules/auth/api/profile/route.js.map +2 -2
  12. package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
  13. package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
  14. package/dist/modules/communication_channels/data/enrichers.js +4 -4
  15. package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
  16. package/dist/modules/customers/analytics.js +1 -0
  17. package/dist/modules/customers/analytics.js.map +2 -2
  18. package/dist/modules/customers/components/detail/DealForm.js +2 -1
  19. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  20. package/dist/modules/customers/components/detail/DealsSection.js +1 -1
  21. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  22. package/dist/modules/customers/data/validators.js +3 -1
  23. package/dist/modules/customers/data/validators.js.map +2 -2
  24. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +52 -2
  25. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +2 -2
  26. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +42 -26
  27. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
  28. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +2 -2
  29. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +2 -2
  30. package/dist/modules/data_sync/lib/queue-policy.js +15 -0
  31. package/dist/modules/data_sync/lib/queue-policy.js.map +7 -0
  32. package/dist/modules/data_sync/lib/queue.js +16 -1
  33. package/dist/modules/data_sync/lib/queue.js.map +2 -2
  34. package/dist/modules/data_sync/lib/start-run.js +2 -1
  35. package/dist/modules/data_sync/lib/start-run.js.map +2 -2
  36. package/dist/modules/data_sync/lib/sync-engine.js +35 -2
  37. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  38. package/dist/modules/data_sync/lib/sync-run-service.js +70 -2
  39. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  40. package/dist/modules/data_sync/workers/sync-export.js +9 -2
  41. package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
  42. package/dist/modules/data_sync/workers/sync-import.js +9 -2
  43. package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
  44. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +34 -12
  45. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
  46. package/dist/modules/dictionaries/api/openapi.js +15 -2
  47. package/dist/modules/dictionaries/api/openapi.js.map +2 -2
  48. package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js +30 -29
  49. package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js.map +2 -2
  50. package/dist/modules/dictionaries/data/validators.js +7 -0
  51. package/dist/modules/dictionaries/data/validators.js.map +2 -2
  52. package/dist/modules/dictionaries/lib/clientEntries.js +4 -3
  53. package/dist/modules/dictionaries/lib/clientEntries.js.map +2 -2
  54. package/dist/modules/dictionaries/lib/fetchAllEntries.js +43 -0
  55. package/dist/modules/dictionaries/lib/fetchAllEntries.js.map +7 -0
  56. package/dist/modules/entities/api/definitions.js +51 -3
  57. package/dist/modules/entities/api/definitions.js.map +2 -2
  58. package/dist/modules/entities/api/entities.js +21 -2
  59. package/dist/modules/entities/api/entities.js.map +2 -2
  60. package/dist/modules/entities/api/records.js +3 -24
  61. package/dist/modules/entities/api/records.js.map +2 -2
  62. package/dist/modules/entities/api/sidebar-entities.js +34 -12
  63. package/dist/modules/entities/api/sidebar-entities.js.map +2 -2
  64. package/dist/modules/entities/lib/entityAcl.js +46 -0
  65. package/dist/modules/entities/lib/entityAcl.js.map +2 -2
  66. package/dist/modules/messages/api/route.js +2 -5
  67. package/dist/modules/messages/api/route.js.map +2 -2
  68. package/dist/modules/messages/lib/participantScope.js +15 -0
  69. package/dist/modules/messages/lib/participantScope.js.map +7 -0
  70. package/dist/modules/notifications/api/route.js +3 -1
  71. package/dist/modules/notifications/api/route.js.map +2 -2
  72. package/dist/modules/notifications/api/unread-count/route.js +25 -6
  73. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  74. package/dist/modules/notifications/lib/notificationScope.js +26 -0
  75. package/dist/modules/notifications/lib/notificationScope.js.map +7 -0
  76. package/dist/modules/notifications/lib/notificationService.js +7 -3
  77. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  78. package/dist/modules/notifications/lib/routeHelpers.js +16 -2
  79. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  80. package/dist/modules/planner/components/unavailabilityReasons.js +3 -3
  81. package/dist/modules/planner/components/unavailabilityReasons.js.map +2 -2
  82. package/dist/modules/progress/lib/progressService.js +2 -0
  83. package/dist/modules/progress/lib/progressService.js.map +2 -2
  84. package/dist/modules/progress/lib/progressServiceImpl.js +322 -104
  85. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  86. package/dist/modules/resources/api/resources.js +2 -0
  87. package/dist/modules/resources/api/resources.js.map +2 -2
  88. package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -2
  89. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  90. package/dist/modules/resources/commands/resources.js +9 -0
  91. package/dist/modules/resources/commands/resources.js.map +2 -2
  92. package/dist/modules/resources/components/detail/dictionaries.js +3 -3
  93. package/dist/modules/resources/components/detail/dictionaries.js.map +2 -2
  94. package/dist/modules/resources/data/entities.js +3 -0
  95. package/dist/modules/resources/data/entities.js.map +2 -2
  96. package/dist/modules/resources/data/validators.js +6 -2
  97. package/dist/modules/resources/data/validators.js.map +2 -2
  98. package/dist/modules/resources/lib/seeds.js +16 -9
  99. package/dist/modules/resources/lib/seeds.js.map +2 -2
  100. package/dist/modules/resources/migrations/Migration20260608231000.js +13 -0
  101. package/dist/modules/resources/migrations/Migration20260608231000.js.map +7 -0
  102. package/dist/modules/sales/backend/sales/documents/create/page.js +11 -14
  103. package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
  104. package/dist/modules/sales/commands/documents.js +8 -0
  105. package/dist/modules/sales/commands/documents.js.map +2 -2
  106. package/dist/modules/sales/components/documents/ItemsSection.js +18 -9
  107. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  108. package/dist/modules/sales/components/documents/LineItemDialog.js +12 -5
  109. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  110. package/dist/modules/sales/components/documents/SalesDocumentForm.js +48 -2
  111. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  112. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +167 -0
  113. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +7 -0
  114. package/dist/modules/sales/data/validators.js +5 -1
  115. package/dist/modules/sales/data/validators.js.map +2 -2
  116. package/dist/modules/staff/components/detail/dictionaries.js +3 -3
  117. package/dist/modules/staff/components/detail/dictionaries.js.map +2 -2
  118. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +11 -4
  119. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
  120. package/generated/entities/resources_resource/index.ts +1 -0
  121. package/generated/entity-fields-registry.ts +1 -0
  122. package/package.json +7 -7
  123. package/src/helpers/integration/salesFixtures.ts +34 -2
  124. package/src/helpers/integration/salesUi.ts +8 -0
  125. package/src/modules/auth/api/profile/route.ts +8 -2
  126. package/src/modules/auth/backend/auth/profile/page.tsx +1 -0
  127. package/src/modules/auth/backend/profile/change-password/page.tsx +1 -0
  128. package/src/modules/communication_channels/data/enrichers.ts +6 -26
  129. package/src/modules/customers/analytics.ts +1 -0
  130. package/src/modules/customers/components/detail/DealForm.tsx +5 -1
  131. package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
  132. package/src/modules/customers/data/validators.ts +5 -1
  133. package/src/modules/dashboards/i18n/de.json +3 -0
  134. package/src/modules/dashboards/i18n/en.json +3 -0
  135. package/src/modules/dashboards/i18n/es.json +3 -0
  136. package/src/modules/dashboards/i18n/pl.json +3 -0
  137. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +86 -0
  138. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +33 -18
  139. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +2 -2
  140. package/src/modules/data_sync/lib/adapter.ts +18 -0
  141. package/src/modules/data_sync/lib/queue-policy.ts +30 -0
  142. package/src/modules/data_sync/lib/queue.ts +19 -1
  143. package/src/modules/data_sync/lib/start-run.ts +2 -1
  144. package/src/modules/data_sync/lib/sync-engine.ts +46 -0
  145. package/src/modules/data_sync/lib/sync-run-service.ts +82 -1
  146. package/src/modules/data_sync/workers/sync-export.ts +8 -1
  147. package/src/modules/data_sync/workers/sync-import.ts +8 -1
  148. package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +39 -11
  149. package/src/modules/dictionaries/api/openapi.ts +19 -0
  150. package/src/modules/dictionaries/components/hooks/useDictionaryEntries.ts +35 -36
  151. package/src/modules/dictionaries/data/validators.ts +14 -0
  152. package/src/modules/dictionaries/lib/clientEntries.ts +4 -3
  153. package/src/modules/dictionaries/lib/fetchAllEntries.ts +72 -0
  154. package/src/modules/entities/api/definitions.ts +51 -3
  155. package/src/modules/entities/api/entities.ts +30 -2
  156. package/src/modules/entities/api/records.ts +3 -27
  157. package/src/modules/entities/api/sidebar-entities.ts +43 -14
  158. package/src/modules/entities/lib/entityAcl.ts +58 -0
  159. package/src/modules/messages/api/route.ts +4 -5
  160. package/src/modules/messages/lib/participantScope.ts +64 -0
  161. package/src/modules/notifications/api/route.ts +2 -0
  162. package/src/modules/notifications/api/unread-count/route.ts +42 -5
  163. package/src/modules/notifications/lib/notificationScope.ts +40 -0
  164. package/src/modules/notifications/lib/notificationService.ts +5 -0
  165. package/src/modules/notifications/lib/routeHelpers.ts +19 -2
  166. package/src/modules/planner/components/unavailabilityReasons.ts +3 -3
  167. package/src/modules/progress/AGENTS.md +11 -1
  168. package/src/modules/progress/lib/progressService.ts +1 -0
  169. package/src/modules/progress/lib/progressServiceImpl.ts +385 -117
  170. package/src/modules/resources/api/resources.ts +2 -0
  171. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +9 -3
  172. package/src/modules/resources/commands/resources.ts +10 -0
  173. package/src/modules/resources/components/detail/dictionaries.ts +3 -3
  174. package/src/modules/resources/data/entities.ts +3 -0
  175. package/src/modules/resources/data/validators.ts +5 -0
  176. package/src/modules/resources/lib/seeds.ts +16 -9
  177. package/src/modules/resources/migrations/.snapshot-open-mercato.json +17 -1
  178. package/src/modules/resources/migrations/Migration20260608231000.ts +13 -0
  179. package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -4
  180. package/src/modules/sales/commands/documents.ts +8 -0
  181. package/src/modules/sales/components/documents/ItemsSection.tsx +19 -10
  182. package/src/modules/sales/components/documents/LineItemDialog.tsx +13 -5
  183. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +48 -6
  184. package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +198 -0
  185. package/src/modules/sales/data/validators.ts +7 -1
  186. package/src/modules/sales/i18n/de.json +2 -0
  187. package/src/modules/sales/i18n/en.json +2 -0
  188. package/src/modules/sales/i18n/es.json +2 -0
  189. package/src/modules/sales/i18n/pl.json +2 -0
  190. package/src/modules/staff/components/detail/dictionaries.ts +3 -3
  191. 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 { calculateEta, calculateProgressPercent, STALE_JOB_TIMEOUT_SECONDS } from "./progressService.js";
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 raw = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS;
8
- if (raw == null || raw.trim() === "") return DEFAULT_BROADCAST_MIN_INTERVAL_MS;
9
- const parsed = Number.parseInt(raw, 10);
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 loadUpdatableJob(jobId, ctx) {
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.job;
43
- return em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx));
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
- async function commitJobUpdate(job, ctx) {
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 cached = jobUpdateThrottle.get(job.id);
48
- const shouldBroadcast = broadcastMinIntervalMs <= 0 || cached == null || now - cached.lastBroadcastAt >= broadcastMinIntervalMs || Math.abs(job.progressPercent - cached.lastBroadcastPercent) >= 1;
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(job),
125
+ ...buildJobPayload(persistedJob),
53
126
  tenantId: ctx.tenantId,
54
- organizationId: job.organizationId ?? null
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 job;
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
- id: jobId,
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 = /* @__PURE__ */ new Date();
105
- job.heartbeatAt = /* @__PURE__ */ new Date();
106
- await em.flush();
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 job = await loadUpdatableJob(jobId, ctx);
116
- if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
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 commitJobUpdate(job, ctx);
223
+ return persistAndMaybeBroadcast(entry, ctx);
141
224
  },
142
225
  async incrementProgress(jobId, delta, ctx) {
143
- const job = await loadUpdatableJob(jobId, ctx);
144
- if (job.status === "completed" || job.status === "failed" || job.status === "cancelled") {
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 commitJobUpdate(job, ctx);
241
+ return persistAndMaybeBroadcast(entry, ctx);
157
242
  },
158
243
  async completeJob(jobId, input, ctx) {
159
- const job = await em.findOne(ProgressJob, {
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
- job.status = "completed";
169
- job.finishedAt = /* @__PURE__ */ new Date();
170
- job.progressPercent = 100;
171
- job.etaSeconds = 0;
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
- job.resultSummary = input.resultSummary;
280
+ snapshot.resultSummary = input.resultSummary;
174
281
  }
175
- await em.flush();
282
+ const persistedJob = await loadFreshJob(jobId, ctx) ?? snapshot;
176
283
  await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {
177
- ...buildJobPayload(job),
178
- resultSummary: job.resultSummary,
284
+ ...buildJobPayload(persistedJob),
285
+ resultSummary: persistedJob.resultSummary,
179
286
  tenantId: ctx.tenantId,
180
- organizationId: job.organizationId ?? null
287
+ organizationId: persistedJob.organizationId ?? null
181
288
  });
182
- forgetJobThrottle(jobId);
183
- return job;
289
+ return persistedJob;
184
290
  },
185
291
  async failJob(jobId, input, ctx) {
186
- const job = await em.findOne(ProgressJob, {
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 === "cancelled") {
294
+ if (TERMINAL_STATUSES.includes(job.status)) {
295
+ forgetJobThrottle(jobId);
193
296
  return job;
194
297
  }
195
- job.status = "failed";
196
- job.finishedAt = /* @__PURE__ */ new Date();
197
- job.errorMessage = input.errorMessage;
198
- job.errorStack = input.errorStack;
199
- await em.flush();
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(job),
202
- errorMessage: job.errorMessage,
328
+ ...buildJobPayload(persistedJob),
329
+ errorMessage: persistedJob.errorMessage,
203
330
  tenantId: ctx.tenantId,
204
- organizationId: job.organizationId ?? null
331
+ organizationId: persistedJob.organizationId ?? null
205
332
  });
206
- forgetJobThrottle(jobId);
207
- return job;
333
+ return persistedJob;
208
334
  },
209
335
  async cancelJob(jobId, ctx) {
210
- const job = await em.findOneOrFail(ProgressJob, {
211
- id: jobId,
212
- tenantId: ctx.tenantId,
213
- ...ctx.organizationId ? { organizationId: ctx.organizationId } : {},
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: { $in: ["pending", "running"] }
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
- job.cancelRequestedAt = /* @__PURE__ */ new Date();
218
- job.cancelledByUserId = ctx.userId;
219
- if (job.status === "pending") {
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 = /* @__PURE__ */ new Date();
375
+ job.finishedAt = now;
376
+ job.etaSeconds = 0;
222
377
  }
223
- await em.flush();
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 em.findOne(ProgressJob, {
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
- job.cancelRequestedAt = job.cancelRequestedAt ?? /* @__PURE__ */ new Date();
243
- job.cancelledByUserId = ctx.userId;
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.finishedAt = job.finishedAt ?? /* @__PURE__ */ new Date();
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 cutoff = new Date(Date.now() - timeoutSeconds * 1e3);
297
- const staleJobs = await em.find(ProgressJob, {
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
- for (const job of staleJobs) {
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 = /* @__PURE__ */ new Date();
312
- job.errorMessage = `Job stale: no heartbeat for ${timeoutSeconds} seconds`;
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
- await em.flush();
322
- return staleJobs.length;
540
+ return failedCount;
323
541
  }
324
542
  };
325
543
  }