@open-mercato/core 0.6.8-develop.6986.1.3adb0d0df6 → 0.6.8-develop.6992.1.00c90fecff

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 (50) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/components/detail/AssignRoleDialog.js +9 -3
  3. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  4. package/dist/modules/customers/components/detail/DealsSection.js +2 -3
  5. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  6. package/dist/modules/customers/components/detail/assignableStaff.js +2 -1
  7. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  8. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +11 -12
  9. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
  10. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +7 -4
  11. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
  12. package/dist/modules/data_sync/api/run.js +9 -1
  13. package/dist/modules/data_sync/api/run.js.map +2 -2
  14. package/dist/modules/data_sync/api/runs/[id]/retry.js +9 -1
  15. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  16. package/dist/modules/data_sync/lib/adapter-registry.js +10 -1
  17. package/dist/modules/data_sync/lib/adapter-registry.js.map +2 -2
  18. package/dist/modules/data_sync/lib/start-cursor.js +17 -0
  19. package/dist/modules/data_sync/lib/start-cursor.js.map +7 -0
  20. package/dist/modules/data_sync/lib/sync-engine.js +84 -27
  21. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  22. package/dist/modules/data_sync/lib/sync-run-service.js +86 -10
  23. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  24. package/dist/modules/data_sync/workers/sync-scheduled.js +9 -6
  25. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  26. package/dist/modules/progress/lib/progressService.js +2 -0
  27. package/dist/modules/progress/lib/progressService.js.map +2 -2
  28. package/dist/modules/progress/lib/progressServiceImpl.js +78 -34
  29. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  30. package/dist/modules/sales/api/channels/route.js +1 -1
  31. package/dist/modules/sales/api/channels/route.js.map +2 -2
  32. package/package.json +7 -7
  33. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +12 -3
  34. package/src/modules/customers/components/detail/DealsSection.tsx +6 -11
  35. package/src/modules/customers/components/detail/assignableStaff.ts +9 -1
  36. package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +16 -13
  37. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +11 -4
  38. package/src/modules/data_sync/AGENTS.md +6 -1
  39. package/src/modules/data_sync/api/run.ts +9 -1
  40. package/src/modules/data_sync/api/runs/[id]/retry.ts +9 -1
  41. package/src/modules/data_sync/lib/adapter-registry.ts +15 -0
  42. package/src/modules/data_sync/lib/adapter.ts +18 -0
  43. package/src/modules/data_sync/lib/start-cursor.ts +35 -0
  44. package/src/modules/data_sync/lib/sync-engine.ts +101 -27
  45. package/src/modules/data_sync/lib/sync-run-service.ts +118 -10
  46. package/src/modules/data_sync/workers/sync-scheduled.ts +9 -6
  47. package/src/modules/progress/AGENTS.md +2 -1
  48. package/src/modules/progress/lib/progressService.ts +9 -0
  49. package/src/modules/progress/lib/progressServiceImpl.ts +111 -37
  50. package/src/modules/sales/api/channels/route.ts +1 -1
@@ -5,7 +5,8 @@ import {
5
5
  calculateProgressPercent,
6
6
  HEARTBEAT_INTERVAL_MS,
7
7
  STALE_JOB_TIMEOUT_SECONDS,
8
- STALE_PENDING_TIMEOUT_SECONDS
8
+ STALE_PENDING_TIMEOUT_SECONDS,
9
+ STALE_SWEEP_ERROR_PREFIX
9
10
  } from "./progressService.js";
10
11
  import { PROGRESS_EVENTS } from "./events.js";
11
12
  import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
@@ -90,6 +91,37 @@ function createProgressService(em, eventBus) {
90
91
  }
91
92
  return {};
92
93
  }
94
+ async function startJobViaCas(targetEm, job, ctx, options = {}) {
95
+ const now = /* @__PURE__ */ new Date();
96
+ const startedAt = job.startedAt ?? now;
97
+ const filter = {
98
+ ...jobScopeFilter(job.id, ctx),
99
+ status: { $in: START_FROM_STATUSES }
100
+ };
101
+ if (options.staleSweptOnly) filter.errorMessage = { $like: `${STALE_SWEEP_ERROR_PREFIX}%` };
102
+ const affected = await targetEm.nativeUpdate(ProgressJob, filter, {
103
+ status: "running",
104
+ startedAt,
105
+ heartbeatAt: now,
106
+ finishedAt: null,
107
+ errorMessage: null,
108
+ errorStack: null,
109
+ updatedAt: now
110
+ });
111
+ if (affected === 0) return null;
112
+ job.status = "running";
113
+ job.startedAt = startedAt;
114
+ job.heartbeatAt = now;
115
+ job.finishedAt = null;
116
+ job.errorMessage = null;
117
+ job.errorStack = null;
118
+ await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {
119
+ ...buildJobPayload(job),
120
+ tenantId: ctx.tenantId,
121
+ organizationId: job.organizationId ?? null
122
+ });
123
+ return job;
124
+ }
93
125
  async function persistAndMaybeBroadcast(entry, ctx) {
94
126
  const job = entry.job;
95
127
  const now = Date.now();
@@ -106,14 +138,28 @@ function createProgressService(em, eventBus) {
106
138
  updatedAt: /* @__PURE__ */ new Date(),
107
139
  ...buildBufferedCountData(entry)
108
140
  };
109
- const affected = await em.nativeUpdate(ProgressJob, {
141
+ const writableFilter = {
110
142
  ...jobScopeFilter(job.id, ctx),
111
143
  status: { $in: PROGRESS_WRITABLE_STATUSES }
112
- }, data);
144
+ };
145
+ let affected = await em.nativeUpdate(ProgressJob, writableFilter, data);
113
146
  if (affected === 0) {
114
- forgetJobThrottle(job.id);
115
147
  const fresh = await loadFreshJob(job.id, ctx);
116
- return fresh ?? job;
148
+ let revived = false;
149
+ if (fresh && fresh.status === "failed" && await startJobViaCas(em, fresh, ctx, { staleSweptOnly: true })) {
150
+ revived = true;
151
+ job.status = "running";
152
+ job.startedAt = fresh.startedAt;
153
+ job.finishedAt = null;
154
+ job.errorMessage = null;
155
+ job.errorStack = null;
156
+ affected = await em.nativeUpdate(ProgressJob, writableFilter, data);
157
+ }
158
+ if (affected === 0) {
159
+ forgetJobThrottle(job.id);
160
+ const latest = revived ? await loadFreshJob(job.id, ctx) : fresh;
161
+ return latest ?? job;
162
+ }
117
163
  }
118
164
  const persistedJob = usesAtomicIncrement ? await loadFreshJob(job.id, ctx) ?? job : job;
119
165
  entry.job = persistedJob;
@@ -161,42 +207,36 @@ function createProgressService(em, eventBus) {
161
207
  if (job.status === "running" || job.status === "cancelled" || job.status === "completed") {
162
208
  return job;
163
209
  }
210
+ const started = await startJobViaCas(em, job, ctx);
211
+ if (started) return started;
212
+ const fresh = await loadFreshJob(jobId, ctx);
213
+ return fresh ?? job;
214
+ },
215
+ async touchJobHeartbeat(jobId, ctx) {
216
+ const fork = em.fork();
164
217
  const now = /* @__PURE__ */ new Date();
165
- const affected = await em.nativeUpdate(ProgressJob, {
218
+ const affected = await fork.nativeUpdate(ProgressJob, {
166
219
  ...jobScopeFilter(jobId, ctx),
167
- status: { $in: START_FROM_STATUSES }
220
+ status: { $in: PROGRESS_WRITABLE_STATUSES }
168
221
  }, {
169
- status: "running",
170
- startedAt: now,
171
222
  heartbeatAt: now,
172
- finishedAt: null,
173
- errorMessage: null,
174
- errorStack: null,
175
223
  updatedAt: now
176
224
  });
177
- if (affected === 0) {
178
- const fresh = await loadFreshJob(jobId, ctx);
179
- return fresh ?? job;
180
- }
181
- job.status = "running";
182
- job.startedAt = now;
183
- job.heartbeatAt = now;
184
- job.finishedAt = null;
185
- job.errorMessage = null;
186
- job.errorStack = null;
187
- await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {
188
- ...buildJobPayload(job),
189
- tenantId: ctx.tenantId,
190
- organizationId: job.organizationId ?? null
191
- });
192
- return job;
225
+ if (affected > 0) return;
226
+ const job = await fork.findOne(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true });
227
+ if (!job || job.status !== "failed") return;
228
+ await startJobViaCas(fork, job, ctx, { staleSweptOnly: true });
193
229
  },
194
230
  async updateProgress(jobId, input, ctx) {
195
231
  const entry = await ensureThrottleEntry(jobId, ctx);
196
232
  const job = entry.job;
197
233
  if (TERMINAL_STATUSES.includes(job.status)) {
198
- forgetJobThrottle(jobId);
199
- return job;
234
+ const revived = job.status === "failed" && await startJobViaCas(em, job, ctx, { staleSweptOnly: true });
235
+ if (!revived) {
236
+ forgetJobThrottle(jobId);
237
+ return job;
238
+ }
239
+ entry.lastPersistedAt = 0;
200
240
  }
201
241
  if (input.processedCount !== void 0) {
202
242
  job.processedCount = input.processedCount;
@@ -226,8 +266,12 @@ function createProgressService(em, eventBus) {
226
266
  const entry = await ensureThrottleEntry(jobId, ctx);
227
267
  const job = entry.job;
228
268
  if (TERMINAL_STATUSES.includes(job.status)) {
229
- forgetJobThrottle(jobId);
230
- return job;
269
+ const revived = job.status === "failed" && await startJobViaCas(em, job, ctx, { staleSweptOnly: true });
270
+ if (!revived) {
271
+ forgetJobThrottle(jobId);
272
+ return job;
273
+ }
274
+ entry.lastPersistedAt = 0;
231
275
  }
232
276
  job.processedCount += delta;
233
277
  entry.pendingDelta += delta;
@@ -486,7 +530,7 @@ function createProgressService(em, eventBus) {
486
530
  };
487
531
  const staleRunning = await em.find(ProgressJob, { ...scope, ...staleFilter }, { disableIdentityMap: true });
488
532
  for (const job of staleRunning) {
489
- const errorMessage = `Job stale: no heartbeat for ${timeoutSeconds} seconds`;
533
+ const errorMessage = `${STALE_SWEEP_ERROR_PREFIX} no heartbeat for ${timeoutSeconds} seconds`;
490
534
  const affected = await em.nativeUpdate(ProgressJob, {
491
535
  id: job.id,
492
536
  tenantId: job.tenantId,
@@ -517,7 +561,7 @@ function createProgressService(em, eventBus) {
517
561
  };
518
562
  const stalePending = await em.find(ProgressJob, { ...scope, ...stalePendingFilter }, { disableIdentityMap: true });
519
563
  for (const job of stalePending) {
520
- const errorMessage = `Job stale: never started within ${STALE_PENDING_TIMEOUT_SECONDS} seconds`;
564
+ const errorMessage = `${STALE_SWEEP_ERROR_PREFIX} never started within ${STALE_PENDING_TIMEOUT_SECONDS} seconds`;
521
565
  const affected = await em.nativeUpdate(ProgressJob, {
522
566
  id: job.id,
523
567
  tenantId: job.tenantId,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/progress/lib/progressServiceImpl.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { raw } from '@mikro-orm/core'\nimport type { EntityData, FilterQuery } from '@mikro-orm/core'\nimport { ProgressJob, type ProgressJobStatus } from '../data/entities'\nimport type { ProgressService, ProgressServiceContext } from './progressService'\nimport {\n calculateEta,\n calculateProgressPercent,\n HEARTBEAT_INTERVAL_MS,\n STALE_JOB_TIMEOUT_SECONDS,\n STALE_PENDING_TIMEOUT_SECONDS,\n} from './progressService'\nimport { PROGRESS_EVENTS } from './events'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst DEFAULT_BROADCAST_MIN_INTERVAL_MS = 250\n\n// Minimum elapsed time between coalesced `progress.job.updated` broadcasts for a\n// single job. Bulk workers call updateProgress/incrementProgress once per record, and\n// every emit of the `clientBroadcast: true` event pays a serialized pg_notify roundtrip\n// plus a tenant-wide SSE fan-out. Setting the knob to 0 restores per-record emission.\n// Persistence is throttled separately: heartbeats hit the database at least every\n// HEARTBEAT_INTERVAL_MS regardless of this knob, so a high broadcast interval can\n// never starve the stale-job sweep of heartbeats.\nfunction resolveBroadcastMinIntervalMs(): number {\n const rawValue = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS\n if (rawValue == null || rawValue.trim() === '') return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n const parsed = Number.parseInt(rawValue, 10)\n if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n return parsed\n}\n\n// Statuses a given transition is allowed to move FROM. Every write below is a\n// compare-and-swap (`nativeUpdate` guarded on status), so concurrent writers in other\n// processes can never resurrect a terminal job or double-apply a transition \u2014 the\n// UPDATE that matches zero rows simply lost the race and must not emit events.\nconst TERMINAL_STATUSES: readonly ProgressJobStatus[] = ['completed', 'failed', 'cancelled']\nconst PROGRESS_WRITABLE_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running']\n// `failed` is restartable/completable so a job wrongly swept as stale (worker alive but\n// slow) or retried by an at-least-once queue converges to its true outcome.\nconst START_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'failed']\nconst COMPLETE_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running', 'failed']\nconst FAIL_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running']\nconst CANCEL_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running', 'failed']\n\ntype JobUpdateThrottleEntry = {\n job: ProgressJob\n lastBroadcastAt: number\n lastPersistedAt: number\n lastBroadcastPercent: number\n pendingDelta: number\n absoluteCountsPending: boolean\n}\n\nfunction buildJobPayload(job: ProgressJob): Record<string, unknown> {\n return {\n jobId: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description ?? null,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n cancellable: job.cancellable,\n meta: job.meta ?? null,\n startedAt: job.startedAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n }\n}\n\nexport function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {\n const broadcastMinIntervalMs = resolveBroadcastMinIntervalMs()\n // Per-job coalescing state, scoped to this service instance (request/worker scope).\n // The cached entity is a DETACHED snapshot (loaded with disableIdentityMap) that doubles\n // as the in-memory buffer: intermediate updates mutate it without touching the shared\n // identity map or UnitOfWork, so an unrelated em.flush() elsewhere can never write the\n // buffered values. Persistence happens exclusively through the CAS nativeUpdate below.\n const jobUpdateThrottle = new Map<string, JobUpdateThrottleEntry>()\n\n function jobScopeFilter(jobId: string, ctx: ProgressServiceContext) {\n return {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n }\n\n async function loadFreshJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob | null> {\n return em.findOne(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n }\n\n async function ensureThrottleEntry(jobId: string, ctx: ProgressServiceContext): Promise<JobUpdateThrottleEntry> {\n const cached = jobUpdateThrottle.get(jobId)\n if (cached) return cached\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n const entry: JobUpdateThrottleEntry = {\n job,\n lastBroadcastAt: 0,\n lastPersistedAt: 0,\n lastBroadcastPercent: job.progressPercent,\n pendingDelta: 0,\n absoluteCountsPending: false,\n }\n jobUpdateThrottle.set(jobId, entry)\n return entry\n }\n\n function forgetJobThrottle(jobId: string) {\n jobUpdateThrottle.delete(jobId)\n }\n\n function buildBufferedCountData(entry: JobUpdateThrottleEntry): EntityData<ProgressJob> {\n if (entry.absoluteCountsPending || !Number.isSafeInteger(entry.pendingDelta)) {\n return { processedCount: entry.job.processedCount }\n }\n if (entry.pendingDelta !== 0) {\n const processedCount = raw(`processed_count + ${entry.pendingDelta}`)\n const totalCount = entry.job.totalCount\n if (Number.isSafeInteger(totalCount) && totalCount != null && totalCount > 0) {\n return {\n processedCount,\n progressPercent: raw(\n `least(100, round(((processed_count + ${entry.pendingDelta})::numeric / ${totalCount}) * 100))`,\n ),\n }\n }\n return { processedCount }\n }\n return {}\n }\n\n async function persistAndMaybeBroadcast(entry: JobUpdateThrottleEntry, ctx: ProgressServiceContext): Promise<ProgressJob> {\n const job = entry.job\n const now = Date.now()\n const shouldBroadcast =\n broadcastMinIntervalMs <= 0 ||\n now - entry.lastBroadcastAt >= broadcastMinIntervalMs ||\n Math.abs(job.progressPercent - entry.lastBroadcastPercent) >= 1\n const shouldPersist = shouldBroadcast || now - entry.lastPersistedAt >= HEARTBEAT_INTERVAL_MS\n\n if (!shouldPersist) return job\n\n const usesAtomicIncrement =\n !entry.absoluteCountsPending && Number.isSafeInteger(entry.pendingDelta) && entry.pendingDelta !== 0\n const data: EntityData<ProgressJob> = {\n progressPercent: job.progressPercent,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n meta: job.meta ?? null,\n heartbeatAt: job.heartbeatAt ?? new Date(),\n updatedAt: new Date(),\n ...buildBufferedCountData(entry),\n }\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(job.id, ctx),\n status: { $in: PROGRESS_WRITABLE_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, data)\n\n if (affected === 0) {\n // The job reached a terminal state in another process; stop writing to it.\n forgetJobThrottle(job.id)\n const fresh = await loadFreshJob(job.id, ctx)\n return fresh ?? job\n }\n\n const persistedJob = usesAtomicIncrement ? (await loadFreshJob(job.id, ctx)) ?? job : job\n entry.job = persistedJob\n entry.pendingDelta = 0\n entry.absoluteCountsPending = false\n entry.lastPersistedAt = now\n\n if (shouldBroadcast) {\n await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {\n ...buildJobPayload(persistedJob),\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n entry.lastBroadcastAt = now\n entry.lastBroadcastPercent = persistedJob.progressPercent\n }\n\n return persistedJob\n }\n\n return {\n async createJob(input, ctx) {\n const job = em.create(ProgressJob, {\n jobType: input.jobType,\n name: input.name,\n description: input.description,\n totalCount: input.totalCount,\n cancellable: input.cancellable ?? false,\n meta: input.meta,\n parentJobId: input.parentJobId,\n partitionIndex: input.partitionIndex,\n partitionCount: input.partitionCount,\n startedByUserId: ctx.userId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: 'pending',\n })\n\n await em.persist(job).flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CREATED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n\n return job\n },\n\n async startJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (job.status === 'running' || job.status === 'cancelled' || job.status === 'completed') {\n return job\n }\n\n const now = new Date()\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: START_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, {\n status: 'running',\n startedAt: now,\n heartbeatAt: now,\n finishedAt: null,\n errorMessage: null,\n errorStack: null,\n updatedAt: now,\n })\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n job.status = 'running'\n job.startedAt = now\n job.heartbeatAt = now\n job.finishedAt = null\n job.errorMessage = null\n job.errorStack = null\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async updateProgress(jobId, input, ctx) {\n const entry = await ensureThrottleEntry(jobId, ctx)\n const job = entry.job\n if (TERMINAL_STATUSES.includes(job.status)) {\n forgetJobThrottle(jobId)\n return job\n }\n\n if (input.processedCount !== undefined) {\n job.processedCount = input.processedCount\n entry.absoluteCountsPending = true\n entry.pendingDelta = 0\n }\n if (input.totalCount !== undefined) {\n job.totalCount = input.totalCount\n }\n if (input.meta !== undefined) {\n job.meta = { ...job.meta, ...input.meta }\n }\n\n if (input.progressPercent !== undefined) {\n job.progressPercent = input.progressPercent\n } else if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n }\n\n if (input.etaSeconds !== undefined) {\n job.etaSeconds = input.etaSeconds\n } else if (job.startedAt && job.totalCount) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n\n job.heartbeatAt = new Date()\n\n return persistAndMaybeBroadcast(entry, ctx)\n },\n\n async incrementProgress(jobId, delta, ctx) {\n const entry = await ensureThrottleEntry(jobId, ctx)\n const job = entry.job\n if (TERMINAL_STATUSES.includes(job.status)) {\n forgetJobThrottle(jobId)\n return job\n }\n\n job.processedCount += delta\n entry.pendingDelta += delta\n job.heartbeatAt = new Date()\n\n if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n if (job.startedAt) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n }\n\n return persistAndMaybeBroadcast(entry, ctx)\n },\n\n async completeJob(jobId, input, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled' || job.status === 'completed') {\n forgetJobThrottle(jobId)\n return job\n }\n\n const entry = jobUpdateThrottle.get(jobId)\n const snapshot = entry?.job ?? job\n const now = new Date()\n const data: EntityData<ProgressJob> = {\n status: 'completed',\n finishedAt: now,\n etaSeconds: 0,\n updatedAt: now,\n ...(input?.resultSummary ? { resultSummary: input.resultSummary } : {}),\n ...(entry\n ? {\n totalCount: snapshot.totalCount ?? null,\n meta: snapshot.meta ?? null,\n ...buildBufferedCountData(entry),\n }\n : {}),\n progressPercent: 100,\n }\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: COMPLETE_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, data)\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n snapshot.status = 'completed'\n snapshot.finishedAt = now\n snapshot.progressPercent = 100\n snapshot.etaSeconds = 0\n if (input?.resultSummary) {\n snapshot.resultSummary = input.resultSummary\n }\n\n const persistedJob = (await loadFreshJob(jobId, ctx)) ?? snapshot\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {\n ...buildJobPayload(persistedJob),\n resultSummary: persistedJob.resultSummary,\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n\n return persistedJob\n },\n\n async failJob(jobId, input, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (TERMINAL_STATUSES.includes(job.status)) {\n forgetJobThrottle(jobId)\n return job\n }\n\n const entry = jobUpdateThrottle.get(jobId)\n const snapshot = entry?.job ?? job\n const now = new Date()\n const data: EntityData<ProgressJob> = {\n status: 'failed',\n finishedAt: now,\n errorMessage: input.errorMessage,\n errorStack: input.errorStack,\n ...(input.resultSummary ? { resultSummary: input.resultSummary } : {}),\n updatedAt: now,\n ...(entry\n ? {\n totalCount: snapshot.totalCount ?? null,\n meta: snapshot.meta ?? null,\n ...buildBufferedCountData(entry),\n }\n : {}),\n }\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: FAIL_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, data)\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n snapshot.status = 'failed'\n snapshot.finishedAt = now\n snapshot.errorMessage = input.errorMessage\n snapshot.errorStack = input.errorStack\n if (input.resultSummary) {\n snapshot.resultSummary = input.resultSummary\n }\n\n const persistedJob = (await loadFreshJob(jobId, ctx)) ?? snapshot\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(persistedJob),\n errorMessage: persistedJob.errorMessage,\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n\n return persistedJob\n },\n\n async cancelJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (TERMINAL_STATUSES.includes(job.status)) {\n // The job finished while the user clicked cancel \u2014 a benign race, not an error.\n return job\n }\n if (!job.cancellable) {\n throw new Error(`Job ${jobId} is not cancellable`)\n }\n\n const now = new Date()\n const cancelledNow = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n cancellable: true,\n status: 'pending',\n } as FilterQuery<ProgressJob>, {\n status: 'cancelled',\n cancelRequestedAt: now,\n cancelledByUserId: ctx.userId ?? null,\n finishedAt: now,\n etaSeconds: 0,\n updatedAt: now,\n })\n\n let requested = 0\n if (cancelledNow === 0) {\n requested = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n cancellable: true,\n status: 'running',\n } as FilterQuery<ProgressJob>, {\n cancelRequestedAt: now,\n cancelledByUserId: ctx.userId ?? null,\n updatedAt: now,\n })\n }\n\n if (cancelledNow === 0 && requested === 0) {\n // Raced into a terminal state between the read and the CAS.\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n forgetJobThrottle(jobId)\n if (cancelledNow > 0) {\n job.status = 'cancelled'\n job.finishedAt = now\n job.etaSeconds = 0\n }\n job.cancelRequestedAt = now\n job.cancelledByUserId = ctx.userId ?? null\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async markCancelled(jobId, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled' || job.status === 'completed') {\n forgetJobThrottle(jobId)\n return job\n }\n\n const now = new Date()\n const cancelRequestedAt = job.cancelRequestedAt ?? now\n const cancelledByUserId = job.cancelledByUserId ?? ctx.userId ?? null\n const finishedAt = job.finishedAt ?? now\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: CANCEL_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, {\n status: 'cancelled',\n cancelRequestedAt,\n cancelledByUserId,\n finishedAt,\n etaSeconds: 0,\n updatedAt: now,\n })\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n job.status = 'cancelled'\n job.cancelRequestedAt = cancelRequestedAt\n job.cancelledByUserId = cancelledByUserId\n job.finishedAt = finishedAt\n job.etaSeconds = 0\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async isCancellationRequested(jobId, tenantId, organizationId) {\n // disableIdentityMap forces a fresh read: a managed copy of the job in this\n // EntityManager (loaded by updateProgress) must never mask a cancellation\n // requested from another process.\n const job = await findOneWithDecryption(em, ProgressJob, {\n id: jobId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n }, { disableIdentityMap: true })\n return job?.cancelRequestedAt != null\n },\n\n async getActiveJobs(ctx) {\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['pending', 'running'] },\n parentJobId: null,\n }, {\n orderBy: { createdAt: 'DESC' },\n limit: 50,\n })\n },\n\n async getRecentlyCompletedJobs(ctx, sinceSeconds = 30) {\n const cutoff = new Date(Date.now() - sinceSeconds * 1000)\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['completed', 'failed'] },\n finishedAt: { $gte: cutoff },\n parentJobId: null,\n }, {\n orderBy: { finishedAt: 'DESC' },\n limit: 10,\n })\n },\n\n async getJob(jobId, ctx) {\n return em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n },\n\n async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS, organizationId?: string | null) {\n const now = new Date()\n const cutoff = new Date(now.getTime() - timeoutSeconds * 1000)\n const scope = {\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n }\n let failedCount = 0\n\n const staleFilter = {\n status: 'running' as ProgressJobStatus,\n $or: [\n { heartbeatAt: { $lt: cutoff } },\n {\n heartbeatAt: null,\n startedAt: { $lt: cutoff },\n },\n ],\n }\n const staleRunning = await em.find(ProgressJob, { ...scope, ...staleFilter }, { disableIdentityMap: true })\n\n for (const job of staleRunning) {\n const errorMessage = `Job stale: no heartbeat for ${timeoutSeconds} seconds`\n // Per-row CAS (re-checking the staleness condition): exactly one concurrent\n // sweeper wins, and a heartbeat that landed after the SELECT aborts the failure.\n const affected = await em.nativeUpdate(ProgressJob, {\n id: job.id,\n tenantId: job.tenantId,\n ...staleFilter,\n } as FilterQuery<ProgressJob>, {\n status: 'failed',\n finishedAt: now,\n errorMessage,\n updatedAt: now,\n })\n if (affected === 0) continue\n\n failedCount += 1\n job.status = 'failed'\n job.finishedAt = now\n job.errorMessage = errorMessage\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n // Jobs stuck in `pending` (enqueue failed, worker died before startJob) have no\n // heartbeat, so they need their own sweep or they stay in the active list forever.\n // A late queue delivery still recovers such a job: startJob transitions failed \u2192 running.\n const pendingCutoff = new Date(now.getTime() - STALE_PENDING_TIMEOUT_SECONDS * 1000)\n const stalePendingFilter = {\n status: 'pending' as ProgressJobStatus,\n createdAt: { $lt: pendingCutoff },\n }\n const stalePending = await em.find(ProgressJob, { ...scope, ...stalePendingFilter }, { disableIdentityMap: true })\n\n for (const job of stalePending) {\n const errorMessage = `Job stale: never started within ${STALE_PENDING_TIMEOUT_SECONDS} seconds`\n const affected = await em.nativeUpdate(ProgressJob, {\n id: job.id,\n tenantId: job.tenantId,\n ...stalePendingFilter,\n } as FilterQuery<ProgressJob>, {\n status: 'failed',\n finishedAt: now,\n errorMessage,\n updatedAt: now,\n })\n if (affected === 0) continue\n\n failedCount += 1\n job.status = 'failed'\n job.finishedAt = now\n job.errorMessage = errorMessage\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n return failedCount\n },\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { raw } from '@mikro-orm/core'\nimport type { EntityData, FilterQuery } from '@mikro-orm/core'\nimport { ProgressJob, type ProgressJobStatus } from '../data/entities'\nimport type { ProgressService, ProgressServiceContext } from './progressService'\nimport {\n calculateEta,\n calculateProgressPercent,\n HEARTBEAT_INTERVAL_MS,\n STALE_JOB_TIMEOUT_SECONDS,\n STALE_PENDING_TIMEOUT_SECONDS,\n STALE_SWEEP_ERROR_PREFIX,\n} from './progressService'\nimport { PROGRESS_EVENTS } from './events'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\n\nconst DEFAULT_BROADCAST_MIN_INTERVAL_MS = 250\n\n// Minimum elapsed time between coalesced `progress.job.updated` broadcasts for a\n// single job. Bulk workers call updateProgress/incrementProgress once per record, and\n// every emit of the `clientBroadcast: true` event pays a serialized pg_notify roundtrip\n// plus a tenant-wide SSE fan-out. Setting the knob to 0 restores per-record emission.\n// Persistence is throttled separately: heartbeats hit the database at least every\n// HEARTBEAT_INTERVAL_MS regardless of this knob, so a high broadcast interval can\n// never starve the stale-job sweep of heartbeats.\nfunction resolveBroadcastMinIntervalMs(): number {\n const rawValue = process.env.OM_PROGRESS_BROADCAST_MIN_INTERVAL_MS\n if (rawValue == null || rawValue.trim() === '') return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n const parsed = Number.parseInt(rawValue, 10)\n if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_BROADCAST_MIN_INTERVAL_MS\n return parsed\n}\n\n// Statuses a given transition is allowed to move FROM. Every write below is a\n// compare-and-swap (`nativeUpdate` guarded on status), so concurrent writers in other\n// processes can never resurrect a terminal job or double-apply a transition \u2014 the\n// UPDATE that matches zero rows simply lost the race and must not emit events.\nconst TERMINAL_STATUSES: readonly ProgressJobStatus[] = ['completed', 'failed', 'cancelled']\nconst PROGRESS_WRITABLE_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running']\n// `failed` is restartable/completable so a job wrongly swept as stale (worker alive but\n// slow) or retried by an at-least-once queue converges to its true outcome.\nconst START_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'failed']\nconst COMPLETE_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running', 'failed']\nconst FAIL_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running']\nconst CANCEL_FROM_STATUSES: readonly ProgressJobStatus[] = ['pending', 'running', 'failed']\n\ntype JobUpdateThrottleEntry = {\n job: ProgressJob\n lastBroadcastAt: number\n lastPersistedAt: number\n lastBroadcastPercent: number\n pendingDelta: number\n absoluteCountsPending: boolean\n}\n\nfunction buildJobPayload(job: ProgressJob): Record<string, unknown> {\n return {\n jobId: job.id,\n jobType: job.jobType,\n name: job.name,\n description: job.description ?? null,\n status: job.status,\n progressPercent: job.progressPercent,\n processedCount: job.processedCount,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n cancellable: job.cancellable,\n meta: job.meta ?? null,\n startedAt: job.startedAt?.toISOString() ?? null,\n finishedAt: job.finishedAt?.toISOString() ?? null,\n }\n}\n\nexport function createProgressService(em: EntityManager, eventBus: { emit: (event: string, payload: Record<string, unknown>) => Promise<void> }): ProgressService {\n const broadcastMinIntervalMs = resolveBroadcastMinIntervalMs()\n // Per-job coalescing state, scoped to this service instance (request/worker scope).\n // The cached entity is a DETACHED snapshot (loaded with disableIdentityMap) that doubles\n // as the in-memory buffer: intermediate updates mutate it without touching the shared\n // identity map or UnitOfWork, so an unrelated em.flush() elsewhere can never write the\n // buffered values. Persistence happens exclusively through the CAS nativeUpdate below.\n const jobUpdateThrottle = new Map<string, JobUpdateThrottleEntry>()\n\n function jobScopeFilter(jobId: string, ctx: ProgressServiceContext) {\n return {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n }\n }\n\n async function loadFreshJob(jobId: string, ctx: ProgressServiceContext): Promise<ProgressJob | null> {\n return em.findOne(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n }\n\n async function ensureThrottleEntry(jobId: string, ctx: ProgressServiceContext): Promise<JobUpdateThrottleEntry> {\n const cached = jobUpdateThrottle.get(jobId)\n if (cached) return cached\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n const entry: JobUpdateThrottleEntry = {\n job,\n lastBroadcastAt: 0,\n lastPersistedAt: 0,\n lastBroadcastPercent: job.progressPercent,\n pendingDelta: 0,\n absoluteCountsPending: false,\n }\n jobUpdateThrottle.set(jobId, entry)\n return entry\n }\n\n function forgetJobThrottle(jobId: string) {\n jobUpdateThrottle.delete(jobId)\n }\n\n function buildBufferedCountData(entry: JobUpdateThrottleEntry): EntityData<ProgressJob> {\n if (entry.absoluteCountsPending || !Number.isSafeInteger(entry.pendingDelta)) {\n return { processedCount: entry.job.processedCount }\n }\n if (entry.pendingDelta !== 0) {\n const processedCount = raw(`processed_count + ${entry.pendingDelta}`)\n const totalCount = entry.job.totalCount\n if (Number.isSafeInteger(totalCount) && totalCount != null && totalCount > 0) {\n return {\n processedCount,\n progressPercent: raw(\n `least(100, round(((processed_count + ${entry.pendingDelta})::numeric / ${totalCount}) * 100))`,\n ),\n }\n }\n return { processedCount }\n }\n return {}\n }\n\n // The start transition (START_FROM_STATUSES) doubles as the recovery path for jobs a\n // stale sweep flipped to `failed` while their producer was alive but slow. Callers pass\n // the EM to run against so the forked heartbeat path can reuse the same CAS.\n //\n // `staleSweptOnly` is for the revive paths: a live write proves the producer is alive,\n // but it does NOT prove the recorded failure was the sweep's. Narrowing the CAS to rows\n // the sweep tagged keeps a genuine failure's message and stack intact instead of\n // resurrecting the job and erasing why it died. `startJob` stays unrestricted so an\n // at-least-once queue can still retry a genuinely failed job from the top.\n async function startJobViaCas(\n targetEm: EntityManager,\n job: ProgressJob,\n ctx: ProgressServiceContext,\n options: { staleSweptOnly?: boolean } = {},\n ): Promise<ProgressJob | null> {\n const now = new Date()\n // Progress counts are absolute and survive across deliveries, so the elapsed window\n // calculateEta divides them by has to survive too. Resetting it on every revive makes\n // a job with hours of work left report seconds remaining.\n const startedAt = job.startedAt ?? now\n const filter: Record<string, unknown> = {\n ...jobScopeFilter(job.id, ctx),\n status: { $in: START_FROM_STATUSES as ProgressJobStatus[] },\n }\n if (options.staleSweptOnly) filter.errorMessage = { $like: `${STALE_SWEEP_ERROR_PREFIX}%` }\n const affected = await targetEm.nativeUpdate(ProgressJob, filter as FilterQuery<ProgressJob>, {\n status: 'running',\n startedAt,\n heartbeatAt: now,\n finishedAt: null,\n errorMessage: null,\n errorStack: null,\n updatedAt: now,\n })\n if (affected === 0) return null\n\n job.status = 'running'\n job.startedAt = startedAt\n job.heartbeatAt = now\n job.finishedAt = null\n job.errorMessage = null\n job.errorStack = null\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_STARTED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n }\n\n async function persistAndMaybeBroadcast(entry: JobUpdateThrottleEntry, ctx: ProgressServiceContext): Promise<ProgressJob> {\n const job = entry.job\n const now = Date.now()\n const shouldBroadcast =\n broadcastMinIntervalMs <= 0 ||\n now - entry.lastBroadcastAt >= broadcastMinIntervalMs ||\n Math.abs(job.progressPercent - entry.lastBroadcastPercent) >= 1\n const shouldPersist = shouldBroadcast || now - entry.lastPersistedAt >= HEARTBEAT_INTERVAL_MS\n\n if (!shouldPersist) return job\n\n const usesAtomicIncrement =\n !entry.absoluteCountsPending && Number.isSafeInteger(entry.pendingDelta) && entry.pendingDelta !== 0\n const data: EntityData<ProgressJob> = {\n progressPercent: job.progressPercent,\n totalCount: job.totalCount ?? null,\n etaSeconds: job.etaSeconds ?? null,\n meta: job.meta ?? null,\n heartbeatAt: job.heartbeatAt ?? new Date(),\n updatedAt: new Date(),\n ...buildBufferedCountData(entry),\n }\n const writableFilter = {\n ...jobScopeFilter(job.id, ctx),\n status: { $in: PROGRESS_WRITABLE_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>\n let affected = await em.nativeUpdate(ProgressJob, writableFilter, data)\n\n if (affected === 0) {\n // A stale sweep may have flipped a live job to `failed` between writes. Revive it\n // through the start CAS and retry once so the buffered delta isn't silently dropped.\n const fresh = await loadFreshJob(job.id, ctx)\n let revived = false\n if (fresh && fresh.status === 'failed' && (await startJobViaCas(em, fresh, ctx, { staleSweptOnly: true }))) {\n revived = true\n job.status = 'running'\n job.startedAt = fresh.startedAt\n job.finishedAt = null\n job.errorMessage = null\n job.errorStack = null\n affected = await em.nativeUpdate(ProgressJob, writableFilter, data)\n }\n if (affected === 0) {\n // The job reached a genuinely terminal state in another process; stop writing to it.\n forgetJobThrottle(job.id)\n const latest = revived ? await loadFreshJob(job.id, ctx) : fresh\n return latest ?? job\n }\n }\n\n const persistedJob = usesAtomicIncrement ? (await loadFreshJob(job.id, ctx)) ?? job : job\n entry.job = persistedJob\n entry.pendingDelta = 0\n entry.absoluteCountsPending = false\n entry.lastPersistedAt = now\n\n if (shouldBroadcast) {\n await eventBus.emit(PROGRESS_EVENTS.JOB_UPDATED, {\n ...buildJobPayload(persistedJob),\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n entry.lastBroadcastAt = now\n entry.lastBroadcastPercent = persistedJob.progressPercent\n }\n\n return persistedJob\n }\n\n return {\n async createJob(input, ctx) {\n const job = em.create(ProgressJob, {\n jobType: input.jobType,\n name: input.name,\n description: input.description,\n totalCount: input.totalCount,\n cancellable: input.cancellable ?? false,\n meta: input.meta,\n parentJobId: input.parentJobId,\n partitionIndex: input.partitionIndex,\n partitionCount: input.partitionCount,\n startedByUserId: ctx.userId,\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n status: 'pending',\n })\n\n await em.persist(job).flush()\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CREATED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n })\n\n return job\n },\n\n async startJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (job.status === 'running' || job.status === 'cancelled' || job.status === 'completed') {\n return job\n }\n\n const started = await startJobViaCas(em, job, ctx)\n if (started) return started\n\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n },\n\n async touchJobHeartbeat(jobId, ctx) {\n // Forked EM: keepalive timers call this while the shared EM may be mid-transaction\n // inside a producer's own writes, so the heartbeat must not join that unit of work.\n const fork = em.fork()\n const now = new Date()\n const affected = await fork.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: PROGRESS_WRITABLE_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, {\n heartbeatAt: now,\n updatedAt: now,\n })\n if (affected > 0) return\n\n const job = await fork.findOne(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (!job || job.status !== 'failed') return\n // A live producer heartbeating a `failed` job means a stale sweep false-positived;\n // revive through the start CAS so the card recovers without waiting for a batch boundary.\n await startJobViaCas(fork, job, ctx, { staleSweptOnly: true })\n },\n\n async updateProgress(jobId, input, ctx) {\n const entry = await ensureThrottleEntry(jobId, ctx)\n const job = entry.job\n if (TERMINAL_STATUSES.includes(job.status)) {\n // `failed` may be a false-positive stale sweep on a slow-but-alive producer; the\n // fact that this write is happening proves the producer is alive, so revive and\n // apply the update. `completed`/`cancelled` stay terminal, and a failure the sweep\n // did not write keeps its diagnostics (see startJobViaCas).\n const revived = job.status === 'failed' && (await startJobViaCas(em, job, ctx, { staleSweptOnly: true }))\n if (!revived) {\n forgetJobThrottle(jobId)\n return job\n }\n entry.lastPersistedAt = 0\n }\n\n if (input.processedCount !== undefined) {\n job.processedCount = input.processedCount\n entry.absoluteCountsPending = true\n entry.pendingDelta = 0\n }\n if (input.totalCount !== undefined) {\n job.totalCount = input.totalCount\n }\n if (input.meta !== undefined) {\n job.meta = { ...job.meta, ...input.meta }\n }\n\n if (input.progressPercent !== undefined) {\n job.progressPercent = input.progressPercent\n } else if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n }\n\n if (input.etaSeconds !== undefined) {\n job.etaSeconds = input.etaSeconds\n } else if (job.startedAt && job.totalCount) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n\n job.heartbeatAt = new Date()\n\n return persistAndMaybeBroadcast(entry, ctx)\n },\n\n async incrementProgress(jobId, delta, ctx) {\n const entry = await ensureThrottleEntry(jobId, ctx)\n const job = entry.job\n if (TERMINAL_STATUSES.includes(job.status)) {\n const revived = job.status === 'failed' && (await startJobViaCas(em, job, ctx, { staleSweptOnly: true }))\n if (!revived) {\n forgetJobThrottle(jobId)\n return job\n }\n entry.lastPersistedAt = 0\n }\n\n job.processedCount += delta\n entry.pendingDelta += delta\n job.heartbeatAt = new Date()\n\n if (job.totalCount) {\n job.progressPercent = calculateProgressPercent(job.processedCount, job.totalCount)\n if (job.startedAt) {\n job.etaSeconds = calculateEta(job.processedCount, job.totalCount, job.startedAt)\n }\n }\n\n return persistAndMaybeBroadcast(entry, ctx)\n },\n\n async completeJob(jobId, input, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled' || job.status === 'completed') {\n forgetJobThrottle(jobId)\n return job\n }\n\n const entry = jobUpdateThrottle.get(jobId)\n const snapshot = entry?.job ?? job\n const now = new Date()\n const data: EntityData<ProgressJob> = {\n status: 'completed',\n finishedAt: now,\n etaSeconds: 0,\n updatedAt: now,\n ...(input?.resultSummary ? { resultSummary: input.resultSummary } : {}),\n ...(entry\n ? {\n totalCount: snapshot.totalCount ?? null,\n meta: snapshot.meta ?? null,\n ...buildBufferedCountData(entry),\n }\n : {}),\n progressPercent: 100,\n }\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: COMPLETE_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, data)\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n snapshot.status = 'completed'\n snapshot.finishedAt = now\n snapshot.progressPercent = 100\n snapshot.etaSeconds = 0\n if (input?.resultSummary) {\n snapshot.resultSummary = input.resultSummary\n }\n\n const persistedJob = (await loadFreshJob(jobId, ctx)) ?? snapshot\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_COMPLETED, {\n ...buildJobPayload(persistedJob),\n resultSummary: persistedJob.resultSummary,\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n\n return persistedJob\n },\n\n async failJob(jobId, input, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (TERMINAL_STATUSES.includes(job.status)) {\n forgetJobThrottle(jobId)\n return job\n }\n\n const entry = jobUpdateThrottle.get(jobId)\n const snapshot = entry?.job ?? job\n const now = new Date()\n const data: EntityData<ProgressJob> = {\n status: 'failed',\n finishedAt: now,\n errorMessage: input.errorMessage,\n errorStack: input.errorStack,\n ...(input.resultSummary ? { resultSummary: input.resultSummary } : {}),\n updatedAt: now,\n ...(entry\n ? {\n totalCount: snapshot.totalCount ?? null,\n meta: snapshot.meta ?? null,\n ...buildBufferedCountData(entry),\n }\n : {}),\n }\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: FAIL_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, data)\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n snapshot.status = 'failed'\n snapshot.finishedAt = now\n snapshot.errorMessage = input.errorMessage\n snapshot.errorStack = input.errorStack\n if (input.resultSummary) {\n snapshot.resultSummary = input.resultSummary\n }\n\n const persistedJob = (await loadFreshJob(jobId, ctx)) ?? snapshot\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(persistedJob),\n errorMessage: persistedJob.errorMessage,\n tenantId: ctx.tenantId,\n organizationId: persistedJob.organizationId ?? null,\n })\n\n return persistedJob\n },\n\n async cancelJob(jobId, ctx) {\n const job = await em.findOneOrFail(ProgressJob, jobScopeFilter(jobId, ctx), { disableIdentityMap: true })\n if (TERMINAL_STATUSES.includes(job.status)) {\n // The job finished while the user clicked cancel \u2014 a benign race, not an error.\n return job\n }\n if (!job.cancellable) {\n throw new Error(`Job ${jobId} is not cancellable`)\n }\n\n const now = new Date()\n const cancelledNow = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n cancellable: true,\n status: 'pending',\n } as FilterQuery<ProgressJob>, {\n status: 'cancelled',\n cancelRequestedAt: now,\n cancelledByUserId: ctx.userId ?? null,\n finishedAt: now,\n etaSeconds: 0,\n updatedAt: now,\n })\n\n let requested = 0\n if (cancelledNow === 0) {\n requested = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n cancellable: true,\n status: 'running',\n } as FilterQuery<ProgressJob>, {\n cancelRequestedAt: now,\n cancelledByUserId: ctx.userId ?? null,\n updatedAt: now,\n })\n }\n\n if (cancelledNow === 0 && requested === 0) {\n // Raced into a terminal state between the read and the CAS.\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n forgetJobThrottle(jobId)\n if (cancelledNow > 0) {\n job.status = 'cancelled'\n job.finishedAt = now\n job.etaSeconds = 0\n }\n job.cancelRequestedAt = now\n job.cancelledByUserId = ctx.userId ?? null\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async markCancelled(jobId, ctx) {\n const job = await loadFreshJob(jobId, ctx)\n if (!job) throw new Error(`Job ${jobId} not found`)\n if (job.status === 'cancelled' || job.status === 'completed') {\n forgetJobThrottle(jobId)\n return job\n }\n\n const now = new Date()\n const cancelRequestedAt = job.cancelRequestedAt ?? now\n const cancelledByUserId = job.cancelledByUserId ?? ctx.userId ?? null\n const finishedAt = job.finishedAt ?? now\n\n const affected = await em.nativeUpdate(ProgressJob, {\n ...jobScopeFilter(jobId, ctx),\n status: { $in: CANCEL_FROM_STATUSES as ProgressJobStatus[] },\n } as FilterQuery<ProgressJob>, {\n status: 'cancelled',\n cancelRequestedAt,\n cancelledByUserId,\n finishedAt,\n etaSeconds: 0,\n updatedAt: now,\n })\n forgetJobThrottle(jobId)\n\n if (affected === 0) {\n const fresh = await loadFreshJob(jobId, ctx)\n return fresh ?? job\n }\n\n job.status = 'cancelled'\n job.cancelRequestedAt = cancelRequestedAt\n job.cancelledByUserId = cancelledByUserId\n job.finishedAt = finishedAt\n job.etaSeconds = 0\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_CANCELLED, {\n ...buildJobPayload(job),\n tenantId: ctx.tenantId,\n organizationId: job.organizationId ?? null,\n })\n\n return job\n },\n\n async isCancellationRequested(jobId, tenantId, organizationId) {\n // disableIdentityMap forces a fresh read: a managed copy of the job in this\n // EntityManager (loaded by updateProgress) must never mask a cancellation\n // requested from another process.\n const job = await findOneWithDecryption(em, ProgressJob, {\n id: jobId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n }, { disableIdentityMap: true })\n return job?.cancelRequestedAt != null\n },\n\n async getActiveJobs(ctx) {\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['pending', 'running'] },\n parentJobId: null,\n }, {\n orderBy: { createdAt: 'DESC' },\n limit: 50,\n })\n },\n\n async getRecentlyCompletedJobs(ctx, sinceSeconds = 30) {\n const cutoff = new Date(Date.now() - sinceSeconds * 1000)\n return em.find(ProgressJob, {\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n status: { $in: ['completed', 'failed'] },\n finishedAt: { $gte: cutoff },\n parentJobId: null,\n }, {\n orderBy: { finishedAt: 'DESC' },\n limit: 10,\n })\n },\n\n async getJob(jobId, ctx) {\n return em.findOne(ProgressJob, {\n id: jobId,\n tenantId: ctx.tenantId,\n ...(ctx.organizationId ? { organizationId: ctx.organizationId } : {}),\n })\n },\n\n async markStaleJobsFailed(tenantId: string, timeoutSeconds = STALE_JOB_TIMEOUT_SECONDS, organizationId?: string | null) {\n const now = new Date()\n const cutoff = new Date(now.getTime() - timeoutSeconds * 1000)\n const scope = {\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n }\n let failedCount = 0\n\n const staleFilter = {\n status: 'running' as ProgressJobStatus,\n $or: [\n { heartbeatAt: { $lt: cutoff } },\n {\n heartbeatAt: null,\n startedAt: { $lt: cutoff },\n },\n ],\n }\n const staleRunning = await em.find(ProgressJob, { ...scope, ...staleFilter }, { disableIdentityMap: true })\n\n for (const job of staleRunning) {\n const errorMessage = `${STALE_SWEEP_ERROR_PREFIX} no heartbeat for ${timeoutSeconds} seconds`\n // Per-row CAS (re-checking the staleness condition): exactly one concurrent\n // sweeper wins, and a heartbeat that landed after the SELECT aborts the failure.\n const affected = await em.nativeUpdate(ProgressJob, {\n id: job.id,\n tenantId: job.tenantId,\n ...staleFilter,\n } as FilterQuery<ProgressJob>, {\n status: 'failed',\n finishedAt: now,\n errorMessage,\n updatedAt: now,\n })\n if (affected === 0) continue\n\n failedCount += 1\n job.status = 'failed'\n job.finishedAt = now\n job.errorMessage = errorMessage\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n // Jobs stuck in `pending` (enqueue failed, worker died before startJob) have no\n // heartbeat, so they need their own sweep or they stay in the active list forever.\n // A late queue delivery still recovers such a job: startJob transitions failed \u2192 running.\n const pendingCutoff = new Date(now.getTime() - STALE_PENDING_TIMEOUT_SECONDS * 1000)\n const stalePendingFilter = {\n status: 'pending' as ProgressJobStatus,\n createdAt: { $lt: pendingCutoff },\n }\n const stalePending = await em.find(ProgressJob, { ...scope, ...stalePendingFilter }, { disableIdentityMap: true })\n\n for (const job of stalePending) {\n const errorMessage = `${STALE_SWEEP_ERROR_PREFIX} never started within ${STALE_PENDING_TIMEOUT_SECONDS} seconds`\n const affected = await em.nativeUpdate(ProgressJob, {\n id: job.id,\n tenantId: job.tenantId,\n ...stalePendingFilter,\n } as FilterQuery<ProgressJob>, {\n status: 'failed',\n finishedAt: now,\n errorMessage,\n updatedAt: now,\n })\n if (affected === 0) continue\n\n failedCount += 1\n job.status = 'failed'\n job.finishedAt = now\n job.errorMessage = errorMessage\n\n await eventBus.emit(PROGRESS_EVENTS.JOB_FAILED, {\n ...buildJobPayload(job),\n errorMessage: job.errorMessage,\n tenantId: job.tenantId,\n stale: true,\n organizationId: job.organizationId ?? null,\n })\n }\n\n return failedCount\n },\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -30,7 +30,7 @@ const listSchema = z.object({
30
30
  withDeleted: z.coerce.boolean().optional()
31
31
  }).passthrough();
32
32
  const routeMetadata = {
33
- GET: { requireAuth: true, requireFeatures: ["sales.channels.manage"] },
33
+ GET: { requireAuth: true, requireFeatures: ["sales.channels.view"] },
34
34
  POST: { requireAuth: true, requireFeatures: ["sales.channels.manage"] },
35
35
  PUT: { requireAuth: true, requireFeatures: ["sales.channels.manage"] },
36
36
  DELETE: { requireAuth: true, requireFeatures: ["sales.channels.manage"] }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/sales/api/channels/route.ts"],
4
- "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { SalesChannel } from '../../data/entities'\nimport { channelCreateSchema, channelUpdateSchema } from '../../data/validators'\nimport { buildAggregateSearchFilter, parseScopedCommandInput, resolveCrudRecordId } from '../utils'\nimport { E } from '#generated/entities.ids.generated'\nimport * as F from '#generated/entities/sales_channel'\nimport {\n createPagedListResponseSchema,\n createSalesCrudOpenApi,\n defaultDeleteRequestSchema,\n} from '../openapi'\nimport { CatalogOffer } from '@open-mercato/core/modules/catalog/data/entities'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n id: z.string().uuid().optional(),\n ids: z.string().optional(),\n isActive: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n withDeleted: z.coerce.boolean().optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['sales.channels.manage'] },\n POST: { requireAuth: true, requireFeatures: ['sales.channels.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['sales.channels.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['sales.channels.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst salesChannelItemSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n code: z.string().nullable(),\n description: z.string().nullable(),\n statusEntryId: z.string().uuid().nullable(),\n isActive: z.boolean(),\n organizationId: z.string().uuid().nullable(),\n tenantId: z.string().uuid().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n offerCount: z.number().optional(),\n})\n\nconst salesChannelListResponseSchema = createPagedListResponseSchema(salesChannelItemSchema)\n\nexport function parseIdList(raw?: string): string[] {\n if (!raw) return []\n return raw\n .split(',')\n .map((value) => value.trim())\n .filter((value) => UUID_REGEX.test(value))\n}\n\nexport function buildSearchFilters(query: z.infer<typeof listSchema>): Record<string, unknown> {\n const filters: Record<string, unknown> = {}\n if (query.id) filters.id = { $eq: query.id }\n else {\n const ids = parseIdList(query.ids)\n if (ids.length) filters.id = { $in: ids }\n }\n const searchFilter = buildAggregateSearchFilter(query.search)\n if (searchFilter) Object.assign(filters, searchFilter)\n const isActive = parseBooleanToken(query.isActive)\n if (isActive !== null) filters.is_active = isActive\n return filters\n}\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: SalesChannel,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.sales.sales_channel },\n list: {\n schema: listSchema,\n entityId: E.sales.sales_channel,\n fields: [\n F.id,\n F.name,\n F.code,\n F.description,\n F.status_entry_id,\n F.is_active,\n F.website_url,\n F.contact_email,\n F.contact_phone,\n F.address_line1,\n F.address_line2,\n F.city,\n F.region,\n F.postal_code,\n F.country,\n F.latitude,\n F.longitude,\n F.organization_id,\n F.tenant_id,\n F.created_at,\n F.updated_at,\n ],\n sortFieldMap: {\n id: F.id,\n name: F.name,\n code: F.code,\n createdAt: F.created_at,\n updatedAt: F.updated_at,\n },\n buildFilters: async (query) => buildSearchFilters(query),\n decorateCustomFields: { entityIds: [E.sales.sales_channel] },\n transformItem: (item: any) => {\n const offerCount =\n typeof item.offerCount === 'number'\n ? item.offerCount\n : typeof item.offer_count === 'number'\n ? item.offer_count\n : 0\n const base = {\n id: item.id,\n name: item.name,\n code: item.code ?? null,\n description: item.description ?? null,\n statusEntryId: item.status_entry_id ?? null,\n isActive: item.is_active ?? false,\n websiteUrl: item.website_url ?? null,\n contactEmail: item.contact_email ?? null,\n contactPhone: item.contact_phone ?? null,\n addressLine1: item.address_line1 ?? null,\n addressLine2: item.address_line2 ?? null,\n city: item.city ?? null,\n region: item.region ?? null,\n postalCode: item.postal_code ?? null,\n country: item.country ?? null,\n latitude: item.latitude ?? null,\n longitude: item.longitude ?? null,\n organizationId: item.organization_id ?? null,\n tenantId: item.tenant_id ?? null,\n createdAt: item.created_at,\n updatedAt: item.updated_at,\n offerCount,\n }\n const { custom } = splitCustomFieldPayload(item)\n return Object.keys(custom).length ? { ...base, customFields: custom } : base\n },\n },\n actions: {\n create: {\n commandId: 'sales.channels.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(channelCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({ id: result?.channelId ?? result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'sales.channels.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(channelUpdateSchema, raw ?? {}, ctx, translate)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'sales.channels.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id = resolveCrudRecordId(parsed, ctx, translate)\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n hooks: {\n afterList: async (payload, ctx) => {\n await decorateChannelsWithOfferCounts(payload, ctx)\n },\n },\n})\n\nexport const openApi = createSalesCrudOpenApi({\n resourceName: 'Sales channel',\n pluralName: 'Sales channels',\n description: 'Manage sales channels to segment orders and pricing across marketplaces or stores.',\n querySchema: listSchema,\n listResponseSchema: salesChannelListResponseSchema,\n create: { schema: channelCreateSchema },\n update: { schema: channelUpdateSchema },\n del: { schema: defaultDeleteRequestSchema },\n})\n\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nexport async function decorateChannelsWithOfferCounts(\n payload: { items?: Array<Record<string, unknown>> },\n ctx: CrudCtx,\n) {\n const items = Array.isArray(payload.items) ? payload.items : []\n if (!items.length) return\n const channelIds = items\n .map((item) => {\n const value = item?.id\n return typeof value === 'string' && value.length ? value : null\n })\n .filter((value): value is string => !!value)\n if (!channelIds.length) return\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const offers = await em.find(\n CatalogOffer,\n { channelId: { $in: channelIds }, deletedAt: null },\n { fields: ['id', 'channelId'] },\n )\n const countMap = new Map<string, number>()\n offers.forEach((offer) => {\n const channelId = offer.channelId\n if (!channelId) return\n countMap.set(channelId, (countMap.get(channelId) ?? 0) + 1)\n })\n items.forEach((item) => {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return\n ;(item as Record<string, unknown>).offerCount = countMap.get(id) ?? 0\n })\n } catch (err) {\n logger.warn('sales.channels failed to resolve channel offer counts', { err })\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { z } from 'zod'\nimport { makeCrudRoute, type CrudCtx } from '@open-mercato/shared/lib/crud/factory'\nimport { splitCustomFieldPayload } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { SalesChannel } from '../../data/entities'\nimport { channelCreateSchema, channelUpdateSchema } from '../../data/validators'\nimport { buildAggregateSearchFilter, parseScopedCommandInput, resolveCrudRecordId } from '../utils'\nimport { E } from '#generated/entities.ids.generated'\nimport * as F from '#generated/entities/sales_channel'\nimport {\n createPagedListResponseSchema,\n createSalesCrudOpenApi,\n defaultDeleteRequestSchema,\n} from '../openapi'\nimport { CatalogOffer } from '@open-mercato/core/modules/catalog/data/entities'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('sales')\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/\n\nconst listSchema = z\n .object({\n page: z.coerce.number().min(1).default(1),\n pageSize: z.coerce.number().min(1).max(100).default(50),\n search: z.string().optional(),\n id: z.string().uuid().optional(),\n ids: z.string().optional(),\n isActive: z.string().optional(),\n sortField: z.string().optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n withDeleted: z.coerce.boolean().optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['sales.channels.view'] },\n POST: { requireAuth: true, requireFeatures: ['sales.channels.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['sales.channels.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['sales.channels.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst salesChannelItemSchema = z.object({\n id: z.string().uuid(),\n name: z.string(),\n code: z.string().nullable(),\n description: z.string().nullable(),\n statusEntryId: z.string().uuid().nullable(),\n isActive: z.boolean(),\n organizationId: z.string().uuid().nullable(),\n tenantId: z.string().uuid().nullable(),\n createdAt: z.string(),\n updatedAt: z.string(),\n customFields: z.record(z.string(), z.unknown()).optional(),\n offerCount: z.number().optional(),\n})\n\nconst salesChannelListResponseSchema = createPagedListResponseSchema(salesChannelItemSchema)\n\nexport function parseIdList(raw?: string): string[] {\n if (!raw) return []\n return raw\n .split(',')\n .map((value) => value.trim())\n .filter((value) => UUID_REGEX.test(value))\n}\n\nexport function buildSearchFilters(query: z.infer<typeof listSchema>): Record<string, unknown> {\n const filters: Record<string, unknown> = {}\n if (query.id) filters.id = { $eq: query.id }\n else {\n const ids = parseIdList(query.ids)\n if (ids.length) filters.id = { $in: ids }\n }\n const searchFilter = buildAggregateSearchFilter(query.search)\n if (searchFilter) Object.assign(filters, searchFilter)\n const isActive = parseBooleanToken(query.isActive)\n if (isActive !== null) filters.is_active = isActive\n return filters\n}\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: SalesChannel,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n indexer: { entityType: E.sales.sales_channel },\n list: {\n schema: listSchema,\n entityId: E.sales.sales_channel,\n fields: [\n F.id,\n F.name,\n F.code,\n F.description,\n F.status_entry_id,\n F.is_active,\n F.website_url,\n F.contact_email,\n F.contact_phone,\n F.address_line1,\n F.address_line2,\n F.city,\n F.region,\n F.postal_code,\n F.country,\n F.latitude,\n F.longitude,\n F.organization_id,\n F.tenant_id,\n F.created_at,\n F.updated_at,\n ],\n sortFieldMap: {\n id: F.id,\n name: F.name,\n code: F.code,\n createdAt: F.created_at,\n updatedAt: F.updated_at,\n },\n buildFilters: async (query) => buildSearchFilters(query),\n decorateCustomFields: { entityIds: [E.sales.sales_channel] },\n transformItem: (item: any) => {\n const offerCount =\n typeof item.offerCount === 'number'\n ? item.offerCount\n : typeof item.offer_count === 'number'\n ? item.offer_count\n : 0\n const base = {\n id: item.id,\n name: item.name,\n code: item.code ?? null,\n description: item.description ?? null,\n statusEntryId: item.status_entry_id ?? null,\n isActive: item.is_active ?? false,\n websiteUrl: item.website_url ?? null,\n contactEmail: item.contact_email ?? null,\n contactPhone: item.contact_phone ?? null,\n addressLine1: item.address_line1 ?? null,\n addressLine2: item.address_line2 ?? null,\n city: item.city ?? null,\n region: item.region ?? null,\n postalCode: item.postal_code ?? null,\n country: item.country ?? null,\n latitude: item.latitude ?? null,\n longitude: item.longitude ?? null,\n organizationId: item.organization_id ?? null,\n tenantId: item.tenant_id ?? null,\n createdAt: item.created_at,\n updatedAt: item.updated_at,\n offerCount,\n }\n const { custom } = splitCustomFieldPayload(item)\n return Object.keys(custom).length ? { ...base, customFields: custom } : base\n },\n },\n actions: {\n create: {\n commandId: 'sales.channels.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(channelCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({ id: result?.channelId ?? result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'sales.channels.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(channelUpdateSchema, raw ?? {}, ctx, translate)\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'sales.channels.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id = resolveCrudRecordId(parsed, ctx, translate)\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n hooks: {\n afterList: async (payload, ctx) => {\n await decorateChannelsWithOfferCounts(payload, ctx)\n },\n },\n})\n\nexport const openApi = createSalesCrudOpenApi({\n resourceName: 'Sales channel',\n pluralName: 'Sales channels',\n description: 'Manage sales channels to segment orders and pricing across marketplaces or stores.',\n querySchema: listSchema,\n listResponseSchema: salesChannelListResponseSchema,\n create: { schema: channelCreateSchema },\n update: { schema: channelUpdateSchema },\n del: { schema: defaultDeleteRequestSchema },\n})\n\nexport const GET = crud.GET\nexport const POST = crud.POST\nexport const PUT = crud.PUT\nexport const DELETE = crud.DELETE\n\nexport async function decorateChannelsWithOfferCounts(\n payload: { items?: Array<Record<string, unknown>> },\n ctx: CrudCtx,\n) {\n const items = Array.isArray(payload.items) ? payload.items : []\n if (!items.length) return\n const channelIds = items\n .map((item) => {\n const value = item?.id\n return typeof value === 'string' && value.length ? value : null\n })\n .filter((value): value is string => !!value)\n if (!channelIds.length) return\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const offers = await em.find(\n CatalogOffer,\n { channelId: { $in: channelIds }, deletedAt: null },\n { fields: ['id', 'channelId'] },\n )\n const countMap = new Map<string, number>()\n offers.forEach((offer) => {\n const channelId = offer.channelId\n if (!channelId) return\n countMap.set(channelId, (countMap.get(channelId) ?? 0) + 1)\n })\n items.forEach((item) => {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return\n ;(item as Record<string, unknown>).offerCount = countMap.get(id) ?? 0\n })\n } catch (err) {\n logger.warn('sales.channels failed to resolve channel offer counts', { err })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.8-develop.6986.1.3adb0d0df6",
3
+ "version": "0.6.8-develop.6992.1.00c90fecff",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -254,16 +254,16 @@
254
254
  "zod": "^4.4.3"
255
255
  },
256
256
  "peerDependencies": {
257
- "@open-mercato/ai-assistant": "0.6.8-develop.6986.1.3adb0d0df6",
258
- "@open-mercato/shared": "0.6.8-develop.6986.1.3adb0d0df6",
259
- "@open-mercato/ui": "0.6.8-develop.6986.1.3adb0d0df6",
257
+ "@open-mercato/ai-assistant": "0.6.8-develop.6992.1.00c90fecff",
258
+ "@open-mercato/shared": "0.6.8-develop.6992.1.00c90fecff",
259
+ "@open-mercato/ui": "0.6.8-develop.6992.1.00c90fecff",
260
260
  "react": "^19.0.0",
261
261
  "react-dom": "^19.0.0"
262
262
  },
263
263
  "devDependencies": {
264
- "@open-mercato/ai-assistant": "0.6.8-develop.6986.1.3adb0d0df6",
265
- "@open-mercato/shared": "0.6.8-develop.6986.1.3adb0d0df6",
266
- "@open-mercato/ui": "0.6.8-develop.6986.1.3adb0d0df6",
264
+ "@open-mercato/ai-assistant": "0.6.8-develop.6992.1.00c90fecff",
265
+ "@open-mercato/shared": "0.6.8-develop.6992.1.00c90fecff",
266
+ "@open-mercato/ui": "0.6.8-develop.6992.1.00c90fecff",
267
267
  "@testing-library/dom": "^10.4.1",
268
268
  "@testing-library/jest-dom": "^7.0.0",
269
269
  "@testing-library/react": "^16.3.1",
@@ -7,6 +7,7 @@ import { Avatar } from '@open-mercato/ui/primitives/avatar'
7
7
  import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
8
8
  import { StepIndicator, type StepIndicatorStep } from '@open-mercato/ui/primitives/step-indicator'
9
9
  import { useT } from '@open-mercato/shared/lib/i18n/context'
10
+ import { hasMoreFromPage } from '@open-mercato/shared/lib/pagination/load-more'
10
11
  import { Button } from '@open-mercato/ui/primitives/button'
11
12
  import { Badge } from '@open-mercato/ui/primitives/badge'
12
13
  import { Input } from '@open-mercato/ui/primitives/input'
@@ -75,6 +76,10 @@ export function AssignRoleDialog({
75
76
  const [activeTeam, setActiveTeam] = React.useState('all')
76
77
  const [loadError, setLoadError] = React.useState<string | null>(null)
77
78
  const [totalUsers, setTotalUsers] = React.useState(0)
79
+ // Short-page termination instead of `users.length >= totalUsers` — see
80
+ // `hasMoreFromPage`. Measured on the served count, not on `users`, which is
81
+ // deduped by id.
82
+ const [hasMore, setHasMore] = React.useState(false)
78
83
  const [currentPage, setCurrentPage] = React.useState(1)
79
84
  const deferredSearchQuery = React.useDeferredValue(searchQuery)
80
85
  const requestSequenceRef = React.useRef(0)
@@ -94,6 +99,7 @@ export function AssignRoleDialog({
94
99
  setActiveTeam('all')
95
100
  setLoadError(null)
96
101
  setTotalUsers(0)
102
+ setHasMore(false)
97
103
  setCurrentPage(1)
98
104
  requestSequenceRef.current = 0
99
105
  return
@@ -110,6 +116,7 @@ export function AssignRoleDialog({
110
116
  setActiveTeam('all')
111
117
  setLoadError(null)
112
118
  setTotalUsers(0)
119
+ setHasMore(false)
113
120
  setCurrentPage(1)
114
121
  requestSequenceRef.current = 0
115
122
  }, [initialRoleType, open])
@@ -154,6 +161,7 @@ export function AssignRoleDialog({
154
161
  return Array.from(merged.values())
155
162
  })
156
163
  setTotalUsers(result.total)
164
+ setHasMore(hasMoreFromPage(result.servedCount, ASSIGNABLE_STAFF_PAGE_SIZE))
157
165
  setCurrentPage(result.page)
158
166
  setLoadError(null)
159
167
  } catch {
@@ -161,6 +169,7 @@ export function AssignRoleDialog({
161
169
  if (!append) {
162
170
  setUsers([])
163
171
  setTotalUsers(0)
172
+ setHasMore(false)
164
173
  setCurrentPage(1)
165
174
  }
166
175
  setLoadError(
@@ -189,14 +198,14 @@ export function AssignRoleDialog({
189
198
  }, [deferredSearchQuery, searchUsers, step])
190
199
 
191
200
  const handleLoadMore = React.useCallback(() => {
192
- if (loading || loadingMore || users.length >= totalUsers) return
201
+ if (loading || loadingMore || !hasMore) return
193
202
  // fire-and-forget: search results populate async; errors shown in list UI
194
203
  searchUsers({
195
204
  query: deferredSearchQuery,
196
205
  page: currentPage + 1,
197
206
  append: true,
198
207
  }).catch(() => {})
199
- }, [currentPage, deferredSearchQuery, loading, loadingMore, searchUsers, totalUsers, users.length])
208
+ }, [currentPage, deferredSearchQuery, hasMore, loading, loadingMore, searchUsers])
200
209
 
201
210
  const selectedRole = React.useMemo(
202
211
  () => roleTypes.find((roleType) => roleType.value === selectedRoleType) ?? null,
@@ -262,7 +271,7 @@ export function AssignRoleDialog({
262
271
  )
263
272
  }, [t, totalUsers, users.length])
264
273
 
265
- const canLoadMore = users.length < totalUsers
274
+ const canLoadMore = hasMore
266
275
 
267
276
  const handleAssign = React.useCallback(async () => {
268
277
  if (!selectedRoleType || !selectedUser) return
@@ -13,6 +13,7 @@ import { createDealLinkAdapter } from '../linking/adapters/dealAdapter'
13
13
  import { LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'
14
14
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
15
15
  import { useT } from '@open-mercato/shared/lib/i18n/context'
16
+ import { hasMoreFromPage } from '@open-mercato/shared/lib/pagination/load-more'
16
17
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
17
18
  import { E } from '#generated/entities.ids.generated'
18
19
  import type { DealCustomFieldEntry, DealSummary, SectionAction, TabEmptyStateConfig, Translator } from './types'
@@ -545,17 +546,11 @@ export function DealsSection({
545
546
  return [...updatedPrev, ...appended]
546
547
  })
547
548
  pageRef.current = nextPage
548
- const totalPagesRaw = payload?.totalPages
549
- const totalPages =
550
- typeof totalPagesRaw === 'number'
551
- ? totalPagesRaw
552
- : typeof totalPagesRaw === 'string' && totalPagesRaw.trim().length
553
- ? Number(totalPagesRaw)
554
- : null
555
- const nextHasMore =
556
- totalPages && Number.isFinite(totalPages)
557
- ? nextPage < totalPages
558
- : mapped.length === DEALS_PAGE_SIZE
549
+ // Short-page termination, unconditionally — see `hasMoreFromPage`.
550
+ // Preferring `totalPages` here hid deals that do exist. Measured on
551
+ // `rawItems`, which is what the server served, rather than on the
552
+ // deduped rows appended above.
553
+ const nextHasMore = hasMoreFromPage(rawItems.length, DEALS_PAGE_SIZE)
559
554
  hasMoreRef.current = nextHasMore
560
555
  setHasMore(nextHasMore)
561
556
  setLoadError(null)
@@ -18,6 +18,13 @@ type AssignableStaffResponse = {
18
18
 
19
19
  export type AssignableStaffMembersPage = {
20
20
  items: AssignableStaffMember[]
21
+ /**
22
+ * How many rows the server served for this page, before the dedupe below.
23
+ * "Load more" guards must measure this rather than `items.length`: a deduped
24
+ * length shorter than the served page reads as a short page and terminates
25
+ * the sequence early, stranding the rest of the roster.
26
+ */
27
+ servedCount: number
21
28
  total: number
22
29
  page: number
23
30
  pageSize: number
@@ -59,7 +66,7 @@ export async function fetchAssignableStaffMembersPage(
59
66
  )
60
67
  } catch (error) {
61
68
  if (isAssignableEndpointMissing(error)) {
62
- return { items: [], total: 0, page, pageSize }
69
+ return { items: [], servedCount: 0, total: 0, page, pageSize }
63
70
  }
64
71
  throw error
65
72
  }
@@ -125,6 +132,7 @@ export async function fetchAssignableStaffMembersPage(
125
132
 
126
133
  return {
127
134
  items: Array.from(deduped.values()),
135
+ servedCount: rawItems.length,
128
136
  total:
129
137
  typeof data?.total === 'number' && Number.isFinite(data.total)
130
138
  ? data.total