@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/customers/components/detail/AssignRoleDialog.js +9 -3
- package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealsSection.js +2 -3
- package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
- package/dist/modules/customers/components/detail/assignableStaff.js +2 -1
- package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +11 -12
- package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +7 -4
- package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
- package/dist/modules/data_sync/api/run.js +9 -1
- package/dist/modules/data_sync/api/run.js.map +2 -2
- package/dist/modules/data_sync/api/runs/[id]/retry.js +9 -1
- package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
- package/dist/modules/data_sync/lib/adapter-registry.js +10 -1
- package/dist/modules/data_sync/lib/adapter-registry.js.map +2 -2
- package/dist/modules/data_sync/lib/start-cursor.js +17 -0
- package/dist/modules/data_sync/lib/start-cursor.js.map +7 -0
- package/dist/modules/data_sync/lib/sync-engine.js +84 -27
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-run-service.js +86 -10
- package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
- package/dist/modules/data_sync/workers/sync-scheduled.js +9 -6
- package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
- package/dist/modules/progress/lib/progressService.js +2 -0
- package/dist/modules/progress/lib/progressService.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +78 -34
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/sales/api/channels/route.js +1 -1
- package/dist/modules/sales/api/channels/route.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/components/detail/AssignRoleDialog.tsx +12 -3
- package/src/modules/customers/components/detail/DealsSection.tsx +6 -11
- package/src/modules/customers/components/detail/assignableStaff.ts +9 -1
- package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +16 -13
- package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +11 -4
- package/src/modules/data_sync/AGENTS.md +6 -1
- package/src/modules/data_sync/api/run.ts +9 -1
- package/src/modules/data_sync/api/runs/[id]/retry.ts +9 -1
- package/src/modules/data_sync/lib/adapter-registry.ts +15 -0
- package/src/modules/data_sync/lib/adapter.ts +18 -0
- package/src/modules/data_sync/lib/start-cursor.ts +35 -0
- package/src/modules/data_sync/lib/sync-engine.ts +101 -27
- package/src/modules/data_sync/lib/sync-run-service.ts +118 -10
- package/src/modules/data_sync/workers/sync-scheduled.ts +9 -6
- package/src/modules/progress/AGENTS.md +2 -1
- package/src/modules/progress/lib/progressService.ts +9 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +111 -37
- 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
|
|
141
|
+
const writableFilter = {
|
|
110
142
|
...jobScopeFilter(job.id, ctx),
|
|
111
143
|
status: { $in: PROGRESS_WRITABLE_STATUSES }
|
|
112
|
-
}
|
|
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
|
-
|
|
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
|
|
218
|
+
const affected = await fork.nativeUpdate(ProgressJob, {
|
|
166
219
|
...jobScopeFilter(jobId, ctx),
|
|
167
|
-
status: { $in:
|
|
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
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
230
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAClB,SAAS,qBAAmC;AAC5C,SAAS,+BAA+B;AACxC,SAAS,2BAA2B;AAEpC,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB,2BAA2B;AACzD,SAAS,4BAA4B,yBAAyB,2BAA2B;AACzF,SAAS,SAAS;AAClB,YAAY,OAAO;AACnB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAClC,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,OAAO;AAEnC,MAAM,gBAAgB,EAAE,OAAO,CAAC,CAAC,EAAE,YAAY;AAE/C,MAAM,aAAa;AAEnB,MAAM,aAAa,EAChB,OAAO;AAAA,EACN,MAAM,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,QAAQ,EAAE;AAAA,EACtD,QAAQ,EAAE,OAAO,EAAE,SAAS;AAAA,EAC5B,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC/B,KAAK,EAAE,OAAO,EAAE,SAAS;AAAA,EACzB,UAAU,EAAE,OAAO,EAAE,SAAS;AAAA,EAC9B,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,EAC/B,SAAS,EAAE,KAAK,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS;AAAA,EAC1C,aAAa,EAAE,OAAO,QAAQ,EAAE,SAAS;AAC3C,CAAC,EACA,YAAY;AAEf,MAAM,gBAAgB;AAAA,EACpB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,
|
|
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.
|
|
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.
|
|
258
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
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.
|
|
265
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
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 ||
|
|
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
|
|
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 =
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|