@open-mercato/core 0.8.1-develop.7241.1.fad7b2ce2a → 0.8.1-develop.7261.1.5d42353f61
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/attachments/lib/storage.js +15 -1
- package/dist/modules/attachments/lib/storage.js.map +2 -2
- package/dist/modules/auth/api/reset/confirm.js +3 -2
- package/dist/modules/auth/api/reset/confirm.js.map +2 -2
- package/dist/modules/auth/api/reset.js +2 -1
- package/dist/modules/auth/api/reset.js.map +2 -2
- package/dist/modules/auth/di.js +6 -6
- package/dist/modules/auth/di.js.map +2 -2
- package/dist/modules/auth/lib/tenantHint.js +29 -0
- package/dist/modules/auth/lib/tenantHint.js.map +7 -0
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js +11 -3
- package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users.js +27 -14
- package/dist/modules/customer_accounts/api/admin/users.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +9 -0
- package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js +8 -8
- package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js.map +2 -2
- package/dist/modules/customer_accounts/di.js +3 -3
- package/dist/modules/customer_accounts/di.js.map +2 -2
- package/dist/modules/customer_accounts/lib/customerAuth.js +18 -7
- package/dist/modules/customer_accounts/lib/customerAuth.js.map +2 -2
- package/dist/modules/customer_accounts/services/customerSessionService.js +23 -0
- package/dist/modules/customer_accounts/services/customerSessionService.js.map +2 -2
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js +28 -24
- package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/map/page.js +10 -2
- package/dist/modules/customers/backend/customers/deals/map/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +10 -2
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js +41 -66
- package/dist/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +6 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/commands/companies.js +5 -0
- package/dist/modules/customers/commands/companies.js.map +2 -2
- package/dist/modules/customers/components/formConfig.js +3 -3
- package/dist/modules/customers/components/formConfig.js.map +2 -2
- package/dist/modules/customers/data/validators.js +3 -1
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-engine.js +3 -7
- package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
- package/dist/modules/data_sync/lib/sync-schedule-service.js +52 -3
- package/dist/modules/data_sync/lib/sync-schedule-service.js.map +2 -2
- package/dist/modules/progress/lib/progressServiceImpl.js +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/attachments/lib/storage.ts +24 -1
- package/src/modules/auth/api/reset/confirm.ts +3 -2
- package/src/modules/auth/api/reset.ts +2 -1
- package/src/modules/auth/di.ts +6 -6
- package/src/modules/auth/lib/tenantHint.ts +49 -0
- package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +19 -5
- package/src/modules/catalog/i18n/de.json +1 -0
- package/src/modules/catalog/i18n/en.json +1 -0
- package/src/modules/catalog/i18n/es.json +1 -0
- package/src/modules/catalog/i18n/ko.json +1 -0
- package/src/modules/catalog/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/api/admin/users.ts +30 -14
- package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +11 -0
- package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.tsx +11 -11
- package/src/modules/customer_accounts/di.ts +3 -3
- package/src/modules/customer_accounts/i18n/de.json +1 -0
- package/src/modules/customer_accounts/i18n/en.json +1 -0
- package/src/modules/customer_accounts/i18n/es.json +1 -0
- package/src/modules/customer_accounts/i18n/ko.json +1 -0
- package/src/modules/customer_accounts/i18n/pl.json +1 -0
- package/src/modules/customer_accounts/lib/customerAuth.ts +36 -7
- package/src/modules/customer_accounts/services/customerSessionService.ts +28 -0
- package/src/modules/customers/ai-tools/activities-tasks-pack.ts +49 -39
- package/src/modules/customers/backend/customers/deals/map/page.tsx +13 -2
- package/src/modules/customers/backend/customers/deals/page.tsx +13 -2
- package/src/modules/customers/backend/customers/deals/pipeline/components/ViewTabsRow.tsx +69 -56
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +8 -2
- package/src/modules/customers/commands/companies.ts +8 -0
- package/src/modules/customers/components/formConfig.tsx +3 -3
- package/src/modules/customers/data/validators.ts +2 -1
- package/src/modules/customers/i18n/de.json +1 -0
- package/src/modules/customers/i18n/en.json +1 -0
- package/src/modules/customers/i18n/es.json +1 -0
- package/src/modules/customers/i18n/ko.json +1 -0
- package/src/modules/customers/i18n/pl.json +1 -0
- package/src/modules/data_sync/AGENTS.md +1 -1
- package/src/modules/data_sync/lib/sync-engine.ts +23 -12
- package/src/modules/data_sync/lib/sync-schedule-service.ts +54 -3
- package/src/modules/progress/lib/progressServiceImpl.ts +7 -1
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
type CustomerDictionaryKind,
|
|
60
60
|
} from '../lib/dictionaries'
|
|
61
61
|
import { normalizeCustomFieldSubmitValue } from './detail/customFieldUtils'
|
|
62
|
-
import { CUSTOMER_PHONE_INVALID_MESSAGE_KEY } from '../data/validators'
|
|
62
|
+
import { CUSTOMER_PHONE_INVALID_MESSAGE_KEY, CUSTOMER_URL_INVALID_MESSAGE_KEY } from '../data/validators'
|
|
63
63
|
|
|
64
64
|
export const metadata = {
|
|
65
65
|
navHidden: true,
|
|
@@ -1199,7 +1199,7 @@ export const createCompanyFormSchema = () =>
|
|
|
1199
1199
|
websiteUrl: z
|
|
1200
1200
|
.string()
|
|
1201
1201
|
.trim()
|
|
1202
|
-
.url()
|
|
1202
|
+
.url(CUSTOMER_URL_INVALID_MESSAGE_KEY)
|
|
1203
1203
|
.optional()
|
|
1204
1204
|
.or(z.literal(''))
|
|
1205
1205
|
.transform((val) => (val === '' ? undefined : val))
|
|
@@ -1559,7 +1559,7 @@ const clearableUrlField = () =>
|
|
|
1559
1559
|
z
|
|
1560
1560
|
.string()
|
|
1561
1561
|
.trim()
|
|
1562
|
-
.url()
|
|
1562
|
+
.url(CUSTOMER_URL_INVALID_MESSAGE_KEY)
|
|
1563
1563
|
.optional()
|
|
1564
1564
|
.or(z.literal(''))
|
|
1565
1565
|
.transform((val) => (val === '' ? null : val))
|
|
@@ -6,6 +6,7 @@ import { dictionaryEntrySortModeSchema } from '@open-mercato/core/modules/dictio
|
|
|
6
6
|
const uuid = () => z.string().uuid()
|
|
7
7
|
|
|
8
8
|
export const CUSTOMER_PHONE_INVALID_MESSAGE_KEY = 'customers.people.form.primaryPhone.invalid'
|
|
9
|
+
export const CUSTOMER_URL_INVALID_MESSAGE_KEY = 'customers.people.form.websiteUrl.invalid'
|
|
9
10
|
export const ACTIVITY_DATE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.dateRequired'
|
|
10
11
|
export const ACTIVITY_TIME_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.timeRequired'
|
|
11
12
|
export const ACTIVITY_PHONE_REQUIRED_MESSAGE_KEY = 'customers.activities.errors.phoneRequired'
|
|
@@ -41,7 +42,7 @@ const clearableEmailSchema = z.preprocess(
|
|
|
41
42
|
|
|
42
43
|
const clearableUrlSchema = z.preprocess(
|
|
43
44
|
emptyStringToNull,
|
|
44
|
-
z.string().url().max(300).nullable().optional(),
|
|
45
|
+
z.string().url(CUSTOMER_URL_INVALID_MESSAGE_KEY).max(300).nullable().optional(),
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
// Domain is a plain (non-URL) string that maps to a nullable column, so blanking
|
|
@@ -2352,6 +2352,7 @@
|
|
|
2352
2352
|
"customers.people.form.twitterUrl": "X / Twitter",
|
|
2353
2353
|
"customers.people.form.twitterUrl.placeholder": "https://x.com/beispiel",
|
|
2354
2354
|
"customers.people.form.updateSuccess": "Person aktualisiert.",
|
|
2355
|
+
"customers.people.form.websiteUrl.invalid": "Geben Sie eine gültige URL ein (z. B. https://example.com)",
|
|
2355
2356
|
"customers.people.list.actions.delete": "Löschen",
|
|
2356
2357
|
"customers.people.list.actions.new": "Neue Person",
|
|
2357
2358
|
"customers.people.list.actions.openInNewTab": "In neuem Tab öffnen",
|
|
@@ -2352,6 +2352,7 @@
|
|
|
2352
2352
|
"customers.people.form.twitterUrl": "X / Twitter",
|
|
2353
2353
|
"customers.people.form.twitterUrl.placeholder": "https://x.com/example",
|
|
2354
2354
|
"customers.people.form.updateSuccess": "Person updated.",
|
|
2355
|
+
"customers.people.form.websiteUrl.invalid": "Enter a valid URL (e.g. https://example.com)",
|
|
2355
2356
|
"customers.people.list.actions.delete": "Delete",
|
|
2356
2357
|
"customers.people.list.actions.new": "New Person",
|
|
2357
2358
|
"customers.people.list.actions.openInNewTab": "Open in new tab",
|
|
@@ -2352,6 +2352,7 @@
|
|
|
2352
2352
|
"customers.people.form.twitterUrl": "X / Twitter",
|
|
2353
2353
|
"customers.people.form.twitterUrl.placeholder": "https://x.com/ejemplo",
|
|
2354
2354
|
"customers.people.form.updateSuccess": "Persona actualizada.",
|
|
2355
|
+
"customers.people.form.websiteUrl.invalid": "Introduce una URL válida (p. ej., https://example.com)",
|
|
2355
2356
|
"customers.people.list.actions.delete": "Eliminar",
|
|
2356
2357
|
"customers.people.list.actions.new": "Nueva persona",
|
|
2357
2358
|
"customers.people.list.actions.openInNewTab": "Abrir en nueva pestaña",
|
|
@@ -2352,6 +2352,7 @@
|
|
|
2352
2352
|
"customers.people.form.twitterUrl": "X / Twitter",
|
|
2353
2353
|
"customers.people.form.twitterUrl.placeholder": "https://x.com/example",
|
|
2354
2354
|
"customers.people.form.updateSuccess": "연락처가 업데이트되었습니다.",
|
|
2355
|
+
"customers.people.form.websiteUrl.invalid": "올바른 URL을 입력하세요 (예: https://example.com)",
|
|
2355
2356
|
"customers.people.list.actions.delete": "삭제",
|
|
2356
2357
|
"customers.people.list.actions.new": "새 연락처",
|
|
2357
2358
|
"customers.people.list.actions.openInNewTab": "새 탭에서 열기",
|
|
@@ -2352,6 +2352,7 @@
|
|
|
2352
2352
|
"customers.people.form.twitterUrl": "X / Twitter",
|
|
2353
2353
|
"customers.people.form.twitterUrl.placeholder": "https://x.com/przyklad",
|
|
2354
2354
|
"customers.people.form.updateSuccess": "Osoba została zaktualizowana.",
|
|
2355
|
+
"customers.people.form.websiteUrl.invalid": "Podaj prawidłowy adres URL (np. https://example.com)",
|
|
2355
2356
|
"customers.people.list.actions.delete": "Usuń",
|
|
2356
2357
|
"customers.people.list.actions.new": "Nowa osoba",
|
|
2357
2358
|
"customers.people.list.actions.openInNewTab": "Otwórz w nowej karcie",
|
|
@@ -288,7 +288,7 @@ Data sync providers can leverage the **Unified Module Extension System (UMES)**
|
|
|
288
288
|
|
|
289
289
|
- `ProgressTopBar` and sync-run detail pages use `progress.job.*` SSE updates for live progress.
|
|
290
290
|
- Create `ProgressJob` in `run`/`retry` endpoints; start/update/complete/fail in `sync-engine`.
|
|
291
|
-
- The engine heartbeats (`touchJobHeartbeat`, forked-EM) on a timer while an adapter batch is being produced, because
|
|
291
|
+
- The engine heartbeats (`touchJobHeartbeat`, forked-EM) on a timer for the whole life of the batch stream — while an adapter batch is being produced AND while the per-batch consumer body commits progress, refreshes coverage snapshots and writes logs — because either side can outlast the 60s stale-job sweep. Keep the `withHeartbeat` wrapper around `streamImport`/`streamExport` when touching the batch loops, and keep its interval armed once per stream rather than restarted per read. Because that interval now fires during the consumer body, where `commitBatchProgress` holds the shared `EntityManager` inside `em.begin()`/`em.commit()`, **every callback on this timer MUST use a forked EM** — both `touchJobHeartbeat` and the `isCancellationRequested` poll do. Adding a third tick callback that reads on the shared EM would interleave with that open transaction, and the tick swallows its own errors, so the damage would surface somewhere else entirely.
|
|
292
292
|
- On redelivery the progress counter is seeded from the progress job's own `processedCount` (`progressService.getJob`), mirroring how `committedBatches` resumes — never reset it to zero (`updateProgress` writes absolute counts) and never seed it from the run's `created/updated/skipped/failed` columns: those count emitted items, while progress counts source records (`batch.processedCount`), and adapters may emit several items per source record.
|
|
293
293
|
- Include `progressJob` details in run detail response.
|
|
294
294
|
- SSE DOM bridge forwards only events with `clientBroadcast: true`.
|
|
@@ -166,9 +166,6 @@ function applyExportCounters(batch: ExportBatch): SyncCounterDelta {
|
|
|
166
166
|
// the per-interval round-trips for the whole life of a run.
|
|
167
167
|
const HEARTBEAT_TICK_MS = (STALE_JOB_TIMEOUT_SECONDS * 1000) / 4
|
|
168
168
|
|
|
169
|
-
// Runs `tick` on an interval only while the source iterator is pending, so heartbeats
|
|
170
|
-
// stop the moment the producer dies and genuinely stale jobs still get swept. The outer
|
|
171
|
-
// finally closes the adapter generator on early exits (cancellation, ownership conflict).
|
|
172
169
|
// Our own abort, as opposed to a failure that merely coincided with one. Adapters are told to
|
|
173
170
|
// return rather than throw, but `signal.throwIfAborted()` and an aborted `fetch` both surface as
|
|
174
171
|
// this, and either is a cancellation rather than a fault.
|
|
@@ -182,21 +179,33 @@ function isAbortError(error: unknown): boolean {
|
|
|
182
179
|
return typeof error === 'object' && error !== null && (error as { name?: unknown }).name === 'AbortError'
|
|
183
180
|
}
|
|
184
181
|
|
|
182
|
+
// The interval is armed once and spans the whole iteration, the `yield` included, rather than being
|
|
183
|
+
// restarted around each `next()`. Producing a batch is not the only thing that can outlast the stale
|
|
184
|
+
// sweep: the consumer body between `yield` and the next `next()` commits batch progress, refreshes
|
|
185
|
+
// coverage snapshots, logs item failures and writes the operational log, and a coverage refresh over
|
|
186
|
+
// a large query index is slow enough that a per-read timer left that window unheartbeated (#5370).
|
|
187
|
+
// The generator body does not start until the first `next()`, so a stream nobody consumes still arms
|
|
188
|
+
// nothing. Every way out of `forEachBatch` — drain, early close, a throwing read, a throwing consumer —
|
|
189
|
+
// either completes this generator or calls `return()` on it, so the `finally` below always clears the
|
|
190
|
+
// timer. That last part is a property of the consumer rather than of this helper: a consumer that
|
|
191
|
+
// abandoned the generator mid-`yield` without closing it would leak the interval, which the old
|
|
192
|
+
// per-read arming could not do. Any new consumer must honour `IteratorClose`.
|
|
193
|
+
//
|
|
194
|
+
// Every callback on this timer MUST use a forked EntityManager. The window now covers the consumer
|
|
195
|
+
// body, where `commitBatchProgress` holds the shared EM inside `em.begin()`/`em.commit()`, so a tick
|
|
196
|
+
// that read on the shared EM would issue a query into that open transaction and interleave with its
|
|
197
|
+
// UnitOfWork — the shape `packages/core/AGENTS.md` bans and `withAtomicFlush` cannot guard against.
|
|
185
198
|
async function* withHeartbeat<T>(source: AsyncIterable<T>, tick: () => void, intervalMs: number): AsyncGenerator<T, void, undefined> {
|
|
186
199
|
const iterator = source[Symbol.asyncIterator]()
|
|
200
|
+
const timer = setInterval(tick, intervalMs)
|
|
187
201
|
try {
|
|
188
202
|
while (true) {
|
|
189
|
-
const
|
|
190
|
-
let result: IteratorResult<T>
|
|
191
|
-
try {
|
|
192
|
-
result = await iterator.next()
|
|
193
|
-
} finally {
|
|
194
|
-
clearInterval(timer)
|
|
195
|
-
}
|
|
203
|
+
const result = await iterator.next()
|
|
196
204
|
if (result.done) return
|
|
197
205
|
yield result.value
|
|
198
206
|
}
|
|
199
207
|
} finally {
|
|
208
|
+
clearInterval(timer)
|
|
200
209
|
await iterator.return?.()
|
|
201
210
|
}
|
|
202
211
|
}
|
|
@@ -271,8 +280,10 @@ export function createSyncEngine(deps: EngineDeps) {
|
|
|
271
280
|
|
|
272
281
|
// Rides the heartbeat timer, which is the only thing that runs while the adapter is still
|
|
273
282
|
// producing a batch — the engine's own cancellation check sits in the batch handler and is
|
|
274
|
-
// reached only after a yield.
|
|
275
|
-
//
|
|
283
|
+
// reached only after a yield. Since that timer now spans the consumer body too, a cancel is
|
|
284
|
+
// also observed during the per-batch bookkeeping rather than waiting out the next read.
|
|
285
|
+
// Swallows its own errors because it runs on a timer, where an unhandled rejection is fatal,
|
|
286
|
+
// and stops polling once it has aborted.
|
|
276
287
|
function makeCancellationTick(progressJobId: string | null | undefined, scope: SyncScope, controller: AbortController): () => void {
|
|
277
288
|
if (!progressJobId) return () => {}
|
|
278
289
|
let inFlight = false
|
|
@@ -3,8 +3,12 @@ import type { AwilixContainer } from 'awilix'
|
|
|
3
3
|
import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'
|
|
4
4
|
import { findAndCountWithDecryption, findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
5
5
|
import { enforceCommandOptimisticLockWithGuards, enforceRecordGoneIsConflict } from '@open-mercato/shared/lib/crud/optimistic-lock-command'
|
|
6
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
7
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
6
8
|
import { SyncSchedule } from '../data/entities'
|
|
7
9
|
|
|
10
|
+
const logger = createLogger('data_sync').child({ component: 'sync-schedule-service' })
|
|
11
|
+
|
|
8
12
|
type SyncScope = {
|
|
9
13
|
organizationId: string
|
|
10
14
|
tenantId: string
|
|
@@ -236,11 +240,58 @@ export function createSyncScheduleService(em: EntityManager, schedulerService?:
|
|
|
236
240
|
}
|
|
237
241
|
|
|
238
242
|
const scheduledJobId = row.scheduledJobId ?? row.id
|
|
243
|
+
const wasEnabled = row.isEnabled
|
|
239
244
|
await requireScheduler().unregister(scheduledJobId)
|
|
240
245
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
try {
|
|
247
|
+
row.deletedAt = new Date()
|
|
248
|
+
row.isEnabled = false
|
|
249
|
+
await em.flush()
|
|
250
|
+
} catch (error) {
|
|
251
|
+
try {
|
|
252
|
+
await requireScheduler().register({
|
|
253
|
+
id: scheduledJobId,
|
|
254
|
+
name: buildScheduleName(row),
|
|
255
|
+
description: buildScheduleDescription(row),
|
|
256
|
+
scopeType: 'organization',
|
|
257
|
+
organizationId: scope.organizationId,
|
|
258
|
+
tenantId: scope.tenantId,
|
|
259
|
+
scheduleType: row.scheduleType,
|
|
260
|
+
scheduleValue: row.scheduleValue,
|
|
261
|
+
timezone: row.timezone,
|
|
262
|
+
targetType: 'queue',
|
|
263
|
+
targetQueue: 'data-sync-scheduled',
|
|
264
|
+
targetPayload: {
|
|
265
|
+
scheduleId: row.id,
|
|
266
|
+
scope,
|
|
267
|
+
},
|
|
268
|
+
requireFeature: 'data_sync.run',
|
|
269
|
+
sourceType: 'module',
|
|
270
|
+
sourceModule: 'data_sync',
|
|
271
|
+
isEnabled: wasEnabled,
|
|
272
|
+
})
|
|
273
|
+
} catch (compensationError) {
|
|
274
|
+
logger.error('Failed to restore scheduled job after a failed schedule delete', {
|
|
275
|
+
scheduledJobId,
|
|
276
|
+
scheduleId: row.id,
|
|
277
|
+
organizationId: scope.organizationId,
|
|
278
|
+
tenantId: scope.tenantId,
|
|
279
|
+
error: compensationError instanceof Error ? compensationError.message : String(compensationError),
|
|
280
|
+
})
|
|
281
|
+
getTelemetryRuntime()?.reportError(compensationError, {
|
|
282
|
+
module: 'data_sync',
|
|
283
|
+
code: 'data_sync.schedule_delete_compensation_failed',
|
|
284
|
+
attributes: {
|
|
285
|
+
scheduledJobId,
|
|
286
|
+
scheduleId: row.id,
|
|
287
|
+
organizationId: scope.organizationId,
|
|
288
|
+
tenantId: scope.tenantId,
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
throw error
|
|
293
|
+
}
|
|
294
|
+
|
|
244
295
|
return true
|
|
245
296
|
},
|
|
246
297
|
}
|
|
@@ -634,10 +634,16 @@ export function createProgressService(em: EntityManager, eventBus: { emit: (even
|
|
|
634
634
|
},
|
|
635
635
|
|
|
636
636
|
async isCancellationRequested(jobId, tenantId, organizationId) {
|
|
637
|
+
// Forked EM, for the same reason touchJobHeartbeat forks: this is polled from the
|
|
638
|
+
// keepalive timer, which spans the producer's own transactional writes on the shared
|
|
639
|
+
// EM, so an unawaited poll would issue a SELECT into an open em.begin()/em.commit()
|
|
640
|
+
// window and interleave with its UnitOfWork. Every caller only reads the flag, so a
|
|
641
|
+
// fork costs nothing and no caller depends on joining an ambient transaction.
|
|
642
|
+
//
|
|
637
643
|
// disableIdentityMap forces a fresh read: a managed copy of the job in this
|
|
638
644
|
// EntityManager (loaded by updateProgress) must never mask a cancellation
|
|
639
645
|
// requested from another process.
|
|
640
|
-
const job = await findOneWithDecryption(em, ProgressJob, {
|
|
646
|
+
const job = await findOneWithDecryption(em.fork(), ProgressJob, {
|
|
641
647
|
id: jobId,
|
|
642
648
|
tenantId,
|
|
643
649
|
...(organizationId ? { organizationId } : {}),
|