@open-mercato/enterprise 0.4.6-develop-e321a4e2a1 → 0.4.6-main-24e64eef39
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/record_locks/lib/recordLockService.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'crypto'\nimport { UniqueConstraintViolationException, type FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Knex } from 'knex'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { emitRecordLocksEvent } from '../events'\nimport {\n RecordLock,\n RecordLockConflict,\n type RecordLockStatus,\n type RecordLockReleaseReason,\n type RecordLockConflictResolution,\n type RecordLockConflictStatus,\n} from '../data/entities'\nimport {\n recordLockMutationHeaderSchema,\n type RecordLockMutationHeaders,\n type RecordLockReleaseInput as RecordLockReleasePayloadInput,\n type RecordLockSettingsInput,\n} from '../data/validators'\nimport {\n DEFAULT_RECORD_LOCK_SETTINGS,\n RECORD_LOCKS_MODULE_ID,\n RECORD_LOCKS_SETTINGS_NAME,\n isRecordLockingEnabledForResource,\n normalizeRecordLockSettings,\n type RecordLockSettings,\n type RecordLockStrategy,\n} from './config'\n\nconst ACTIVE_LOCK_STATUS: RecordLockStatus = 'active'\nconst ACTIVE_SCOPE_UNIQUE_CONSTRAINTS = new Set([\n 'record_locks_active_scope_org_unique',\n 'record_locks_active_scope_tenant_unique',\n 'record_locks_active_scope_user_org_unique',\n 'record_locks_active_scope_user_tenant_unique',\n])\nconst LOCK_CONTENTION_EVENT_TTL_MS = 15_000\nconst PARTICIPANT_REJOIN_AFTER_SAVE_SUPPRESS_MS = 20_000\nconst LOCK_CONTENTION_EVENT_MAX_ENTRIES = 2_000\nconst lockContentionEventThrottle = new Map<string, number>()\nconst LOCK_CLEANUP_INTERVAL_MS = 5 * 60 * 1000\nconst LOCK_RETENTION_MS = 3 * 24 * 60 * 60 * 1000\nconst RESOLVED_CONFLICT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\nconst PENDING_CONFLICT_RETENTION_MS = 24 * 60 * 60 * 1000\nconst LOCK_CLEANUP_STATE_TTL_MS = 24 * 60 * 60 * 1000\nconst LOCK_CLEANUP_STATE_MAX_ENTRIES = 2_000\nconst lockCleanupStateByTenant = new Map<string, { lastRunAt: number; inFlight: boolean; lastSeenAt: number }>()\n\nexport type RecordLockScope = {\n tenantId: string\n organizationId?: string | null\n userId: string\n}\n\nexport type RecordLockResource = {\n resourceKind: string\n resourceId: string\n}\n\nexport type RecordLockAcquireInput = RecordLockScope & RecordLockResource & {\n lockedByIp?: string | null\n}\n\nexport type RecordLockHeartbeatInput = RecordLockScope & RecordLockResource & {\n token: string\n}\n\nexport type RecordLockReleaseInput = RecordLockScope & RecordLockResource & {\n token?: string\n reason?: Exclude<RecordLockReleaseReason, 'expired' | 'force'>\n} & Pick<RecordLockReleasePayloadInput, 'conflictId' | 'resolution'>\n\nexport type RecordLockForceReleaseInput = RecordLockScope & RecordLockResource & {\n reason?: string | null\n}\n\nexport type RecordLockMutationValidationInput = RecordLockScope & RecordLockResource & {\n method: 'PUT' | 'DELETE'\n headers: Partial<RecordLockMutationHeaders>\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type RecordLockConflictChange = {\n field: string\n displayValue: unknown\n baseValue: unknown\n incomingValue: unknown\n mineValue: unknown\n}\n\nexport type RecordLockConflictPayload = {\n id: string\n resourceKind: string\n resourceId: string\n baseActionLogId: string | null\n incomingActionLogId: string | null\n allowIncomingOverride: boolean\n canOverrideIncoming: boolean\n resolutionOptions: Array<'accept_mine'>\n changes: RecordLockConflictChange[]\n}\n\nexport type RecordLockView = {\n id: string\n resourceKind: string\n resourceId: string\n token: string | null\n strategy: RecordLockStrategy\n status: RecordLockStatus\n lockedByUserId: string\n lockedByIp: string | null\n baseActionLogId: string | null\n lockedAt: string\n lastHeartbeatAt: string\n expiresAt: string\n participants: RecordLockParticipantView[]\n activeParticipantCount: number\n}\n\nexport type RecordLockParticipantView = {\n userId: string\n lockedByIp: string | null\n lockedAt: string\n lastHeartbeatAt: string\n expiresAt: string\n}\n\nexport type RecordLockAcquireResult = {\n ok: true\n enabled: boolean\n resourceEnabled: boolean\n strategy: RecordLockStrategy\n allowForceUnlock: boolean\n heartbeatSeconds: number\n acquired: boolean\n latestActionLogId: string | null\n lock: RecordLockView | null\n}\n\nexport type RecordLockAcquireFailure = RecordLockValidationFailure & {\n allowForceUnlock: boolean\n}\n\nexport type RecordLockHeartbeatResult = {\n ok: true\n expiresAt: string | null\n}\n\nexport type RecordLockReleaseResult = {\n ok: true\n released: boolean\n conflictResolved: boolean\n}\n\nexport type RecordLockForceReleaseResult = {\n ok: true\n released: boolean\n lock: RecordLockView | null\n}\n\nexport type RecordLockValidationSuccess = {\n ok: true\n enabled: boolean\n resourceEnabled: boolean\n strategy: RecordLockStrategy\n shouldReleaseOnSuccess: boolean\n lock: RecordLockView | null\n latestActionLogId: string | null\n}\n\nexport type RecordLockValidationFailure = {\n ok: false\n status: 409 | 423\n error: string\n code: 'record_lock_conflict' | 'record_locked'\n lock: RecordLockView | null\n conflict?: RecordLockConflictPayload\n}\n\nexport type RecordLockValidationResult = RecordLockValidationSuccess | RecordLockValidationFailure\n\nexport type RecordLockServiceDeps = {\n em: EntityManager\n moduleConfigService?: ModuleConfigService | null\n actionLogService?: ActionLogService | null\n rbacService?: RbacService | null\n}\n\nexport type ParsedRecordLockHeaders = Partial<RecordLockMutationHeaders>\n\nfunction normalizeDate(value: Date): string {\n return value.toISOString()\n}\n\nfunction trimToNull(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n}\n\nfunction normalizeScopeOrganization(value: string | null | undefined): string | null {\n const trimmed = trimToNull(value)\n return trimmed ?? null\n}\n\nfunction shouldEmitLockContentionEvent(input: {\n tenantId: string\n organizationId?: string | null\n resourceKind: string\n resourceId: string\n lockedByUserId: string\n attemptedByUserId: string\n}): boolean {\n if (process.env.NODE_ENV === 'test') return true\n const now = Date.now()\n const key = [\n input.tenantId,\n normalizeScopeOrganization(input.organizationId) ?? 'global',\n input.resourceKind,\n input.resourceId,\n input.lockedByUserId,\n input.attemptedByUserId,\n ].join('|')\n\n const lastEmittedAt = lockContentionEventThrottle.get(key)\n if (typeof lastEmittedAt === 'number' && now - lastEmittedAt < LOCK_CONTENTION_EVENT_TTL_MS) {\n return false\n }\n\n lockContentionEventThrottle.set(key, now)\n\n for (const [cachedKey, cachedAt] of lockContentionEventThrottle.entries()) {\n if (now - cachedAt > LOCK_CONTENTION_EVENT_TTL_MS) {\n lockContentionEventThrottle.delete(cachedKey)\n }\n }\n if (lockContentionEventThrottle.size > LOCK_CONTENTION_EVENT_MAX_ENTRIES) {\n const oldest = Array.from(lockContentionEventThrottle.entries())\n .sort((left, right) => left[1] - right[1])\n .slice(0, lockContentionEventThrottle.size - LOCK_CONTENTION_EVENT_MAX_ENTRIES)\n for (const [staleKey] of oldest) lockContentionEventThrottle.delete(staleKey)\n }\n\n return true\n}\n\nfunction isActiveLockScopeUniqueViolation(error: unknown): boolean {\n if (error instanceof UniqueConstraintViolationException) {\n const errorWithConstraint = error as unknown as { constraint?: unknown }\n const constraint = typeof errorWithConstraint.constraint === 'string'\n ? errorWithConstraint.constraint\n : null\n if (constraint && ACTIVE_SCOPE_UNIQUE_CONSTRAINTS.has(constraint)) return true\n }\n if (!error || typeof error !== 'object') return false\n const code = (error as { code?: unknown }).code\n if (code !== '23505') return false\n const message = typeof (error as { message?: unknown }).message === 'string'\n ? (error as { message: string }).message.toLowerCase()\n : ''\n for (const constraint of ACTIVE_SCOPE_UNIQUE_CONSTRAINTS) {\n if (message.includes(constraint)) return true\n }\n return false\n}\n\nfunction getKnex(em: EntityManager): Knex {\n return (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()\n}\n\nconst SKIPPED_CONFLICT_FIELDS = new Set([\n 'updatedAt',\n 'updated_at',\n 'createdAt',\n 'created_at',\n 'deletedAt',\n 'deleted_at',\n])\n\nfunction shouldSkipConflictField(path: string): boolean {\n if (!path.trim().length) return true\n if (SKIPPED_CONFLICT_FIELDS.has(path)) return true\n const segments = path.split('.').filter((segment) => segment.length > 0)\n if (!segments.length) return true\n return SKIPPED_CONFLICT_FIELDS.has(segments[segments.length - 1] ?? '')\n}\n\nconst MISSING_CONFLICT_VALUE = Symbol('record_lock_conflict_missing_value')\n\nfunction isRecordValue(value: unknown): value is Record<string, unknown> {\n return Boolean(value && typeof value === 'object' && !Array.isArray(value))\n}\n\nfunction toIsoDate(value: unknown): string | null {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n }\n if (typeof value === 'string') {\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n }\n return null\n}\n\nfunction valuesEqual(a: unknown, b: unknown, seen?: Set<unknown>): boolean {\n if (Object.is(a, b)) return true\n\n if (a instanceof Date || b instanceof Date) {\n const left = toIsoDate(a)\n const right = toIsoDate(b)\n return left !== null && right !== null && left === right\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let index = 0; index < a.length; index += 1) {\n if (!valuesEqual(a[index], b[index], seen)) return false\n }\n return true\n }\n\n if (isRecordValue(a) && isRecordValue(b)) {\n if (!seen) seen = new Set()\n if (seen.has(a) || seen.has(b)) return false\n seen.add(a)\n seen.add(b)\n const aKeys = Object.keys(a)\n const bKeys = Object.keys(b)\n if (aKeys.length !== bKeys.length) return false\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(b, key)) return false\n if (!valuesEqual(a[key], b[key], seen)) return false\n }\n return true\n }\n\n return false\n}\n\nfunction readPathValue(source: unknown, path: string): unknown | typeof MISSING_CONFLICT_VALUE {\n if (!path.trim().length || !isRecordValue(source)) return MISSING_CONFLICT_VALUE\n if (Object.prototype.hasOwnProperty.call(source, path)) return source[path]\n\n const segments = path.split('.').filter((segment) => segment.length > 0)\n if (!segments.length) return MISSING_CONFLICT_VALUE\n\n let current: unknown = source\n for (const segment of segments) {\n if (!isRecordValue(current)) return MISSING_CONFLICT_VALUE\n if (!Object.prototype.hasOwnProperty.call(current, segment)) return MISSING_CONFLICT_VALUE\n current = current[segment]\n }\n return current\n}\n\nfunction buildPathVariants(path: string): string[] {\n const trimmed = path.trim()\n if (!trimmed.length) return []\n\n const segments = trimmed.split('.').filter((segment) => segment.length > 0)\n if (segments.length <= 1) return [trimmed]\n\n const variants = new Set<string>([trimmed])\n for (let index = 1; index < segments.length; index += 1) {\n variants.add(segments.slice(index).join('.'))\n }\n return Array.from(variants)\n}\n\nfunction readPathValueLoose(source: unknown, path: string): unknown | typeof MISSING_CONFLICT_VALUE {\n const variants = buildPathVariants(path)\n for (const variant of variants) {\n const value = readPathValue(source, variant)\n if (value !== MISSING_CONFLICT_VALUE) return value\n }\n return MISSING_CONFLICT_VALUE\n}\n\nfunction normalizeConflictValue(value: unknown): unknown {\n return value === undefined ? null : value\n}\n\nfunction formatNotificationValue(value: unknown): string {\n if (value === null || value === undefined) return '-'\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n try {\n return JSON.stringify(value)\n } catch {\n return String(value)\n }\n}\n\nfunction formatChangedFieldLabel(rawField: string): string {\n const trimmedField = rawField.trim()\n const withoutNamespace = trimmedField.includes('::') ? (trimmedField.split('::').pop() ?? trimmedField) : trimmedField\n const withoutPrefix = withoutNamespace.includes('.') ? (withoutNamespace.split('.').pop() ?? withoutNamespace) : withoutNamespace\n const words = withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[._-]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .split(' ')\n .filter(Boolean)\n\n if (!words.length) return trimmedField\n return words\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n}\n\nexport function readRecordLockHeaders(headers: Headers): ParsedRecordLockHeaders {\n const raw = {\n resourceKind: trimToNull(headers.get('x-om-record-lock-kind')) ?? undefined,\n resourceId: trimToNull(headers.get('x-om-record-lock-resource-id')) ?? undefined,\n token: trimToNull(headers.get('x-om-record-lock-token')) ?? undefined,\n baseLogId: trimToNull(headers.get('x-om-record-lock-base-log-id')) ?? undefined,\n resolution: trimToNull(headers.get('x-om-record-lock-resolution')) ?? undefined,\n conflictId: trimToNull(headers.get('x-om-record-lock-conflict-id')) ?? undefined,\n }\n\n const parsed = recordLockMutationHeaderSchema.partial().safeParse(raw)\n if (!parsed.success) return {}\n return parsed.data\n}\n\nexport class RecordLockService {\n private readonly em: EntityManager\n\n private readonly moduleConfigService: ModuleConfigService | null\n\n private readonly actionLogService: ActionLogService | null\n\n private readonly rbacService: RbacService | null\n\n constructor(deps: RecordLockServiceDeps) {\n this.em = deps.em\n this.moduleConfigService = deps.moduleConfigService ?? null\n this.actionLogService = deps.actionLogService ?? null\n this.rbacService = deps.rbacService ?? null\n }\n\n async getSettings(): Promise<RecordLockSettings> {\n if (!this.moduleConfigService) return DEFAULT_RECORD_LOCK_SETTINGS\n\n const value = await this.moduleConfigService.getValue<RecordLockSettings>(\n RECORD_LOCKS_MODULE_ID,\n RECORD_LOCKS_SETTINGS_NAME,\n { defaultValue: DEFAULT_RECORD_LOCK_SETTINGS },\n )\n\n return normalizeRecordLockSettings(value ?? DEFAULT_RECORD_LOCK_SETTINGS)\n }\n\n async saveSettings(input: RecordLockSettingsInput): Promise<RecordLockSettings> {\n const settings = normalizeRecordLockSettings(input)\n if (!this.moduleConfigService) return settings\n\n await this.moduleConfigService.setValue(RECORD_LOCKS_MODULE_ID, RECORD_LOCKS_SETTINGS_NAME, settings)\n return settings // NOSONAR \u2014 both paths return settings by design; the branch controls persistence\n }\n\n async acquire(input: RecordLockAcquireInput): Promise<RecordLockAcquireResult | RecordLockAcquireFailure> {\n this.scheduleCleanup(input.tenantId)\n const settings = await this.getSettings()\n const latest = await this.findLatestActionLogWithScopeFallback(input)\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n\n if (!resourceEnabled) {\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: false,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: null,\n }\n }\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n const ownedActiveLock = activeLocks.find((lock) => lock.lockedByUserId === input.userId) ?? null\n const competingActiveLock = activeLocks.find((lock) => lock.lockedByUserId !== input.userId) ?? null\n\n if (settings.strategy === 'pessimistic' && !ownedActiveLock && competingActiveLock) {\n const lockView = this.toLockView(competingActiveLock, false, activeLocks)\n if (shouldEmitLockContentionEvent({\n tenantId: competingActiveLock.tenantId,\n organizationId: competingActiveLock.organizationId,\n resourceKind: competingActiveLock.resourceKind,\n resourceId: competingActiveLock.resourceId,\n lockedByUserId: competingActiveLock.lockedByUserId,\n attemptedByUserId: input.userId,\n })) {\n await emitRecordLocksEvent('record_locks.lock.contended', {\n lockId: competingActiveLock.id,\n resourceKind: competingActiveLock.resourceKind,\n resourceId: competingActiveLock.resourceId,\n tenantId: competingActiveLock.tenantId,\n organizationId: competingActiveLock.organizationId,\n lockedByUserId: competingActiveLock.lockedByUserId,\n attemptedByUserId: input.userId,\n })\n }\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n allowForceUnlock: settings.allowForceUnlock,\n lock: lockView,\n }\n }\n\n if (ownedActiveLock) {\n ownedActiveLock.strategy = settings.strategy\n ownedActiveLock.lockedByIp = input.lockedByIp ?? ownedActiveLock.lockedByIp ?? null\n ownedActiveLock.lastHeartbeatAt = now\n ownedActiveLock.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n\n activeLocks = await this.findActiveLocks(input, now)\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: this.toLockView(ownedActiveLock, true, activeLocks),\n }\n }\n\n const lock = this.em.create(RecordLock, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: randomUUID(),\n strategy: settings.strategy,\n status: ACTIVE_LOCK_STATUS,\n lockedByUserId: input.userId,\n lockedByIp: input.lockedByIp ?? null,\n baseActionLogId: latest?.id ?? null,\n lockedAt: now,\n lastHeartbeatAt: now,\n expiresAt: new Date(now.getTime() + settings.timeoutSeconds * 1000),\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n })\n\n this.em.persist(lock)\n let createdNewLock = true\n try {\n await this.em.flush()\n } catch (error) {\n if (!isActiveLockScopeUniqueViolation(error)) throw error\n const clear = (this.em as { clear?: () => void }).clear\n if (typeof clear === 'function') clear.call(this.em)\n const locksAfterCollision = await this.findActiveLocks(input, now)\n const competingAfterCollision = locksAfterCollision.find((item) => item.lockedByUserId !== input.userId) ?? null\n if (settings.strategy === 'pessimistic' && competingAfterCollision) {\n if (shouldEmitLockContentionEvent({\n tenantId: competingAfterCollision.tenantId,\n organizationId: competingAfterCollision.organizationId,\n resourceKind: competingAfterCollision.resourceKind,\n resourceId: competingAfterCollision.resourceId,\n lockedByUserId: competingAfterCollision.lockedByUserId,\n attemptedByUserId: input.userId,\n })) {\n await emitRecordLocksEvent('record_locks.lock.contended', {\n lockId: competingAfterCollision.id,\n resourceKind: competingAfterCollision.resourceKind,\n resourceId: competingAfterCollision.resourceId,\n tenantId: competingAfterCollision.tenantId,\n organizationId: competingAfterCollision.organizationId,\n lockedByUserId: competingAfterCollision.lockedByUserId,\n attemptedByUserId: input.userId,\n })\n }\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n allowForceUnlock: settings.allowForceUnlock,\n lock: this.toLockView(competingAfterCollision, false, locksAfterCollision),\n }\n }\n const existingOwned = await this.findOwnedActiveLock(input)\n if (!existingOwned) throw error\n existingOwned.strategy = settings.strategy\n existingOwned.lockedByIp = input.lockedByIp ?? existingOwned.lockedByIp ?? null\n existingOwned.lastHeartbeatAt = now\n existingOwned.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n createdNewLock = false\n }\n\n const activeAfterAcquire = await this.findActiveLocks(input, now)\n const ownedAfterAcquire = activeAfterAcquire.find((item) => item.lockedByUserId === input.userId)\n ?? await this.findOwnedActiveLock(input)\n ?? lock\n ?? null\n if (!ownedAfterAcquire) {\n const fallbackLock = activeAfterAcquire[0] ?? null\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: fallbackLock ? this.toLockView(fallbackLock, false, activeAfterAcquire) : null,\n }\n }\n\n if (createdNewLock) {\n await emitRecordLocksEvent('record_locks.lock.acquired', {\n lockId: ownedAfterAcquire.id,\n resourceKind: ownedAfterAcquire.resourceKind,\n resourceId: ownedAfterAcquire.resourceId,\n tenantId: ownedAfterAcquire.tenantId,\n organizationId: ownedAfterAcquire.organizationId,\n lockedByUserId: ownedAfterAcquire.lockedByUserId,\n strategy: ownedAfterAcquire.strategy,\n baseActionLogId: ownedAfterAcquire.baseActionLogId,\n activeParticipantCount: activeAfterAcquire.length,\n })\n\n const recipientUserIds = activeAfterAcquire\n .filter((item) => item.lockedByUserId !== input.userId)\n .map((item) => item.lockedByUserId)\n const shouldSuppressJoinNotification = await this.hasRecentSavedRelease({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n now,\n })\n\n if (!shouldSuppressJoinNotification) {\n await emitRecordLocksEvent('record_locks.participant.joined', {\n lockId: ownedAfterAcquire.id,\n resourceKind: ownedAfterAcquire.resourceKind,\n resourceId: ownedAfterAcquire.resourceId,\n tenantId: ownedAfterAcquire.tenantId,\n organizationId: ownedAfterAcquire.organizationId,\n joinedUserId: input.userId,\n joinedIp: ownedAfterAcquire.lockedByIp ?? null,\n recipientUserIds,\n activeParticipantCount: activeAfterAcquire.length,\n })\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: createdNewLock,\n latestActionLogId: latest?.id ?? null,\n lock: this.toLockView(ownedAfterAcquire, true, activeAfterAcquire),\n }\n }\n\n async heartbeat(input: RecordLockHeartbeatInput): Promise<RecordLockHeartbeatResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n if (!resourceEnabled) return { ok: true, expiresAt: null }\n\n const lock = await this.findOwnedLockByToken(input)\n if (!lock) return { ok: true, expiresAt: null }\n\n const now = new Date()\n if (lock.expiresAt <= now) {\n this.markLockReleased(lock, {\n status: 'expired',\n reason: 'expired',\n releasedByUserId: lock.lockedByUserId,\n now,\n })\n await this.em.flush()\n return { ok: true, expiresAt: null }\n }\n\n lock.lastHeartbeatAt = now\n lock.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n return { ok: true, expiresAt: normalizeDate(lock.expiresAt) }\n }\n\n async release(input: RecordLockReleaseInput): Promise<RecordLockReleaseResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n if (!resourceEnabled) return { ok: true, released: false, conflictResolved: false }\n\n let conflictResolved = false\n if (input.reason === 'conflict_resolved' && input.conflictId && input.resolution === 'accept_incoming') {\n const conflict = await this.findConflictById(input.conflictId, input)\n if (conflict && conflict.status === 'pending' && conflict.conflictActorUserId === input.userId) {\n await this.resolveConflict(conflict, input.resolution, input.userId)\n conflictResolved = true\n }\n }\n\n const lock = input.token\n ? await this.findOwnedLockByToken(input)\n : await this.findOwnedActiveLock(input)\n if (!lock) return { ok: true, released: false, conflictResolved }\n\n const now = new Date()\n this.markLockReleased(lock, {\n status: 'released',\n reason: input.reason ?? 'cancelled',\n releasedByUserId: input.userId,\n now,\n })\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.lock.released', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n lockedByUserId: lock.lockedByUserId,\n releasedByUserId: input.userId,\n reason: lock.releaseReason,\n })\n\n if (lock.releaseReason === 'unmount') {\n const remainingActiveLocks = await this.findActiveLocks(input, now)\n const recipientUserIds = remainingActiveLocks\n .map((activeLock) => activeLock.lockedByUserId)\n .filter((userId) => userId !== lock.lockedByUserId)\n\n if (recipientUserIds.length) {\n await emitRecordLocksEvent('record_locks.participant.left', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n leftUserId: lock.lockedByUserId,\n reason: 'unmount',\n recipientUserIds,\n activeParticipantCount: remainingActiveLocks.length,\n })\n }\n }\n\n return { ok: true, released: true, conflictResolved }\n }\n\n async forceRelease(input: RecordLockForceReleaseInput): Promise<RecordLockForceReleaseResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n const canForceRelease = await this.canUserForceRelease(input, settings)\n if (!resourceEnabled || !settings.allowForceUnlock || !canForceRelease) {\n return { ok: true, released: false, lock: null }\n }\n\n const now = new Date()\n const activeLocks = this.sortLocksByJoinOrder(await this.findActiveLocks(input, now))\n const lock = activeLocks[0] ?? null\n if (!lock) return { ok: true, released: false, lock: null }\n\n this.markLockReleased(lock, {\n status: 'force_released',\n reason: 'force',\n releasedByUserId: input.userId,\n now,\n })\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.lock.force_released', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n lockedByUserId: lock.lockedByUserId,\n releasedByUserId: input.userId,\n reason: input.reason ?? null,\n })\n\n const remainingLocks = this.sortLocksByJoinOrder(activeLocks.filter((item) => item.id !== lock.id))\n const nextInQueue = remainingLocks[0] ?? null\n return { ok: true, released: true, lock: nextInQueue ? this.toLockView(nextInQueue, false, remainingLocks) : null }\n }\n\n async validateMutation(input: RecordLockMutationValidationInput): Promise<RecordLockValidationResult> {\n this.scheduleCleanup(input.tenantId)\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n const canOverrideIncoming = await this.canUserOverrideIncoming(input, settings)\n\n if (!resourceEnabled) {\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: false,\n strategy: settings.strategy,\n shouldReleaseOnSuccess: false,\n lock: null,\n latestActionLogId: null,\n }\n }\n\n const parsedHeaders = this.normalizeMutationHeaders(input.headers)\n const keepMineResolution = parsedHeaders.resolution === 'accept_mine' || parsedHeaders.resolution === 'merged'\n ? parsedHeaders.resolution\n : null\n const hasKeepMineIntent = keepMineResolution !== null\n const now = new Date()\n const activeLocks = await this.findActiveLocks(input, now)\n const ownedActiveLock = activeLocks.find((lock) => lock.lockedByUserId === input.userId) ?? null\n const competingLock = activeLocks.find((lock) => lock.lockedByUserId !== input.userId) ?? null\n const latest = await this.findLatestActionLogWithScopeFallback(input)\n const shouldReleaseOnSuccess = Boolean(\n ownedActiveLock\n && (!parsedHeaders.token || ownedActiveLock.token === parsedHeaders.token),\n )\n\n if (settings.strategy === 'pessimistic') {\n if (competingLock && !ownedActiveLock) {\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n lock: this.toLockView(competingLock, false, activeLocks),\n }\n }\n\n if (ownedActiveLock) {\n if (parsedHeaders.token && ownedActiveLock.token !== parsedHeaders.token) {\n return {\n ok: false,\n status: 423,\n error: 'Valid lock token is required for this mutation',\n code: 'record_locked',\n lock: this.toLockView(ownedActiveLock, false, activeLocks),\n }\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n const existingConflict = parsedHeaders.conflictId\n ? await this.findConflictById(parsedHeaders.conflictId, input)\n : null\n\n if (existingConflict) {\n const canResolveExistingConflict = existingConflict.status === 'pending'\n && existingConflict.conflictActorUserId === input.userId\n\n if (parsedHeaders.resolution === 'accept_mine' || parsedHeaders.resolution === 'merged') {\n const isAlreadyResolvedByRequester = existingConflict.conflictActorUserId === input.userId\n && existingConflict.status !== 'pending'\n && existingConflict.resolution === parsedHeaders.resolution\n\n if (!canResolveExistingConflict && !isAlreadyResolvedByRequester) {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n if (!canOverrideIncoming) {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n if (canResolveExistingConflict) {\n await this.resolveConflict(existingConflict, parsedHeaders.resolution, input.userId)\n }\n } else {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n }\n\n if (!existingConflict) {\n const baseActionLogId = parsedHeaders.baseLogId\n ?? (ownedActiveLock ? ownedActiveLock.baseActionLogId : null)\n\n const hasConflictingBaseLog = Boolean(\n latest?.id\n && baseActionLogId\n && latest.id !== baseActionLogId\n )\n const hasConflictingWriteAfterLockStart = Boolean(\n latest?.id\n && !baseActionLogId\n && ownedActiveLock\n && latest.createdAt instanceof Date\n && ownedActiveLock.lockedAt instanceof Date\n && latest.createdAt.getTime() > ownedActiveLock.lockedAt.getTime()\n && latest.actorUserId !== input.userId\n )\n const isConflictingWrite = hasConflictingBaseLog || hasConflictingWriteAfterLockStart\n\n if (isConflictingWrite) {\n if (keepMineResolution && canOverrideIncoming) {\n const autoResolvedConflict = await this.createConflict({\n scope: input,\n baseActionLogId,\n incomingActionLogId: latest?.id ?? null,\n conflictActorUserId: input.userId,\n incomingActorUserId: latest?.actorUserId ?? null,\n })\n await this.resolveConflict(autoResolvedConflict, keepMineResolution, input.userId)\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n const conflict = await this.createConflict({\n scope: input,\n baseActionLogId,\n incomingActionLogId: latest?.id ?? null,\n conflictActorUserId: input.userId,\n incomingActorUserId: latest?.actorUserId ?? null,\n })\n\n return {\n ok: false,\n status: 409,\n error: 'Record conflict detected',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(conflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n async releaseAfterMutation(input: RecordLockReleaseInput): Promise<void> {\n const releaseResult = await this.release({\n ...input,\n reason: input.reason ?? 'saved',\n })\n if (!releaseResult.released) return\n }\n\n async emitIncomingChangesNotificationAfterMutation(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n method: 'PUT' | 'DELETE'\n }): Promise<void> {\n if (input.method !== 'PUT') return\n const settings = await this.getSettings()\n if (!settings.notifyOnConflict || !isRecordLockingEnabledForResource(settings, input.resourceKind)) return\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n if (!activeLocks.length) {\n const fallbackWhere: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n const fallbackLocks = await this.em.find(RecordLock, fallbackWhere, { orderBy: { updatedAt: 'desc' } })\n activeLocks = Array.isArray(fallbackLocks) ? fallbackLocks : []\n }\n\n const recipientUserIds = new Set<string>()\n for (const lock of activeLocks) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n if (!recipientUserIds.size) {\n const fallbackWindowMs = Math.max((settings.timeoutSeconds ?? 300) * 1000, 60_000)\n const fallbackSince = new Date(now.getTime() - fallbackWindowMs)\n const recentLocks = await this.em.find(RecordLock, {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n updatedAt: { $gte: fallbackSince },\n }, { orderBy: { updatedAt: 'desc' }, limit: 50 })\n\n for (const lock of (Array.isArray(recentLocks) ? recentLocks : [])) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n }\n if (!recipientUserIds.size) return\n\n let latest = await this.findLatestActionLog(input)\n if (!latest) {\n latest = await this.findLatestActionLog({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n }\n let actorLog = latest?.actorUserId === input.userId\n ? latest\n : await this.findLatestActionLogByActor(input, input.userId)\n if (!actorLog) {\n actorLog = await this.findLatestActionLogByActor({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n }, input.userId)\n }\n const incomingLog = actorLog ?? latest\n\n const changedFields = incomingLog\n ? this.summarizeChangedFieldsFromActionLog(incomingLog)\n : ''\n const changedRows = this.buildIncomingChangeRowsFromActionLog(incomingLog)\n const changedRowsJson = changedRows.length ? JSON.stringify(changedRows) : ''\n\n await emitRecordLocksEvent('record_locks.incoming_changes.available', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n incomingActorUserId: input.userId,\n incomingActionLogId: incomingLog?.id ?? null,\n recipientUserIds: Array.from(recipientUserIds),\n changedFields: changedFields || '-',\n changedRowsJson,\n })\n }\n\n async emitRecordDeletedNotificationAfterMutation(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n method: 'PUT' | 'DELETE'\n }): Promise<void> {\n if (input.method !== 'DELETE') return\n const settings = await this.getSettings()\n if (!isRecordLockingEnabledForResource(settings, input.resourceKind)) return\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n if (!activeLocks.length) {\n const fallbackWhere: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n const fallbackLocks = await this.em.find(RecordLock, fallbackWhere, { orderBy: { updatedAt: 'desc' } })\n activeLocks = Array.isArray(fallbackLocks) ? fallbackLocks : []\n }\n\n const recipientUserIds = new Set<string>()\n for (const lock of activeLocks) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n\n if (!recipientUserIds.size) {\n const fallbackWindowMs = Math.max((settings.timeoutSeconds ?? 300) * 1000, 60_000)\n const fallbackSince = new Date(now.getTime() - fallbackWindowMs)\n const recentLocks = await this.em.find(RecordLock, {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n updatedAt: { $gte: fallbackSince },\n }, { orderBy: { updatedAt: 'desc' }, limit: 50 })\n\n for (const lock of (Array.isArray(recentLocks) ? recentLocks : [])) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n }\n if (!recipientUserIds.size) return\n\n await emitRecordLocksEvent('record_locks.record.deleted', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n deletedByUserId: input.userId,\n recipientUserIds: Array.from(recipientUserIds),\n })\n }\n\n async resolveConflictById(input: {\n conflictId: string\n tenantId: string\n organizationId?: string | null\n userId: string\n resolution: 'accept_incoming' | 'accept_mine' | 'merged'\n }): Promise<boolean> {\n const settings = await this.getSettings()\n const canOverrideIncoming = await this.canUserOverrideIncoming(input, settings)\n const conflict = await this.em.findOne(RecordLockConflict, {\n id: input.conflictId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n deletedAt: null,\n })\n if (!conflict || conflict.status !== 'pending' || conflict.conflictActorUserId !== input.userId) {\n return false\n }\n if ((input.resolution === 'accept_mine' || input.resolution === 'merged') && !canOverrideIncoming) {\n return false\n }\n await this.resolveConflict(conflict, input.resolution, input.userId)\n return true\n }\n\n private async canUserOverrideIncoming(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'>,\n settings: RecordLockSettings,\n ): Promise<boolean> {\n if (!settings.allowIncomingOverride) return false\n if (!this.rbacService) return false\n\n try {\n return await this.rbacService.userHasAllFeatures(\n input.userId,\n ['record_locks.override_incoming'],\n {\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n },\n )\n } catch {\n return false\n }\n }\n\n private async canUserForceRelease(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'>,\n settings: RecordLockSettings,\n ): Promise<boolean> {\n if (!settings.allowForceUnlock) return false\n if (!this.rbacService) return false\n\n try {\n return await this.rbacService.userHasAllFeatures(\n input.userId,\n ['record_locks.force_release'],\n {\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n },\n )\n } catch {\n return false\n }\n }\n\n private pruneLockCleanupState(now: number): void {\n for (const [tenantId, state] of lockCleanupStateByTenant.entries()) {\n if (!state.inFlight && now - state.lastSeenAt > LOCK_CLEANUP_STATE_TTL_MS) {\n lockCleanupStateByTenant.delete(tenantId)\n }\n }\n\n if (lockCleanupStateByTenant.size <= LOCK_CLEANUP_STATE_MAX_ENTRIES) return\n\n const removable = Array.from(lockCleanupStateByTenant.entries())\n .filter(([, state]) => !state.inFlight)\n .sort((left, right) => left[1].lastSeenAt - right[1].lastSeenAt)\n const overflow = lockCleanupStateByTenant.size - LOCK_CLEANUP_STATE_MAX_ENTRIES\n for (const [tenantId] of removable.slice(0, Math.max(0, overflow))) {\n lockCleanupStateByTenant.delete(tenantId)\n }\n }\n\n private scheduleCleanup(tenantId: string): void {\n const now = Date.now()\n this.pruneLockCleanupState(now)\n const state = lockCleanupStateByTenant.get(tenantId) ?? { lastRunAt: 0, inFlight: false, lastSeenAt: now }\n state.lastSeenAt = now\n lockCleanupStateByTenant.set(tenantId, state)\n if (state.inFlight) return\n if (now - state.lastRunAt < LOCK_CLEANUP_INTERVAL_MS) return\n\n state.inFlight = true\n state.lastRunAt = now\n lockCleanupStateByTenant.set(tenantId, state)\n\n void this.cleanupHistoricalRecords(tenantId).finally(() => {\n const current = lockCleanupStateByTenant.get(tenantId)\n if (!current) return\n current.inFlight = false\n lockCleanupStateByTenant.set(tenantId, current)\n })\n }\n\n private async cleanupHistoricalRecords(tenantId: string): Promise<void> {\n try {\n const knex = getKnex(this.em)\n const now = Date.now()\n const lockCutoff = new Date(now - LOCK_RETENTION_MS)\n const resolvedConflictCutoff = new Date(now - RESOLVED_CONFLICT_RETENTION_MS)\n const pendingConflictCutoff = new Date(now - PENDING_CONFLICT_RETENTION_MS)\n const deletedAt = new Date(now)\n\n await knex('record_locks')\n .where({ tenant_id: tenantId })\n .whereNull('deleted_at')\n .whereNot('status', ACTIVE_LOCK_STATUS)\n .andWhere('updated_at', '<', lockCutoff)\n .update({\n deleted_at: deletedAt,\n updated_at: deletedAt,\n })\n\n await knex('record_lock_conflicts')\n .where({ tenant_id: tenantId })\n .whereNull('deleted_at')\n .andWhere((query) => {\n query\n .where((pending) => {\n pending.where('status', 'pending').andWhere('created_at', '<', pendingConflictCutoff)\n })\n .orWhere((resolved) => {\n resolved.whereNot('status', 'pending').andWhere('updated_at', '<', resolvedConflictCutoff)\n })\n })\n .update({\n deleted_at: deletedAt,\n updated_at: deletedAt,\n })\n } catch {\n // Best-effort cleanup must never fail lock workflows.\n }\n }\n\n private normalizeMutationHeaders(headers: Partial<RecordLockMutationHeaders>): Partial<RecordLockMutationHeaders> {\n const parsed = recordLockMutationHeaderSchema.partial().safeParse(headers)\n if (!parsed.success) return {}\n return parsed.data\n }\n\n private buildScopeWhere(scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'>): {\n tenantId: string\n deletedAt: null\n organizationId?: string | null\n } {\n const where: {\n tenantId: string\n deletedAt: null\n organizationId?: string | null\n } = {\n tenantId: scope.tenantId,\n deletedAt: null,\n }\n\n if (scope.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(scope.organizationId)\n }\n\n return where\n }\n\n private async findActiveLocks(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n now: Date,\n ): Promise<RecordLock[]> {\n const legacyFinder = (this as unknown as {\n findActiveLock?: (args: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource, at: Date) => Promise<RecordLock | null>\n }).findActiveLock\n if (typeof legacyFinder === 'function') {\n const legacyResult = await legacyFinder(input, now)\n return legacyResult ? [legacyResult] : []\n }\n\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n\n const locks = await this.em.find(RecordLock, where, { orderBy: { updatedAt: 'desc' } })\n if (!Array.isArray(locks) || !locks.length) return []\n\n let dirty = false\n const active: RecordLock[] = []\n const expiredLocks: RecordLock[] = []\n\n for (const lock of locks) {\n if (lock.expiresAt <= now) {\n this.markLockReleased(lock, {\n status: 'expired',\n reason: 'expired',\n releasedByUserId: lock.lockedByUserId,\n now,\n })\n dirty = true\n expiredLocks.push(lock)\n continue\n }\n\n active.push(lock)\n }\n\n if (dirty) await this.em.flush()\n if (expiredLocks.length) {\n const recipientUserIds = active.map((lock) => lock.lockedByUserId)\n for (const expiredLock of expiredLocks) {\n await emitRecordLocksEvent('record_locks.participant.left', {\n lockId: expiredLock.id,\n resourceKind: expiredLock.resourceKind,\n resourceId: expiredLock.resourceId,\n tenantId: expiredLock.tenantId,\n organizationId: expiredLock.organizationId,\n leftUserId: expiredLock.lockedByUserId,\n reason: 'expired',\n recipientUserIds,\n activeParticipantCount: active.length,\n })\n }\n }\n return active\n }\n\n private async findOwnedLockByToken(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'> & RecordLockResource & { token?: string },\n ): Promise<RecordLock | null> {\n if (!input.token) return null\n\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: input.token,\n lockedByUserId: input.userId,\n status: ACTIVE_LOCK_STATUS,\n }\n\n return this.em.findOne(RecordLock, where)\n }\n\n private async findOwnedActiveLock(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'> & RecordLockResource,\n ): Promise<RecordLock | null> {\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: ACTIVE_LOCK_STATUS,\n }\n return this.em.findOne(RecordLock, where)\n }\n\n private async hasRecentSavedRelease(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n now: Date\n }): Promise<boolean> {\n const cutoff = new Date(input.now.getTime() - PARTICIPANT_REJOIN_AFTER_SAVE_SUPPRESS_MS)\n const where: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: 'released',\n releaseReason: 'saved',\n deletedAt: null,\n releasedAt: { $gte: cutoff },\n }\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n const scoped = await this.em.findOne(RecordLock, where, { orderBy: { releasedAt: 'desc' } })\n if (scoped) return true\n if (input.organizationId === undefined) return false\n\n return Boolean(await this.em.findOne(RecordLock, {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: 'released',\n releaseReason: 'saved',\n deletedAt: null,\n releasedAt: { $gte: cutoff },\n }, { orderBy: { releasedAt: 'desc' } }))\n }\n\n private markLockReleased(\n lock: RecordLock,\n params: {\n status: RecordLockStatus\n reason: RecordLockReleaseReason\n releasedByUserId: string\n now: Date\n },\n ) {\n lock.status = params.status\n lock.releaseReason = params.reason\n lock.releasedByUserId = params.releasedByUserId\n lock.releasedAt = params.now\n lock.updatedAt = params.now\n }\n\n private async findLatestActionLog(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n const where: FilterQuery<ActionLog> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n deletedAt: null,\n }\n\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n return this.em.findOne(ActionLog, where, { orderBy: { createdAt: 'desc' } })\n }\n\n private async findLatestActionLogWithScopeFallback(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n const scoped = await this.findLatestActionLog(input)\n if (scoped) return scoped\n if (input.organizationId !== null) return null\n\n return this.findLatestActionLog({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n }\n\n private async findLatestActionLogByActor(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n actorUserId: string,\n ): Promise<ActionLog | null> {\n const where: FilterQuery<ActionLog> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n actorUserId,\n deletedAt: null,\n }\n\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n return this.em.findOne(ActionLog, where, { orderBy: { createdAt: 'desc' } })\n }\n\n private summarizeChangedFieldsFromActionLog(log: ActionLog | null): string {\n if (!log) return ''\n\n if (isRecordValue(log.changesJson)) {\n const fromChanges = Object.keys(log.changesJson)\n .filter((field) => !shouldSkipConflictField(field))\n .slice(0, 12)\n .map(formatChangedFieldLabel)\n .join(', ')\n if (fromChanges) return fromChanges\n }\n\n const before = isRecordValue(log.snapshotBefore) ? log.snapshotBefore : null\n const after = isRecordValue(log.snapshotAfter) ? log.snapshotAfter : null\n if (!before || !after) return ''\n\n const diffPaths = new Set<string>()\n this.collectSnapshotDiffPaths(before, after, null, diffPaths, new Set<unknown>())\n\n return Array.from(diffPaths)\n .filter((field) => !shouldSkipConflictField(field))\n .sort((left, right) => left.localeCompare(right))\n .slice(0, 12)\n .map(formatChangedFieldLabel)\n .join(', ')\n }\n\n private buildIncomingChangeRowsFromActionLog(log: ActionLog | null): Array<{\n field: string\n incoming: string\n current: string\n }> {\n if (!log || !isRecordValue(log.changesJson)) return []\n\n const rows: Array<{ field: string; incoming: string; current: string }> = []\n for (const [rawField, rawChange] of Object.entries(log.changesJson)) {\n if (rows.length >= 12) break\n if (shouldSkipConflictField(rawField)) continue\n\n const change = isRecordValue(rawChange) ? rawChange : {}\n const incoming = Object.prototype.hasOwnProperty.call(change, 'to')\n ? formatNotificationValue(change.to)\n : '-'\n const current = Object.prototype.hasOwnProperty.call(change, 'from')\n ? formatNotificationValue(change.from)\n : '-'\n\n rows.push({\n field: formatChangedFieldLabel(rawField),\n incoming,\n current,\n })\n }\n\n return rows\n }\n\n private toParticipantView(lock: RecordLock): RecordLockParticipantView {\n return {\n userId: lock.lockedByUserId,\n lockedByIp: lock.lockedByIp ?? null,\n lockedAt: normalizeDate(lock.lockedAt),\n lastHeartbeatAt: normalizeDate(lock.lastHeartbeatAt),\n expiresAt: normalizeDate(lock.expiresAt),\n }\n }\n\n private sortLocksByJoinOrder(locks: RecordLock[]): RecordLock[] {\n return [...locks].sort((left, right) => {\n const leftLockedAt = left.lockedAt instanceof Date ? left.lockedAt.getTime() : 0\n const rightLockedAt = right.lockedAt instanceof Date ? right.lockedAt.getTime() : 0\n if (leftLockedAt !== rightLockedAt) return leftLockedAt - rightLockedAt\n\n const leftCreatedAt = left.createdAt instanceof Date ? left.createdAt.getTime() : 0\n const rightCreatedAt = right.createdAt instanceof Date ? right.createdAt.getTime() : 0\n if (leftCreatedAt !== rightCreatedAt) return leftCreatedAt - rightCreatedAt\n\n return left.id.localeCompare(right.id)\n })\n }\n\n private toLockView(lock: RecordLock, includeToken: boolean, activeLocks: RecordLock[] = [lock]): RecordLockView {\n const participants = activeLocks.map((item) => this.toParticipantView(item))\n return {\n id: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n token: includeToken ? lock.token : null,\n strategy: lock.strategy,\n status: lock.status,\n lockedByUserId: lock.lockedByUserId,\n lockedByIp: lock.lockedByIp ?? null,\n baseActionLogId: lock.baseActionLogId,\n lockedAt: normalizeDate(lock.lockedAt),\n lastHeartbeatAt: normalizeDate(lock.lastHeartbeatAt),\n expiresAt: normalizeDate(lock.expiresAt),\n participants,\n activeParticipantCount: participants.length,\n }\n }\n\n private async createConflict(input: {\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource\n baseActionLogId: string | null\n incomingActionLogId: string | null\n conflictActorUserId: string\n incomingActorUserId: string | null\n }): Promise<RecordLockConflict> {\n const dedupeKey = [\n 'record_locks',\n 'conflict',\n input.scope.tenantId,\n normalizeScopeOrganization(input.scope.organizationId) ?? 'global',\n input.scope.resourceKind,\n input.scope.resourceId,\n input.conflictActorUserId,\n input.baseActionLogId ?? 'none',\n input.incomingActionLogId ?? 'none',\n ].join(':')\n\n const result = await this.em.transactional(async (tx) => {\n try {\n const knex = getKnex(tx as EntityManager)\n await knex.raw('select pg_advisory_xact_lock(hashtext(?))', [dedupeKey])\n } catch {\n // Best-effort lock; fallback to find-first behavior below.\n }\n\n const existing = await this.findPendingConflictByFingerprint(tx as EntityManager, input)\n if (existing) {\n return { conflict: existing, created: false as const }\n }\n\n const conflict = tx.create(RecordLockConflict, {\n resourceKind: input.scope.resourceKind,\n resourceId: input.scope.resourceId,\n status: 'pending',\n resolution: null,\n baseActionLogId: input.baseActionLogId,\n incomingActionLogId: input.incomingActionLogId,\n conflictActorUserId: input.conflictActorUserId,\n incomingActorUserId: input.incomingActorUserId,\n tenantId: input.scope.tenantId,\n organizationId: normalizeScopeOrganization(input.scope.organizationId),\n })\n\n tx.persist(conflict)\n await tx.flush()\n return { conflict, created: true as const }\n })\n\n if (result.created) {\n await emitRecordLocksEvent('record_locks.conflict.detected', {\n conflictId: result.conflict.id,\n resourceKind: result.conflict.resourceKind,\n resourceId: result.conflict.resourceId,\n tenantId: result.conflict.tenantId,\n organizationId: result.conflict.organizationId,\n conflictActorUserId: result.conflict.conflictActorUserId,\n incomingActorUserId: result.conflict.incomingActorUserId,\n baseActionLogId: result.conflict.baseActionLogId,\n incomingActionLogId: result.conflict.incomingActionLogId,\n })\n }\n\n return result.conflict\n }\n\n private findPendingConflictByFingerprint(\n em: EntityManager,\n input: {\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource\n baseActionLogId: string | null\n incomingActionLogId: string | null\n conflictActorUserId: string\n },\n ): Promise<RecordLockConflict | null> {\n return em.findOne(RecordLockConflict, {\n tenantId: input.scope.tenantId,\n organizationId: normalizeScopeOrganization(input.scope.organizationId),\n resourceKind: input.scope.resourceKind,\n resourceId: input.scope.resourceId,\n conflictActorUserId: input.conflictActorUserId,\n status: 'pending',\n baseActionLogId: input.baseActionLogId,\n incomingActionLogId: input.incomingActionLogId,\n deletedAt: null,\n }, { orderBy: { createdAt: 'desc' } })\n }\n\n private async resolveConflict(\n conflict: RecordLockConflict,\n resolution: 'accept_incoming' | Extract<RecordLockMutationHeaders['resolution'], 'accept_mine' | 'merged'>,\n resolvedByUserId: string,\n ): Promise<void> {\n const now = new Date()\n\n const resolutionMap: Record<'accept_incoming' | Extract<RecordLockMutationHeaders['resolution'], 'accept_mine' | 'merged'>, {\n status: RecordLockConflictStatus\n resolution: RecordLockConflictResolution\n }> = {\n accept_incoming: { status: 'resolved_accept_incoming', resolution: 'accept_incoming' },\n accept_mine: { status: 'resolved_accept_mine', resolution: 'accept_mine' },\n merged: { status: 'resolved_merged', resolution: 'merged' },\n }\n\n const target = resolutionMap[resolution]\n conflict.status = target.status\n conflict.resolution = target.resolution\n conflict.resolvedByUserId = resolvedByUserId\n conflict.resolvedAt = now\n conflict.updatedAt = now\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.conflict.resolved', {\n conflictId: conflict.id,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n tenantId: conflict.tenantId,\n organizationId: conflict.organizationId,\n conflictActorUserId: conflict.conflictActorUserId,\n incomingActorUserId: conflict.incomingActorUserId,\n resolution: conflict.resolution,\n resolvedByUserId,\n })\n }\n\n private async findConflictById(\n conflictId: string,\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<RecordLockConflict | null> {\n const where: FilterQuery<RecordLockConflict> = {\n id: conflictId,\n tenantId: scope.tenantId,\n resourceKind: scope.resourceKind,\n resourceId: scope.resourceId,\n deletedAt: null,\n }\n\n if (scope.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(scope.organizationId)\n }\n\n const scoped = await this.em.findOne(RecordLockConflict, where)\n if (scoped || scope.organizationId === undefined) return scoped\n\n return this.em.findOne(RecordLockConflict, {\n id: conflictId,\n tenantId: scope.tenantId,\n resourceKind: scope.resourceKind,\n resourceId: scope.resourceId,\n deletedAt: null,\n })\n }\n\n private async findActionLogById(\n logId: string | null,\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n if (!logId) return null\n\n let resolved = this.actionLogService\n ? await this.actionLogService.findById(logId)\n : null\n if (!resolved) {\n resolved = await this.em.findOne(ActionLog, { id: logId, deletedAt: null })\n }\n if (!resolved || resolved.deletedAt) return null\n\n if (resolved.tenantId !== scope.tenantId) return null\n\n if (scope.organizationId !== undefined) {\n const expectedOrganizationId = normalizeScopeOrganization(scope.organizationId)\n if (normalizeScopeOrganization(resolved.organizationId) !== expectedOrganizationId) return null\n }\n\n if (resolved.resourceKind !== scope.resourceKind || resolved.resourceId !== scope.resourceId) {\n return null\n }\n\n return resolved\n }\n\n private collectSnapshotDiffPaths(\n before: unknown,\n after: unknown,\n pathPrefix: string | null,\n output: Set<string>,\n seen: Set<unknown>,\n ): void {\n if (valuesEqual(before, after)) return\n\n const beforeRecord = isRecordValue(before) ? before : null\n const afterRecord = isRecordValue(after) ? after : null\n\n if (!beforeRecord || !afterRecord) {\n if (pathPrefix) output.add(pathPrefix)\n return\n }\n\n if (seen.has(beforeRecord) || seen.has(afterRecord)) {\n if (pathPrefix) output.add(pathPrefix)\n return\n }\n\n seen.add(beforeRecord)\n seen.add(afterRecord)\n\n const keys = new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)])\n for (const key of keys) {\n if (SKIPPED_CONFLICT_FIELDS.has(key)) continue\n const nextPath = pathPrefix ? `${pathPrefix}.${key}` : key\n this.collectSnapshotDiffPaths(beforeRecord[key], afterRecord[key], nextPath, output, seen)\n }\n }\n\n private async buildConflictChanges(\n conflict: RecordLockConflict,\n mutationPayload: Record<string, unknown> | null,\n ): Promise<RecordLockConflictChange[]> {\n const scope = {\n tenantId: conflict.tenantId,\n organizationId: conflict.organizationId,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n }\n\n const baseLog = await this.findActionLogById(conflict.baseActionLogId, scope)\n const incomingLog = await this.findActionLogById(conflict.incomingActionLogId, scope)\n\n const baseSnapshot = isRecordValue(baseLog?.snapshotAfter) ? baseLog.snapshotAfter : null\n const incomingBeforeSnapshot = isRecordValue(incomingLog?.snapshotBefore) ? incomingLog.snapshotBefore : null\n const incomingAfterSnapshot = isRecordValue(incomingLog?.snapshotAfter) ? incomingLog.snapshotAfter : null\n const fallbackBaseSnapshot = baseSnapshot ?? incomingBeforeSnapshot\n\n const changeMap = new Map<string, { baseValue: unknown; incomingValue: unknown }>()\n\n const incomingChanges = isRecordValue(incomingLog?.changesJson) ? incomingLog.changesJson : null\n if (incomingChanges) {\n for (const [fieldPathRaw, rawChange] of Object.entries(incomingChanges)) {\n const fieldPath = fieldPathRaw.trim()\n if (shouldSkipConflictField(fieldPath)) continue\n\n const changeRecord = isRecordValue(rawChange) ? rawChange : null\n const fromValue = changeRecord && Object.prototype.hasOwnProperty.call(changeRecord, 'from')\n ? changeRecord.from\n : readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n const toValue = changeRecord && Object.prototype.hasOwnProperty.call(changeRecord, 'to')\n ? changeRecord.to\n : readPathValueLoose(incomingAfterSnapshot, fieldPath)\n\n changeMap.set(fieldPath, {\n baseValue: fromValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(fromValue),\n incomingValue: toValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(toValue),\n })\n }\n }\n\n if (!changeMap.size && fallbackBaseSnapshot && incomingAfterSnapshot) {\n const diffPaths = new Set<string>()\n this.collectSnapshotDiffPaths(\n fallbackBaseSnapshot,\n incomingAfterSnapshot,\n null,\n diffPaths,\n new Set<unknown>(),\n )\n\n for (const fieldPath of diffPaths) {\n if (shouldSkipConflictField(fieldPath)) continue\n const fromValue = readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n const toValue = readPathValueLoose(incomingAfterSnapshot, fieldPath)\n changeMap.set(fieldPath, {\n baseValue: fromValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(fromValue),\n incomingValue: toValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(toValue),\n })\n }\n }\n\n if (!changeMap.size && mutationPayload && incomingAfterSnapshot) {\n for (const fieldPath of Object.keys(mutationPayload)) {\n if (shouldSkipConflictField(fieldPath)) continue\n const mineValue = readPathValueLoose(mutationPayload, fieldPath)\n const incomingValue = readPathValueLoose(incomingAfterSnapshot, fieldPath)\n if (mineValue === MISSING_CONFLICT_VALUE || incomingValue === MISSING_CONFLICT_VALUE) continue\n if (valuesEqual(mineValue, incomingValue)) continue\n const baseValue = readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n changeMap.set(fieldPath, {\n baseValue: baseValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(baseValue),\n incomingValue: normalizeConflictValue(incomingValue),\n })\n }\n }\n\n if (!changeMap.size) return []\n\n const allFields = Array.from(changeMap.keys())\n const preferredFields = mutationPayload\n ? allFields.filter((fieldPath) => {\n const mineValue = readPathValueLoose(mutationPayload, fieldPath)\n if (mineValue === MISSING_CONFLICT_VALUE) return false\n const incomingValue = changeMap.get(fieldPath)?.incomingValue\n return !valuesEqual(mineValue, incomingValue)\n })\n : []\n const selectedFields = (preferredFields.length ? preferredFields : allFields)\n .filter((fieldPath) => !shouldSkipConflictField(fieldPath))\n .sort((left, right) => left.localeCompare(right))\n .slice(0, 25)\n\n return selectedFields.map((fieldPath) => {\n const entry = changeMap.get(fieldPath) ?? { baseValue: null, incomingValue: null }\n const mineValueRaw = mutationPayload ? readPathValueLoose(mutationPayload, fieldPath) : MISSING_CONFLICT_VALUE\n const mineValue = mineValueRaw === MISSING_CONFLICT_VALUE\n ? entry.baseValue\n : normalizeConflictValue(mineValueRaw)\n\n return {\n field: fieldPath,\n displayValue: normalizeConflictValue(entry.baseValue),\n baseValue: normalizeConflictValue(entry.baseValue),\n incomingValue: normalizeConflictValue(entry.incomingValue),\n mineValue: normalizeConflictValue(mineValue),\n }\n })\n }\n\n private async toConflictPayload(\n conflict: RecordLockConflict,\n mutationPayload: Record<string, unknown> | null,\n allowIncomingOverride: boolean,\n canOverrideIncoming: boolean,\n ): Promise<RecordLockConflictPayload> {\n const changes = await this.buildConflictChanges(conflict, mutationPayload)\n return {\n id: conflict.id,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n baseActionLogId: conflict.baseActionLogId,\n incomingActionLogId: conflict.incomingActionLogId,\n allowIncomingOverride,\n canOverrideIncoming,\n resolutionOptions: canOverrideIncoming ? ['accept_mine'] : [],\n changes,\n }\n }\n}\n\nexport function createRecordLockService(deps: RecordLockServiceDeps): RecordLockService {\n return new RecordLockService(deps)\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'crypto'\nimport { UniqueConstraintViolationException, type FilterQuery } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { Knex } from 'knex'\nimport type { ModuleConfigService } from '@open-mercato/core/modules/configs/lib/module-config-service'\nimport { ActionLog } from '@open-mercato/core/modules/audit_logs/data/entities'\nimport type { ActionLogService } from '@open-mercato/core/modules/audit_logs/services/actionLogService'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { emitRecordLocksEvent } from '../events'\nimport {\n RecordLock,\n RecordLockConflict,\n type RecordLockStatus,\n type RecordLockReleaseReason,\n type RecordLockConflictResolution,\n type RecordLockConflictStatus,\n} from '../data/entities'\nimport {\n recordLockMutationHeaderSchema,\n type RecordLockMutationHeaders,\n type RecordLockReleaseInput as RecordLockReleasePayloadInput,\n type RecordLockSettingsInput,\n} from '../data/validators'\nimport {\n DEFAULT_RECORD_LOCK_SETTINGS,\n RECORD_LOCKS_MODULE_ID,\n RECORD_LOCKS_SETTINGS_NAME,\n isRecordLockingEnabledForResource,\n normalizeRecordLockSettings,\n type RecordLockSettings,\n type RecordLockStrategy,\n} from './config'\n\nconst ACTIVE_LOCK_STATUS: RecordLockStatus = 'active'\nconst ACTIVE_SCOPE_UNIQUE_CONSTRAINTS = new Set([\n 'record_locks_active_scope_org_unique',\n 'record_locks_active_scope_tenant_unique',\n 'record_locks_active_scope_user_org_unique',\n 'record_locks_active_scope_user_tenant_unique',\n])\nconst LOCK_CONTENTION_EVENT_TTL_MS = 15_000\nconst PARTICIPANT_REJOIN_AFTER_SAVE_SUPPRESS_MS = 20_000\nconst LOCK_CONTENTION_EVENT_MAX_ENTRIES = 2_000\nconst lockContentionEventThrottle = new Map<string, number>()\nconst LOCK_CLEANUP_INTERVAL_MS = 5 * 60 * 1000\nconst LOCK_RETENTION_MS = 3 * 24 * 60 * 60 * 1000\nconst RESOLVED_CONFLICT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000\nconst PENDING_CONFLICT_RETENTION_MS = 24 * 60 * 60 * 1000\nconst LOCK_CLEANUP_STATE_TTL_MS = 24 * 60 * 60 * 1000\nconst LOCK_CLEANUP_STATE_MAX_ENTRIES = 2_000\nconst lockCleanupStateByTenant = new Map<string, { lastRunAt: number; inFlight: boolean; lastSeenAt: number }>()\n\nexport type RecordLockScope = {\n tenantId: string\n organizationId?: string | null\n userId: string\n}\n\nexport type RecordLockResource = {\n resourceKind: string\n resourceId: string\n}\n\nexport type RecordLockAcquireInput = RecordLockScope & RecordLockResource & {\n lockedByIp?: string | null\n}\n\nexport type RecordLockHeartbeatInput = RecordLockScope & RecordLockResource & {\n token: string\n}\n\nexport type RecordLockReleaseInput = RecordLockScope & RecordLockResource & {\n token?: string\n reason?: Exclude<RecordLockReleaseReason, 'expired' | 'force'>\n} & Pick<RecordLockReleasePayloadInput, 'conflictId' | 'resolution'>\n\nexport type RecordLockForceReleaseInput = RecordLockScope & RecordLockResource & {\n reason?: string | null\n}\n\nexport type RecordLockMutationValidationInput = RecordLockScope & RecordLockResource & {\n method: 'PUT' | 'DELETE'\n headers: Partial<RecordLockMutationHeaders>\n mutationPayload?: Record<string, unknown> | null\n}\n\nexport type RecordLockConflictChange = {\n field: string\n displayValue: unknown\n baseValue: unknown\n incomingValue: unknown\n mineValue: unknown\n}\n\nexport type RecordLockConflictPayload = {\n id: string\n resourceKind: string\n resourceId: string\n baseActionLogId: string | null\n incomingActionLogId: string | null\n allowIncomingOverride: boolean\n canOverrideIncoming: boolean\n resolutionOptions: Array<'accept_mine'>\n changes: RecordLockConflictChange[]\n}\n\nexport type RecordLockView = {\n id: string\n resourceKind: string\n resourceId: string\n token: string | null\n strategy: RecordLockStrategy\n status: RecordLockStatus\n lockedByUserId: string\n lockedByIp: string | null\n baseActionLogId: string | null\n lockedAt: string\n lastHeartbeatAt: string\n expiresAt: string\n participants: RecordLockParticipantView[]\n activeParticipantCount: number\n}\n\nexport type RecordLockParticipantView = {\n userId: string\n lockedByIp: string | null\n lockedAt: string\n lastHeartbeatAt: string\n expiresAt: string\n}\n\nexport type RecordLockAcquireResult = {\n ok: true\n enabled: boolean\n resourceEnabled: boolean\n strategy: RecordLockStrategy\n allowForceUnlock: boolean\n heartbeatSeconds: number\n acquired: boolean\n latestActionLogId: string | null\n lock: RecordLockView | null\n}\n\nexport type RecordLockAcquireFailure = RecordLockValidationFailure & {\n allowForceUnlock: boolean\n}\n\nexport type RecordLockHeartbeatResult = {\n ok: true\n expiresAt: string | null\n}\n\nexport type RecordLockReleaseResult = {\n ok: true\n released: boolean\n conflictResolved: boolean\n}\n\nexport type RecordLockForceReleaseResult = {\n ok: true\n released: boolean\n lock: RecordLockView | null\n}\n\nexport type RecordLockValidationSuccess = {\n ok: true\n enabled: boolean\n resourceEnabled: boolean\n strategy: RecordLockStrategy\n shouldReleaseOnSuccess: boolean\n lock: RecordLockView | null\n latestActionLogId: string | null\n}\n\nexport type RecordLockValidationFailure = {\n ok: false\n status: 409 | 423\n error: string\n code: 'record_lock_conflict' | 'record_locked'\n lock: RecordLockView | null\n conflict?: RecordLockConflictPayload\n}\n\nexport type RecordLockValidationResult = RecordLockValidationSuccess | RecordLockValidationFailure\n\nexport type RecordLockServiceDeps = {\n em: EntityManager\n moduleConfigService?: ModuleConfigService | null\n actionLogService?: ActionLogService | null\n rbacService?: RbacService | null\n}\n\nexport type ParsedRecordLockHeaders = Partial<RecordLockMutationHeaders>\n\nfunction normalizeDate(value: Date): string {\n return value.toISOString()\n}\n\nfunction trimToNull(value: string | null | undefined): string | null {\n if (typeof value !== 'string') return null\n const trimmed = value.trim()\n return trimmed.length ? trimmed : null\n}\n\nfunction normalizeScopeOrganization(value: string | null | undefined): string | null {\n const trimmed = trimToNull(value)\n return trimmed ?? null\n}\n\nfunction shouldEmitLockContentionEvent(input: {\n tenantId: string\n organizationId?: string | null\n resourceKind: string\n resourceId: string\n lockedByUserId: string\n attemptedByUserId: string\n}): boolean {\n if (process.env.NODE_ENV === 'test') return true\n const now = Date.now()\n const key = [\n input.tenantId,\n normalizeScopeOrganization(input.organizationId) ?? 'global',\n input.resourceKind,\n input.resourceId,\n input.lockedByUserId,\n input.attemptedByUserId,\n ].join('|')\n\n const lastEmittedAt = lockContentionEventThrottle.get(key)\n if (typeof lastEmittedAt === 'number' && now - lastEmittedAt < LOCK_CONTENTION_EVENT_TTL_MS) {\n return false\n }\n\n lockContentionEventThrottle.set(key, now)\n\n for (const [cachedKey, cachedAt] of lockContentionEventThrottle.entries()) {\n if (now - cachedAt > LOCK_CONTENTION_EVENT_TTL_MS) {\n lockContentionEventThrottle.delete(cachedKey)\n }\n }\n if (lockContentionEventThrottle.size > LOCK_CONTENTION_EVENT_MAX_ENTRIES) {\n const oldest = Array.from(lockContentionEventThrottle.entries())\n .sort((left, right) => left[1] - right[1])\n .slice(0, lockContentionEventThrottle.size - LOCK_CONTENTION_EVENT_MAX_ENTRIES)\n for (const [staleKey] of oldest) lockContentionEventThrottle.delete(staleKey)\n }\n\n return true\n}\n\nfunction isActiveLockScopeUniqueViolation(error: unknown): boolean {\n if (error instanceof UniqueConstraintViolationException) {\n const errorWithConstraint = error as unknown as { constraint?: unknown }\n const constraint = typeof errorWithConstraint.constraint === 'string'\n ? errorWithConstraint.constraint\n : null\n if (constraint && ACTIVE_SCOPE_UNIQUE_CONSTRAINTS.has(constraint)) return true\n }\n if (!error || typeof error !== 'object') return false\n const code = (error as { code?: unknown }).code\n if (code !== '23505') return false\n const message = typeof (error as { message?: unknown }).message === 'string'\n ? (error as { message: string }).message.toLowerCase()\n : ''\n for (const constraint of ACTIVE_SCOPE_UNIQUE_CONSTRAINTS) {\n if (message.includes(constraint)) return true\n }\n return false\n}\n\nfunction getKnex(em: EntityManager): Knex {\n return (em.getConnection() as unknown as { getKnex: () => Knex }).getKnex()\n}\n\nconst SKIPPED_CONFLICT_FIELDS = new Set([\n 'updatedAt',\n 'updated_at',\n 'createdAt',\n 'created_at',\n 'deletedAt',\n 'deleted_at',\n])\n\nfunction shouldSkipConflictField(path: string): boolean {\n if (!path.trim().length) return true\n if (SKIPPED_CONFLICT_FIELDS.has(path)) return true\n const segments = path.split('.').filter((segment) => segment.length > 0)\n if (!segments.length) return true\n return SKIPPED_CONFLICT_FIELDS.has(segments[segments.length - 1] ?? '')\n}\n\nconst MISSING_CONFLICT_VALUE = Symbol('record_lock_conflict_missing_value')\n\nfunction isRecordValue(value: unknown): value is Record<string, unknown> {\n return Boolean(value && typeof value === 'object' && !Array.isArray(value))\n}\n\nfunction toIsoDate(value: unknown): string | null {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) return null\n return value.toISOString()\n }\n if (typeof value === 'string') {\n const parsed = new Date(value)\n return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString()\n }\n return null\n}\n\nfunction valuesEqual(a: unknown, b: unknown, seen?: Set<unknown>): boolean {\n if (Object.is(a, b)) return true\n\n if (a instanceof Date || b instanceof Date) {\n const left = toIsoDate(a)\n const right = toIsoDate(b)\n return left !== null && right !== null && left === right\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false\n for (let index = 0; index < a.length; index += 1) {\n if (!valuesEqual(a[index], b[index], seen)) return false\n }\n return true\n }\n\n if (isRecordValue(a) && isRecordValue(b)) {\n if (!seen) seen = new Set()\n if (seen.has(a) || seen.has(b)) return false\n seen.add(a)\n seen.add(b)\n const aKeys = Object.keys(a)\n const bKeys = Object.keys(b)\n if (aKeys.length !== bKeys.length) return false\n for (const key of aKeys) {\n if (!Object.prototype.hasOwnProperty.call(b, key)) return false\n if (!valuesEqual(a[key], b[key], seen)) return false\n }\n return true\n }\n\n return false\n}\n\nfunction readPathValue(source: unknown, path: string): unknown | typeof MISSING_CONFLICT_VALUE {\n if (!path.trim().length || !isRecordValue(source)) return MISSING_CONFLICT_VALUE\n if (Object.prototype.hasOwnProperty.call(source, path)) return source[path]\n\n const segments = path.split('.').filter((segment) => segment.length > 0)\n if (!segments.length) return MISSING_CONFLICT_VALUE\n\n let current: unknown = source\n for (const segment of segments) {\n if (!isRecordValue(current)) return MISSING_CONFLICT_VALUE\n if (!Object.prototype.hasOwnProperty.call(current, segment)) return MISSING_CONFLICT_VALUE\n current = current[segment]\n }\n return current\n}\n\nfunction buildPathVariants(path: string): string[] {\n const trimmed = path.trim()\n if (!trimmed.length) return []\n\n const segments = trimmed.split('.').filter((segment) => segment.length > 0)\n if (segments.length <= 1) return [trimmed]\n\n const variants = new Set<string>([trimmed])\n for (let index = 1; index < segments.length; index += 1) {\n variants.add(segments.slice(index).join('.'))\n }\n return Array.from(variants)\n}\n\nfunction readPathValueLoose(source: unknown, path: string): unknown | typeof MISSING_CONFLICT_VALUE {\n const variants = buildPathVariants(path)\n for (const variant of variants) {\n const value = readPathValue(source, variant)\n if (value !== MISSING_CONFLICT_VALUE) return value\n }\n return MISSING_CONFLICT_VALUE\n}\n\nfunction normalizeConflictValue(value: unknown): unknown {\n return value === undefined ? null : value\n}\n\nfunction formatNotificationValue(value: unknown): string {\n if (value === null || value === undefined) return '-'\n if (typeof value === 'string') return value\n if (typeof value === 'number' || typeof value === 'boolean') return String(value)\n if (value instanceof Date) return value.toISOString()\n try {\n return JSON.stringify(value)\n } catch {\n return String(value)\n }\n}\n\nfunction formatChangedFieldLabel(rawField: string): string {\n const trimmedField = rawField.trim()\n const withoutNamespace = trimmedField.includes('::') ? (trimmedField.split('::').pop() ?? trimmedField) : trimmedField\n const withoutPrefix = withoutNamespace.includes('.') ? (withoutNamespace.split('.').pop() ?? withoutNamespace) : withoutNamespace\n const words = withoutPrefix\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[._-]+/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .split(' ')\n .filter(Boolean)\n\n if (!words.length) return trimmedField\n return words\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ')\n}\n\nexport function readRecordLockHeaders(headers: Headers): ParsedRecordLockHeaders {\n const raw = {\n resourceKind: trimToNull(headers.get('x-om-record-lock-kind')) ?? undefined,\n resourceId: trimToNull(headers.get('x-om-record-lock-resource-id')) ?? undefined,\n token: trimToNull(headers.get('x-om-record-lock-token')) ?? undefined,\n baseLogId: trimToNull(headers.get('x-om-record-lock-base-log-id')) ?? undefined,\n resolution: trimToNull(headers.get('x-om-record-lock-resolution')) ?? undefined,\n conflictId: trimToNull(headers.get('x-om-record-lock-conflict-id')) ?? undefined,\n }\n\n const parsed = recordLockMutationHeaderSchema.partial().safeParse(raw)\n if (!parsed.success) return {}\n return parsed.data\n}\n\nexport class RecordLockService {\n private readonly em: EntityManager\n\n private readonly moduleConfigService: ModuleConfigService | null\n\n private readonly actionLogService: ActionLogService | null\n\n private readonly rbacService: RbacService | null\n\n constructor(deps: RecordLockServiceDeps) {\n this.em = deps.em\n this.moduleConfigService = deps.moduleConfigService ?? null\n this.actionLogService = deps.actionLogService ?? null\n this.rbacService = deps.rbacService ?? null\n }\n\n async getSettings(): Promise<RecordLockSettings> {\n if (!this.moduleConfigService) return DEFAULT_RECORD_LOCK_SETTINGS\n\n const value = await this.moduleConfigService.getValue<RecordLockSettings>(\n RECORD_LOCKS_MODULE_ID,\n RECORD_LOCKS_SETTINGS_NAME,\n { defaultValue: DEFAULT_RECORD_LOCK_SETTINGS },\n )\n\n return normalizeRecordLockSettings(value ?? DEFAULT_RECORD_LOCK_SETTINGS)\n }\n\n async saveSettings(input: RecordLockSettingsInput): Promise<RecordLockSettings> {\n const settings = normalizeRecordLockSettings(input)\n if (!this.moduleConfigService) return settings\n\n await this.moduleConfigService.setValue(RECORD_LOCKS_MODULE_ID, RECORD_LOCKS_SETTINGS_NAME, settings)\n return settings\n }\n\n async acquire(input: RecordLockAcquireInput): Promise<RecordLockAcquireResult | RecordLockAcquireFailure> {\n this.scheduleCleanup(input.tenantId)\n const settings = await this.getSettings()\n const latest = await this.findLatestActionLogWithScopeFallback(input)\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n\n if (!resourceEnabled) {\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: false,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: null,\n }\n }\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n const ownedActiveLock = activeLocks.find((lock) => lock.lockedByUserId === input.userId) ?? null\n const competingActiveLock = activeLocks.find((lock) => lock.lockedByUserId !== input.userId) ?? null\n\n if (settings.strategy === 'pessimistic' && !ownedActiveLock && competingActiveLock) {\n const lockView = this.toLockView(competingActiveLock, false, activeLocks)\n if (shouldEmitLockContentionEvent({\n tenantId: competingActiveLock.tenantId,\n organizationId: competingActiveLock.organizationId,\n resourceKind: competingActiveLock.resourceKind,\n resourceId: competingActiveLock.resourceId,\n lockedByUserId: competingActiveLock.lockedByUserId,\n attemptedByUserId: input.userId,\n })) {\n await emitRecordLocksEvent('record_locks.lock.contended', {\n lockId: competingActiveLock.id,\n resourceKind: competingActiveLock.resourceKind,\n resourceId: competingActiveLock.resourceId,\n tenantId: competingActiveLock.tenantId,\n organizationId: competingActiveLock.organizationId,\n lockedByUserId: competingActiveLock.lockedByUserId,\n attemptedByUserId: input.userId,\n })\n }\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n allowForceUnlock: settings.allowForceUnlock,\n lock: lockView,\n }\n }\n\n if (ownedActiveLock) {\n ownedActiveLock.strategy = settings.strategy\n ownedActiveLock.lockedByIp = input.lockedByIp ?? ownedActiveLock.lockedByIp ?? null\n ownedActiveLock.lastHeartbeatAt = now\n ownedActiveLock.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n\n activeLocks = await this.findActiveLocks(input, now)\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: this.toLockView(ownedActiveLock, true, activeLocks),\n }\n }\n\n const lock = this.em.create(RecordLock, {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: randomUUID(),\n strategy: settings.strategy,\n status: ACTIVE_LOCK_STATUS,\n lockedByUserId: input.userId,\n lockedByIp: input.lockedByIp ?? null,\n baseActionLogId: latest?.id ?? null,\n lockedAt: now,\n lastHeartbeatAt: now,\n expiresAt: new Date(now.getTime() + settings.timeoutSeconds * 1000),\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n })\n\n this.em.persist(lock)\n let createdNewLock = true\n try {\n await this.em.flush()\n } catch (error) {\n if (!isActiveLockScopeUniqueViolation(error)) throw error\n const clear = (this.em as { clear?: () => void }).clear\n if (typeof clear === 'function') clear.call(this.em)\n const locksAfterCollision = await this.findActiveLocks(input, now)\n const competingAfterCollision = locksAfterCollision.find((item) => item.lockedByUserId !== input.userId) ?? null\n if (settings.strategy === 'pessimistic' && competingAfterCollision) {\n if (shouldEmitLockContentionEvent({\n tenantId: competingAfterCollision.tenantId,\n organizationId: competingAfterCollision.organizationId,\n resourceKind: competingAfterCollision.resourceKind,\n resourceId: competingAfterCollision.resourceId,\n lockedByUserId: competingAfterCollision.lockedByUserId,\n attemptedByUserId: input.userId,\n })) {\n await emitRecordLocksEvent('record_locks.lock.contended', {\n lockId: competingAfterCollision.id,\n resourceKind: competingAfterCollision.resourceKind,\n resourceId: competingAfterCollision.resourceId,\n tenantId: competingAfterCollision.tenantId,\n organizationId: competingAfterCollision.organizationId,\n lockedByUserId: competingAfterCollision.lockedByUserId,\n attemptedByUserId: input.userId,\n })\n }\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n allowForceUnlock: settings.allowForceUnlock,\n lock: this.toLockView(competingAfterCollision, false, locksAfterCollision),\n }\n }\n const existingOwned = await this.findOwnedActiveLock(input)\n if (!existingOwned) throw error\n existingOwned.strategy = settings.strategy\n existingOwned.lockedByIp = input.lockedByIp ?? existingOwned.lockedByIp ?? null\n existingOwned.lastHeartbeatAt = now\n existingOwned.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n createdNewLock = false\n }\n\n const activeAfterAcquire = await this.findActiveLocks(input, now)\n const ownedAfterAcquire = activeAfterAcquire.find((item) => item.lockedByUserId === input.userId)\n ?? await this.findOwnedActiveLock(input)\n ?? lock\n ?? null\n if (!ownedAfterAcquire) {\n const fallbackLock = activeAfterAcquire[0] ?? null\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: false,\n latestActionLogId: latest?.id ?? null,\n lock: fallbackLock ? this.toLockView(fallbackLock, false, activeAfterAcquire) : null,\n }\n }\n\n if (createdNewLock) {\n await emitRecordLocksEvent('record_locks.lock.acquired', {\n lockId: ownedAfterAcquire.id,\n resourceKind: ownedAfterAcquire.resourceKind,\n resourceId: ownedAfterAcquire.resourceId,\n tenantId: ownedAfterAcquire.tenantId,\n organizationId: ownedAfterAcquire.organizationId,\n lockedByUserId: ownedAfterAcquire.lockedByUserId,\n strategy: ownedAfterAcquire.strategy,\n baseActionLogId: ownedAfterAcquire.baseActionLogId,\n activeParticipantCount: activeAfterAcquire.length,\n })\n\n const recipientUserIds = activeAfterAcquire\n .filter((item) => item.lockedByUserId !== input.userId)\n .map((item) => item.lockedByUserId)\n const shouldSuppressJoinNotification = await this.hasRecentSavedRelease({\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n now,\n })\n\n if (!shouldSuppressJoinNotification) {\n await emitRecordLocksEvent('record_locks.participant.joined', {\n lockId: ownedAfterAcquire.id,\n resourceKind: ownedAfterAcquire.resourceKind,\n resourceId: ownedAfterAcquire.resourceId,\n tenantId: ownedAfterAcquire.tenantId,\n organizationId: ownedAfterAcquire.organizationId,\n joinedUserId: input.userId,\n joinedIp: ownedAfterAcquire.lockedByIp ?? null,\n recipientUserIds,\n activeParticipantCount: activeAfterAcquire.length,\n })\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n allowForceUnlock: settings.allowForceUnlock,\n heartbeatSeconds: settings.heartbeatSeconds,\n acquired: createdNewLock,\n latestActionLogId: latest?.id ?? null,\n lock: this.toLockView(ownedAfterAcquire, true, activeAfterAcquire),\n }\n }\n\n async heartbeat(input: RecordLockHeartbeatInput): Promise<RecordLockHeartbeatResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n if (!resourceEnabled) return { ok: true, expiresAt: null }\n\n const lock = await this.findOwnedLockByToken(input)\n if (!lock) return { ok: true, expiresAt: null }\n\n const now = new Date()\n if (lock.expiresAt <= now) {\n this.markLockReleased(lock, {\n status: 'expired',\n reason: 'expired',\n releasedByUserId: lock.lockedByUserId,\n now,\n })\n await this.em.flush()\n return { ok: true, expiresAt: null }\n }\n\n lock.lastHeartbeatAt = now\n lock.expiresAt = new Date(now.getTime() + settings.timeoutSeconds * 1000)\n await this.em.flush()\n return { ok: true, expiresAt: normalizeDate(lock.expiresAt) }\n }\n\n async release(input: RecordLockReleaseInput): Promise<RecordLockReleaseResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n if (!resourceEnabled) return { ok: true, released: false, conflictResolved: false }\n\n let conflictResolved = false\n if (input.reason === 'conflict_resolved' && input.conflictId && input.resolution === 'accept_incoming') {\n const conflict = await this.findConflictById(input.conflictId, input)\n if (conflict && conflict.status === 'pending' && conflict.conflictActorUserId === input.userId) {\n await this.resolveConflict(conflict, input.resolution, input.userId)\n conflictResolved = true\n }\n }\n\n const lock = input.token\n ? await this.findOwnedLockByToken(input)\n : await this.findOwnedActiveLock(input)\n if (!lock) return { ok: true, released: false, conflictResolved }\n\n const now = new Date()\n this.markLockReleased(lock, {\n status: 'released',\n reason: input.reason ?? 'cancelled',\n releasedByUserId: input.userId,\n now,\n })\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.lock.released', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n lockedByUserId: lock.lockedByUserId,\n releasedByUserId: input.userId,\n reason: lock.releaseReason,\n })\n\n if (lock.releaseReason === 'unmount') {\n const remainingActiveLocks = await this.findActiveLocks(input, now)\n const recipientUserIds = remainingActiveLocks\n .map((activeLock) => activeLock.lockedByUserId)\n .filter((userId) => userId !== lock.lockedByUserId)\n\n if (recipientUserIds.length) {\n await emitRecordLocksEvent('record_locks.participant.left', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n leftUserId: lock.lockedByUserId,\n reason: 'unmount',\n recipientUserIds,\n activeParticipantCount: remainingActiveLocks.length,\n })\n }\n }\n\n return { ok: true, released: true, conflictResolved }\n }\n\n async forceRelease(input: RecordLockForceReleaseInput): Promise<RecordLockForceReleaseResult> {\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n const canForceRelease = await this.canUserForceRelease(input, settings)\n if (!resourceEnabled || !settings.allowForceUnlock || !canForceRelease) {\n return { ok: true, released: false, lock: null }\n }\n\n const now = new Date()\n const activeLocks = this.sortLocksByJoinOrder(await this.findActiveLocks(input, now))\n const lock = activeLocks[0] ?? null\n if (!lock) return { ok: true, released: false, lock: null }\n\n this.markLockReleased(lock, {\n status: 'force_released',\n reason: 'force',\n releasedByUserId: input.userId,\n now,\n })\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.lock.force_released', {\n lockId: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n tenantId: lock.tenantId,\n organizationId: lock.organizationId,\n lockedByUserId: lock.lockedByUserId,\n releasedByUserId: input.userId,\n reason: input.reason ?? null,\n })\n\n const remainingLocks = this.sortLocksByJoinOrder(activeLocks.filter((item) => item.id !== lock.id))\n const nextInQueue = remainingLocks[0] ?? null\n return { ok: true, released: true, lock: nextInQueue ? this.toLockView(nextInQueue, false, remainingLocks) : null }\n }\n\n async validateMutation(input: RecordLockMutationValidationInput): Promise<RecordLockValidationResult> {\n this.scheduleCleanup(input.tenantId)\n const settings = await this.getSettings()\n const resourceEnabled = isRecordLockingEnabledForResource(settings, input.resourceKind)\n const canOverrideIncoming = await this.canUserOverrideIncoming(input, settings)\n\n if (!resourceEnabled) {\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: false,\n strategy: settings.strategy,\n shouldReleaseOnSuccess: false,\n lock: null,\n latestActionLogId: null,\n }\n }\n\n const parsedHeaders = this.normalizeMutationHeaders(input.headers)\n const keepMineResolution = parsedHeaders.resolution === 'accept_mine' || parsedHeaders.resolution === 'merged'\n ? parsedHeaders.resolution\n : null\n const hasKeepMineIntent = keepMineResolution !== null\n const now = new Date()\n const activeLocks = await this.findActiveLocks(input, now)\n const ownedActiveLock = activeLocks.find((lock) => lock.lockedByUserId === input.userId) ?? null\n const competingLock = activeLocks.find((lock) => lock.lockedByUserId !== input.userId) ?? null\n const latest = await this.findLatestActionLogWithScopeFallback(input)\n const shouldReleaseOnSuccess = Boolean(\n ownedActiveLock\n && (!parsedHeaders.token || ownedActiveLock.token === parsedHeaders.token),\n )\n\n if (settings.strategy === 'pessimistic') {\n if (competingLock && !ownedActiveLock) {\n return {\n ok: false,\n status: 423,\n error: 'Record is currently locked by another user',\n code: 'record_locked',\n lock: this.toLockView(competingLock, false, activeLocks),\n }\n }\n\n if (ownedActiveLock) {\n if (parsedHeaders.token && ownedActiveLock.token !== parsedHeaders.token) {\n return {\n ok: false,\n status: 423,\n error: 'Valid lock token is required for this mutation',\n code: 'record_locked',\n lock: this.toLockView(ownedActiveLock, false, activeLocks),\n }\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n const existingConflict = parsedHeaders.conflictId\n ? await this.findConflictById(parsedHeaders.conflictId, input)\n : null\n\n if (existingConflict) {\n const canResolveExistingConflict = existingConflict.status === 'pending'\n && existingConflict.conflictActorUserId === input.userId\n\n if (parsedHeaders.resolution === 'accept_mine' || parsedHeaders.resolution === 'merged') {\n const isAlreadyResolvedByRequester = existingConflict.conflictActorUserId === input.userId\n && existingConflict.status !== 'pending'\n && existingConflict.resolution === parsedHeaders.resolution\n\n if (!canResolveExistingConflict && !isAlreadyResolvedByRequester) {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n if (!canOverrideIncoming) {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n if (canResolveExistingConflict) {\n await this.resolveConflict(existingConflict, parsedHeaders.resolution, input.userId)\n }\n } else {\n return {\n ok: false,\n status: 409,\n error: 'Record conflict requires resolution before saving',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(existingConflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n }\n\n if (!existingConflict) {\n const baseActionLogId = parsedHeaders.baseLogId\n ?? (ownedActiveLock ? ownedActiveLock.baseActionLogId : null)\n\n const hasConflictingBaseLog = Boolean(\n latest?.id\n && baseActionLogId\n && latest.id !== baseActionLogId\n )\n const hasConflictingWriteAfterLockStart = Boolean(\n latest?.id\n && !baseActionLogId\n && ownedActiveLock\n && latest.createdAt instanceof Date\n && ownedActiveLock.lockedAt instanceof Date\n && latest.createdAt.getTime() > ownedActiveLock.lockedAt.getTime()\n && latest.actorUserId !== input.userId\n )\n const isConflictingWrite = hasConflictingBaseLog || hasConflictingWriteAfterLockStart\n\n if (isConflictingWrite) {\n if (keepMineResolution && canOverrideIncoming) {\n const autoResolvedConflict = await this.createConflict({\n scope: input,\n baseActionLogId,\n incomingActionLogId: latest?.id ?? null,\n conflictActorUserId: input.userId,\n incomingActorUserId: latest?.actorUserId ?? null,\n })\n await this.resolveConflict(autoResolvedConflict, keepMineResolution, input.userId)\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n const conflict = await this.createConflict({\n scope: input,\n baseActionLogId,\n incomingActionLogId: latest?.id ?? null,\n conflictActorUserId: input.userId,\n incomingActorUserId: latest?.actorUserId ?? null,\n })\n\n return {\n ok: false,\n status: 409,\n error: 'Record conflict detected',\n code: 'record_lock_conflict',\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n conflict: await this.toConflictPayload(conflict, input.mutationPayload ?? null, settings.allowIncomingOverride, canOverrideIncoming),\n }\n }\n }\n\n return {\n ok: true,\n enabled: settings.enabled,\n resourceEnabled: true,\n strategy: settings.strategy,\n shouldReleaseOnSuccess,\n lock: ownedActiveLock ? this.toLockView(ownedActiveLock, false, activeLocks) : null,\n latestActionLogId: latest?.id ?? null,\n }\n }\n\n async releaseAfterMutation(input: RecordLockReleaseInput): Promise<void> {\n const releaseResult = await this.release({\n ...input,\n reason: input.reason ?? 'saved',\n })\n if (!releaseResult.released) return\n }\n\n async emitIncomingChangesNotificationAfterMutation(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n method: 'PUT' | 'DELETE'\n }): Promise<void> {\n if (input.method !== 'PUT') return\n const settings = await this.getSettings()\n if (!settings.notifyOnConflict || !isRecordLockingEnabledForResource(settings, input.resourceKind)) return\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n if (!activeLocks.length) {\n const fallbackWhere: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n const fallbackLocks = await this.em.find(RecordLock, fallbackWhere, { orderBy: { updatedAt: 'desc' } })\n activeLocks = Array.isArray(fallbackLocks) ? fallbackLocks : []\n }\n\n const recipientUserIds = new Set<string>()\n for (const lock of activeLocks) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n if (!recipientUserIds.size) {\n const fallbackWindowMs = Math.max((settings.timeoutSeconds ?? 300) * 1000, 60_000)\n const fallbackSince = new Date(now.getTime() - fallbackWindowMs)\n const recentLocks = await this.em.find(RecordLock, {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n updatedAt: { $gte: fallbackSince },\n }, { orderBy: { updatedAt: 'desc' }, limit: 50 })\n\n for (const lock of (Array.isArray(recentLocks) ? recentLocks : [])) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n }\n if (!recipientUserIds.size) return\n\n let latest = await this.findLatestActionLog(input)\n if (!latest) {\n latest = await this.findLatestActionLog({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n }\n let actorLog = latest?.actorUserId === input.userId\n ? latest\n : await this.findLatestActionLogByActor(input, input.userId)\n if (!actorLog) {\n actorLog = await this.findLatestActionLogByActor({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n }, input.userId)\n }\n const incomingLog = actorLog ?? latest\n\n const changedFields = incomingLog\n ? this.summarizeChangedFieldsFromActionLog(incomingLog)\n : ''\n const changedRows = this.buildIncomingChangeRowsFromActionLog(incomingLog)\n const changedRowsJson = changedRows.length ? JSON.stringify(changedRows) : ''\n\n await emitRecordLocksEvent('record_locks.incoming_changes.available', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n incomingActorUserId: input.userId,\n incomingActionLogId: incomingLog?.id ?? null,\n recipientUserIds: Array.from(recipientUserIds),\n changedFields: changedFields || '-',\n changedRowsJson,\n })\n }\n\n async emitRecordDeletedNotificationAfterMutation(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n method: 'PUT' | 'DELETE'\n }): Promise<void> {\n if (input.method !== 'DELETE') return\n const settings = await this.getSettings()\n if (!isRecordLockingEnabledForResource(settings, input.resourceKind)) return\n\n const now = new Date()\n let activeLocks = await this.findActiveLocks(input, now)\n if (!activeLocks.length) {\n const fallbackWhere: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n const fallbackLocks = await this.em.find(RecordLock, fallbackWhere, { orderBy: { updatedAt: 'desc' } })\n activeLocks = Array.isArray(fallbackLocks) ? fallbackLocks : []\n }\n\n const recipientUserIds = new Set<string>()\n for (const lock of activeLocks) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n\n if (!recipientUserIds.size) {\n const fallbackWindowMs = Math.max((settings.timeoutSeconds ?? 300) * 1000, 60_000)\n const fallbackSince = new Date(now.getTime() - fallbackWindowMs)\n const recentLocks = await this.em.find(RecordLock, {\n tenantId: input.tenantId,\n deletedAt: null,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n updatedAt: { $gte: fallbackSince },\n }, { orderBy: { updatedAt: 'desc' }, limit: 50 })\n\n for (const lock of (Array.isArray(recentLocks) ? recentLocks : [])) {\n if (lock.lockedByUserId !== input.userId) {\n recipientUserIds.add(lock.lockedByUserId)\n }\n }\n }\n if (!recipientUserIds.size) return\n\n await emitRecordLocksEvent('record_locks.record.deleted', {\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n deletedByUserId: input.userId,\n recipientUserIds: Array.from(recipientUserIds),\n })\n }\n\n async resolveConflictById(input: {\n conflictId: string\n tenantId: string\n organizationId?: string | null\n userId: string\n resolution: 'accept_incoming' | 'accept_mine' | 'merged'\n }): Promise<boolean> {\n const settings = await this.getSettings()\n const canOverrideIncoming = await this.canUserOverrideIncoming(input, settings)\n const conflict = await this.em.findOne(RecordLockConflict, {\n id: input.conflictId,\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n deletedAt: null,\n })\n if (!conflict || conflict.status !== 'pending' || conflict.conflictActorUserId !== input.userId) {\n return false\n }\n if ((input.resolution === 'accept_mine' || input.resolution === 'merged') && !canOverrideIncoming) {\n return false\n }\n await this.resolveConflict(conflict, input.resolution, input.userId)\n return true\n }\n\n private async canUserOverrideIncoming(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'>,\n settings: RecordLockSettings,\n ): Promise<boolean> {\n if (!settings.allowIncomingOverride) return false\n if (!this.rbacService) return false\n\n try {\n return await this.rbacService.userHasAllFeatures(\n input.userId,\n ['record_locks.override_incoming'],\n {\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n },\n )\n } catch {\n return false\n }\n }\n\n private async canUserForceRelease(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'>,\n settings: RecordLockSettings,\n ): Promise<boolean> {\n if (!settings.allowForceUnlock) return false\n if (!this.rbacService) return false\n\n try {\n return await this.rbacService.userHasAllFeatures(\n input.userId,\n ['record_locks.force_release'],\n {\n tenantId: input.tenantId,\n organizationId: normalizeScopeOrganization(input.organizationId),\n },\n )\n } catch {\n return false\n }\n }\n\n private pruneLockCleanupState(now: number): void {\n for (const [tenantId, state] of lockCleanupStateByTenant.entries()) {\n if (!state.inFlight && now - state.lastSeenAt > LOCK_CLEANUP_STATE_TTL_MS) {\n lockCleanupStateByTenant.delete(tenantId)\n }\n }\n\n if (lockCleanupStateByTenant.size <= LOCK_CLEANUP_STATE_MAX_ENTRIES) return\n\n const removable = Array.from(lockCleanupStateByTenant.entries())\n .filter(([, state]) => !state.inFlight)\n .sort((left, right) => left[1].lastSeenAt - right[1].lastSeenAt)\n const overflow = lockCleanupStateByTenant.size - LOCK_CLEANUP_STATE_MAX_ENTRIES\n for (const [tenantId] of removable.slice(0, Math.max(0, overflow))) {\n lockCleanupStateByTenant.delete(tenantId)\n }\n }\n\n private scheduleCleanup(tenantId: string): void {\n const now = Date.now()\n this.pruneLockCleanupState(now)\n const state = lockCleanupStateByTenant.get(tenantId) ?? { lastRunAt: 0, inFlight: false, lastSeenAt: now }\n state.lastSeenAt = now\n lockCleanupStateByTenant.set(tenantId, state)\n if (state.inFlight) return\n if (now - state.lastRunAt < LOCK_CLEANUP_INTERVAL_MS) return\n\n state.inFlight = true\n state.lastRunAt = now\n lockCleanupStateByTenant.set(tenantId, state)\n\n void this.cleanupHistoricalRecords(tenantId).finally(() => {\n const current = lockCleanupStateByTenant.get(tenantId)\n if (!current) return\n current.inFlight = false\n lockCleanupStateByTenant.set(tenantId, current)\n })\n }\n\n private async cleanupHistoricalRecords(tenantId: string): Promise<void> {\n try {\n const knex = getKnex(this.em)\n const now = Date.now()\n const lockCutoff = new Date(now - LOCK_RETENTION_MS)\n const resolvedConflictCutoff = new Date(now - RESOLVED_CONFLICT_RETENTION_MS)\n const pendingConflictCutoff = new Date(now - PENDING_CONFLICT_RETENTION_MS)\n const deletedAt = new Date(now)\n\n await knex('record_locks')\n .where({ tenant_id: tenantId })\n .whereNull('deleted_at')\n .whereNot('status', ACTIVE_LOCK_STATUS)\n .andWhere('updated_at', '<', lockCutoff)\n .update({\n deleted_at: deletedAt,\n updated_at: deletedAt,\n })\n\n await knex('record_lock_conflicts')\n .where({ tenant_id: tenantId })\n .whereNull('deleted_at')\n .andWhere((query) => {\n query\n .where((pending) => {\n pending.where('status', 'pending').andWhere('created_at', '<', pendingConflictCutoff)\n })\n .orWhere((resolved) => {\n resolved.whereNot('status', 'pending').andWhere('updated_at', '<', resolvedConflictCutoff)\n })\n })\n .update({\n deleted_at: deletedAt,\n updated_at: deletedAt,\n })\n } catch {\n // Best-effort cleanup must never fail lock workflows.\n }\n }\n\n private normalizeMutationHeaders(headers: Partial<RecordLockMutationHeaders>): Partial<RecordLockMutationHeaders> {\n const parsed = recordLockMutationHeaderSchema.partial().safeParse(headers)\n if (!parsed.success) return {}\n return parsed.data\n }\n\n private buildScopeWhere(scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'>): {\n tenantId: string\n deletedAt: null\n organizationId?: string | null\n } {\n const where: {\n tenantId: string\n deletedAt: null\n organizationId?: string | null\n } = {\n tenantId: scope.tenantId,\n deletedAt: null,\n }\n\n if (scope.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(scope.organizationId)\n }\n\n return where\n }\n\n private async findActiveLocks(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n now: Date,\n ): Promise<RecordLock[]> {\n const legacyFinder = (this as unknown as {\n findActiveLock?: (args: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource, at: Date) => Promise<RecordLock | null>\n }).findActiveLock\n if (typeof legacyFinder === 'function') {\n const legacyResult = await legacyFinder(input, now)\n return legacyResult ? [legacyResult] : []\n }\n\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n status: ACTIVE_LOCK_STATUS,\n }\n\n const locks = await this.em.find(RecordLock, where, { orderBy: { updatedAt: 'desc' } })\n if (!Array.isArray(locks) || !locks.length) return []\n\n let dirty = false\n const active: RecordLock[] = []\n const expiredLocks: RecordLock[] = []\n\n for (const lock of locks) {\n if (lock.expiresAt <= now) {\n this.markLockReleased(lock, {\n status: 'expired',\n reason: 'expired',\n releasedByUserId: lock.lockedByUserId,\n now,\n })\n dirty = true\n expiredLocks.push(lock)\n continue\n }\n\n active.push(lock)\n }\n\n if (dirty) await this.em.flush()\n if (expiredLocks.length) {\n const recipientUserIds = active.map((lock) => lock.lockedByUserId)\n for (const expiredLock of expiredLocks) {\n await emitRecordLocksEvent('record_locks.participant.left', {\n lockId: expiredLock.id,\n resourceKind: expiredLock.resourceKind,\n resourceId: expiredLock.resourceId,\n tenantId: expiredLock.tenantId,\n organizationId: expiredLock.organizationId,\n leftUserId: expiredLock.lockedByUserId,\n reason: 'expired',\n recipientUserIds,\n activeParticipantCount: active.length,\n })\n }\n }\n return active\n }\n\n private async findOwnedLockByToken(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'> & RecordLockResource & { token?: string },\n ): Promise<RecordLock | null> {\n if (!input.token) return null\n\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n token: input.token,\n lockedByUserId: input.userId,\n status: ACTIVE_LOCK_STATUS,\n }\n\n return this.em.findOne(RecordLock, where)\n }\n\n private async findOwnedActiveLock(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId' | 'userId'> & RecordLockResource,\n ): Promise<RecordLock | null> {\n const where: FilterQuery<RecordLock> = {\n ...this.buildScopeWhere(input),\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: ACTIVE_LOCK_STATUS,\n }\n return this.em.findOne(RecordLock, where)\n }\n\n private async hasRecentSavedRelease(input: {\n tenantId: string\n organizationId?: string | null\n userId: string\n resourceKind: string\n resourceId: string\n now: Date\n }): Promise<boolean> {\n const cutoff = new Date(input.now.getTime() - PARTICIPANT_REJOIN_AFTER_SAVE_SUPPRESS_MS)\n const where: FilterQuery<RecordLock> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: 'released',\n releaseReason: 'saved',\n deletedAt: null,\n releasedAt: { $gte: cutoff },\n }\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n const scoped = await this.em.findOne(RecordLock, where, { orderBy: { releasedAt: 'desc' } })\n if (scoped) return true\n if (input.organizationId === undefined) return false\n\n return Boolean(await this.em.findOne(RecordLock, {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n lockedByUserId: input.userId,\n status: 'released',\n releaseReason: 'saved',\n deletedAt: null,\n releasedAt: { $gte: cutoff },\n }, { orderBy: { releasedAt: 'desc' } }))\n }\n\n private markLockReleased(\n lock: RecordLock,\n params: {\n status: RecordLockStatus\n reason: RecordLockReleaseReason\n releasedByUserId: string\n now: Date\n },\n ) {\n lock.status = params.status\n lock.releaseReason = params.reason\n lock.releasedByUserId = params.releasedByUserId\n lock.releasedAt = params.now\n lock.updatedAt = params.now\n }\n\n private async findLatestActionLog(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n const where: FilterQuery<ActionLog> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n deletedAt: null,\n }\n\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n return this.em.findOne(ActionLog, where, { orderBy: { createdAt: 'desc' } })\n }\n\n private async findLatestActionLogWithScopeFallback(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n const scoped = await this.findLatestActionLog(input)\n if (scoped) return scoped\n if (input.organizationId !== null) return null\n\n return this.findLatestActionLog({\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n })\n }\n\n private async findLatestActionLogByActor(\n input: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n actorUserId: string,\n ): Promise<ActionLog | null> {\n const where: FilterQuery<ActionLog> = {\n tenantId: input.tenantId,\n resourceKind: input.resourceKind,\n resourceId: input.resourceId,\n actorUserId,\n deletedAt: null,\n }\n\n if (input.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(input.organizationId)\n }\n\n return this.em.findOne(ActionLog, where, { orderBy: { createdAt: 'desc' } })\n }\n\n private summarizeChangedFieldsFromActionLog(log: ActionLog | null): string {\n if (!log) return ''\n\n if (isRecordValue(log.changesJson)) {\n const fromChanges = Object.keys(log.changesJson)\n .filter((field) => !shouldSkipConflictField(field))\n .slice(0, 12)\n .map(formatChangedFieldLabel)\n .join(', ')\n if (fromChanges) return fromChanges\n }\n\n const before = isRecordValue(log.snapshotBefore) ? log.snapshotBefore : null\n const after = isRecordValue(log.snapshotAfter) ? log.snapshotAfter : null\n if (!before || !after) return ''\n\n const diffPaths = new Set<string>()\n this.collectSnapshotDiffPaths(before, after, null, diffPaths, new Set<unknown>())\n\n return Array.from(diffPaths)\n .filter((field) => !shouldSkipConflictField(field))\n .sort((left, right) => left.localeCompare(right))\n .slice(0, 12)\n .map(formatChangedFieldLabel)\n .join(', ')\n }\n\n private buildIncomingChangeRowsFromActionLog(log: ActionLog | null): Array<{\n field: string\n incoming: string\n current: string\n }> {\n if (!log || !isRecordValue(log.changesJson)) return []\n\n const rows: Array<{ field: string; incoming: string; current: string }> = []\n for (const [rawField, rawChange] of Object.entries(log.changesJson)) {\n if (rows.length >= 12) break\n if (shouldSkipConflictField(rawField)) continue\n\n const change = isRecordValue(rawChange) ? rawChange : {}\n const incoming = Object.prototype.hasOwnProperty.call(change, 'to')\n ? formatNotificationValue(change.to)\n : '-'\n const current = Object.prototype.hasOwnProperty.call(change, 'from')\n ? formatNotificationValue(change.from)\n : '-'\n\n rows.push({\n field: formatChangedFieldLabel(rawField),\n incoming,\n current,\n })\n }\n\n return rows\n }\n\n private toParticipantView(lock: RecordLock): RecordLockParticipantView {\n return {\n userId: lock.lockedByUserId,\n lockedByIp: lock.lockedByIp ?? null,\n lockedAt: normalizeDate(lock.lockedAt),\n lastHeartbeatAt: normalizeDate(lock.lastHeartbeatAt),\n expiresAt: normalizeDate(lock.expiresAt),\n }\n }\n\n private sortLocksByJoinOrder(locks: RecordLock[]): RecordLock[] {\n return [...locks].sort((left, right) => {\n const leftLockedAt = left.lockedAt instanceof Date ? left.lockedAt.getTime() : 0\n const rightLockedAt = right.lockedAt instanceof Date ? right.lockedAt.getTime() : 0\n if (leftLockedAt !== rightLockedAt) return leftLockedAt - rightLockedAt\n\n const leftCreatedAt = left.createdAt instanceof Date ? left.createdAt.getTime() : 0\n const rightCreatedAt = right.createdAt instanceof Date ? right.createdAt.getTime() : 0\n if (leftCreatedAt !== rightCreatedAt) return leftCreatedAt - rightCreatedAt\n\n return left.id.localeCompare(right.id)\n })\n }\n\n private toLockView(lock: RecordLock, includeToken: boolean, activeLocks: RecordLock[] = [lock]): RecordLockView {\n const participants = activeLocks.map((item) => this.toParticipantView(item))\n return {\n id: lock.id,\n resourceKind: lock.resourceKind,\n resourceId: lock.resourceId,\n token: includeToken ? lock.token : null,\n strategy: lock.strategy,\n status: lock.status,\n lockedByUserId: lock.lockedByUserId,\n lockedByIp: lock.lockedByIp ?? null,\n baseActionLogId: lock.baseActionLogId,\n lockedAt: normalizeDate(lock.lockedAt),\n lastHeartbeatAt: normalizeDate(lock.lastHeartbeatAt),\n expiresAt: normalizeDate(lock.expiresAt),\n participants,\n activeParticipantCount: participants.length,\n }\n }\n\n private async createConflict(input: {\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource\n baseActionLogId: string | null\n incomingActionLogId: string | null\n conflictActorUserId: string\n incomingActorUserId: string | null\n }): Promise<RecordLockConflict> {\n const dedupeKey = [\n 'record_locks',\n 'conflict',\n input.scope.tenantId,\n normalizeScopeOrganization(input.scope.organizationId) ?? 'global',\n input.scope.resourceKind,\n input.scope.resourceId,\n input.conflictActorUserId,\n input.baseActionLogId ?? 'none',\n input.incomingActionLogId ?? 'none',\n ].join(':')\n\n const result = await this.em.transactional(async (tx) => {\n try {\n const knex = getKnex(tx as EntityManager)\n await knex.raw('select pg_advisory_xact_lock(hashtext(?))', [dedupeKey])\n } catch {\n // Best-effort lock; fallback to find-first behavior below.\n }\n\n const existing = await this.findPendingConflictByFingerprint(tx as EntityManager, input)\n if (existing) {\n return { conflict: existing, created: false as const }\n }\n\n const conflict = tx.create(RecordLockConflict, {\n resourceKind: input.scope.resourceKind,\n resourceId: input.scope.resourceId,\n status: 'pending',\n resolution: null,\n baseActionLogId: input.baseActionLogId,\n incomingActionLogId: input.incomingActionLogId,\n conflictActorUserId: input.conflictActorUserId,\n incomingActorUserId: input.incomingActorUserId,\n tenantId: input.scope.tenantId,\n organizationId: normalizeScopeOrganization(input.scope.organizationId),\n })\n\n tx.persist(conflict)\n await tx.flush()\n return { conflict, created: true as const }\n })\n\n if (result.created) {\n await emitRecordLocksEvent('record_locks.conflict.detected', {\n conflictId: result.conflict.id,\n resourceKind: result.conflict.resourceKind,\n resourceId: result.conflict.resourceId,\n tenantId: result.conflict.tenantId,\n organizationId: result.conflict.organizationId,\n conflictActorUserId: result.conflict.conflictActorUserId,\n incomingActorUserId: result.conflict.incomingActorUserId,\n baseActionLogId: result.conflict.baseActionLogId,\n incomingActionLogId: result.conflict.incomingActionLogId,\n })\n }\n\n return result.conflict\n }\n\n private findPendingConflictByFingerprint(\n em: EntityManager,\n input: {\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource\n baseActionLogId: string | null\n incomingActionLogId: string | null\n conflictActorUserId: string\n },\n ): Promise<RecordLockConflict | null> {\n return em.findOne(RecordLockConflict, {\n tenantId: input.scope.tenantId,\n organizationId: normalizeScopeOrganization(input.scope.organizationId),\n resourceKind: input.scope.resourceKind,\n resourceId: input.scope.resourceId,\n conflictActorUserId: input.conflictActorUserId,\n status: 'pending',\n baseActionLogId: input.baseActionLogId,\n incomingActionLogId: input.incomingActionLogId,\n deletedAt: null,\n }, { orderBy: { createdAt: 'desc' } })\n }\n\n private async resolveConflict(\n conflict: RecordLockConflict,\n resolution: 'accept_incoming' | Extract<RecordLockMutationHeaders['resolution'], 'accept_mine' | 'merged'>,\n resolvedByUserId: string,\n ): Promise<void> {\n const now = new Date()\n\n const resolutionMap: Record<'accept_incoming' | Extract<RecordLockMutationHeaders['resolution'], 'accept_mine' | 'merged'>, {\n status: RecordLockConflictStatus\n resolution: RecordLockConflictResolution\n }> = {\n accept_incoming: { status: 'resolved_accept_incoming', resolution: 'accept_incoming' },\n accept_mine: { status: 'resolved_accept_mine', resolution: 'accept_mine' },\n merged: { status: 'resolved_merged', resolution: 'merged' },\n }\n\n const target = resolutionMap[resolution]\n conflict.status = target.status\n conflict.resolution = target.resolution\n conflict.resolvedByUserId = resolvedByUserId\n conflict.resolvedAt = now\n conflict.updatedAt = now\n await this.em.flush()\n\n await emitRecordLocksEvent('record_locks.conflict.resolved', {\n conflictId: conflict.id,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n tenantId: conflict.tenantId,\n organizationId: conflict.organizationId,\n conflictActorUserId: conflict.conflictActorUserId,\n incomingActorUserId: conflict.incomingActorUserId,\n resolution: conflict.resolution,\n resolvedByUserId,\n })\n }\n\n private async findConflictById(\n conflictId: string,\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<RecordLockConflict | null> {\n const where: FilterQuery<RecordLockConflict> = {\n id: conflictId,\n tenantId: scope.tenantId,\n resourceKind: scope.resourceKind,\n resourceId: scope.resourceId,\n deletedAt: null,\n }\n\n if (scope.organizationId !== undefined) {\n where.organizationId = normalizeScopeOrganization(scope.organizationId)\n }\n\n const scoped = await this.em.findOne(RecordLockConflict, where)\n if (scoped || scope.organizationId === undefined) return scoped\n\n return this.em.findOne(RecordLockConflict, {\n id: conflictId,\n tenantId: scope.tenantId,\n resourceKind: scope.resourceKind,\n resourceId: scope.resourceId,\n deletedAt: null,\n })\n }\n\n private async findActionLogById(\n logId: string | null,\n scope: Pick<RecordLockScope, 'tenantId' | 'organizationId'> & RecordLockResource,\n ): Promise<ActionLog | null> {\n if (!logId) return null\n\n let resolved = this.actionLogService\n ? await this.actionLogService.findById(logId)\n : null\n if (!resolved) {\n resolved = await this.em.findOne(ActionLog, { id: logId, deletedAt: null })\n }\n if (!resolved || resolved.deletedAt) return null\n\n if (resolved.tenantId !== scope.tenantId) return null\n\n if (scope.organizationId !== undefined) {\n const expectedOrganizationId = normalizeScopeOrganization(scope.organizationId)\n if (normalizeScopeOrganization(resolved.organizationId) !== expectedOrganizationId) return null\n }\n\n if (resolved.resourceKind !== scope.resourceKind || resolved.resourceId !== scope.resourceId) {\n return null\n }\n\n return resolved\n }\n\n private collectSnapshotDiffPaths(\n before: unknown,\n after: unknown,\n pathPrefix: string | null,\n output: Set<string>,\n seen: Set<unknown>,\n ): void {\n if (valuesEqual(before, after)) return\n\n const beforeRecord = isRecordValue(before) ? before : null\n const afterRecord = isRecordValue(after) ? after : null\n\n if (!beforeRecord || !afterRecord) {\n if (pathPrefix) output.add(pathPrefix)\n return\n }\n\n if (seen.has(beforeRecord) || seen.has(afterRecord)) {\n if (pathPrefix) output.add(pathPrefix)\n return\n }\n\n seen.add(beforeRecord)\n seen.add(afterRecord)\n\n const keys = new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)])\n for (const key of keys) {\n if (SKIPPED_CONFLICT_FIELDS.has(key)) continue\n const nextPath = pathPrefix ? `${pathPrefix}.${key}` : key\n this.collectSnapshotDiffPaths(beforeRecord[key], afterRecord[key], nextPath, output, seen)\n }\n }\n\n private async buildConflictChanges(\n conflict: RecordLockConflict,\n mutationPayload: Record<string, unknown> | null,\n ): Promise<RecordLockConflictChange[]> {\n const scope = {\n tenantId: conflict.tenantId,\n organizationId: conflict.organizationId,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n }\n\n const baseLog = await this.findActionLogById(conflict.baseActionLogId, scope)\n const incomingLog = await this.findActionLogById(conflict.incomingActionLogId, scope)\n\n const baseSnapshot = isRecordValue(baseLog?.snapshotAfter) ? baseLog.snapshotAfter : null\n const incomingBeforeSnapshot = isRecordValue(incomingLog?.snapshotBefore) ? incomingLog.snapshotBefore : null\n const incomingAfterSnapshot = isRecordValue(incomingLog?.snapshotAfter) ? incomingLog.snapshotAfter : null\n const fallbackBaseSnapshot = baseSnapshot ?? incomingBeforeSnapshot\n\n const changeMap = new Map<string, { baseValue: unknown; incomingValue: unknown }>()\n\n const incomingChanges = isRecordValue(incomingLog?.changesJson) ? incomingLog.changesJson : null\n if (incomingChanges) {\n for (const [fieldPathRaw, rawChange] of Object.entries(incomingChanges)) {\n const fieldPath = fieldPathRaw.trim()\n if (shouldSkipConflictField(fieldPath)) continue\n\n const changeRecord = isRecordValue(rawChange) ? rawChange : null\n const fromValue = changeRecord && Object.prototype.hasOwnProperty.call(changeRecord, 'from')\n ? changeRecord.from\n : readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n const toValue = changeRecord && Object.prototype.hasOwnProperty.call(changeRecord, 'to')\n ? changeRecord.to\n : readPathValueLoose(incomingAfterSnapshot, fieldPath)\n\n changeMap.set(fieldPath, {\n baseValue: fromValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(fromValue),\n incomingValue: toValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(toValue),\n })\n }\n }\n\n if (!changeMap.size && fallbackBaseSnapshot && incomingAfterSnapshot) {\n const diffPaths = new Set<string>()\n this.collectSnapshotDiffPaths(\n fallbackBaseSnapshot,\n incomingAfterSnapshot,\n null,\n diffPaths,\n new Set<unknown>(),\n )\n\n for (const fieldPath of diffPaths) {\n if (shouldSkipConflictField(fieldPath)) continue\n const fromValue = readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n const toValue = readPathValueLoose(incomingAfterSnapshot, fieldPath)\n changeMap.set(fieldPath, {\n baseValue: fromValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(fromValue),\n incomingValue: toValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(toValue),\n })\n }\n }\n\n if (!changeMap.size && mutationPayload && incomingAfterSnapshot) {\n for (const fieldPath of Object.keys(mutationPayload)) {\n if (shouldSkipConflictField(fieldPath)) continue\n const mineValue = readPathValueLoose(mutationPayload, fieldPath)\n const incomingValue = readPathValueLoose(incomingAfterSnapshot, fieldPath)\n if (mineValue === MISSING_CONFLICT_VALUE || incomingValue === MISSING_CONFLICT_VALUE) continue\n if (valuesEqual(mineValue, incomingValue)) continue\n const baseValue = readPathValueLoose(fallbackBaseSnapshot, fieldPath)\n changeMap.set(fieldPath, {\n baseValue: baseValue === MISSING_CONFLICT_VALUE ? null : normalizeConflictValue(baseValue),\n incomingValue: normalizeConflictValue(incomingValue),\n })\n }\n }\n\n if (!changeMap.size) return []\n\n const allFields = Array.from(changeMap.keys())\n const preferredFields = mutationPayload\n ? allFields.filter((fieldPath) => {\n const mineValue = readPathValueLoose(mutationPayload, fieldPath)\n if (mineValue === MISSING_CONFLICT_VALUE) return false\n const incomingValue = changeMap.get(fieldPath)?.incomingValue\n return !valuesEqual(mineValue, incomingValue)\n })\n : []\n const selectedFields = (preferredFields.length ? preferredFields : allFields)\n .filter((fieldPath) => !shouldSkipConflictField(fieldPath))\n .sort((left, right) => left.localeCompare(right))\n .slice(0, 25)\n\n return selectedFields.map((fieldPath) => {\n const entry = changeMap.get(fieldPath) ?? { baseValue: null, incomingValue: null }\n const mineValueRaw = mutationPayload ? readPathValueLoose(mutationPayload, fieldPath) : MISSING_CONFLICT_VALUE\n const mineValue = mineValueRaw === MISSING_CONFLICT_VALUE\n ? entry.baseValue\n : normalizeConflictValue(mineValueRaw)\n\n return {\n field: fieldPath,\n displayValue: normalizeConflictValue(entry.baseValue),\n baseValue: normalizeConflictValue(entry.baseValue),\n incomingValue: normalizeConflictValue(entry.incomingValue),\n mineValue: normalizeConflictValue(mineValue),\n }\n })\n }\n\n private async toConflictPayload(\n conflict: RecordLockConflict,\n mutationPayload: Record<string, unknown> | null,\n allowIncomingOverride: boolean,\n canOverrideIncoming: boolean,\n ): Promise<RecordLockConflictPayload> {\n const changes = await this.buildConflictChanges(conflict, mutationPayload)\n return {\n id: conflict.id,\n resourceKind: conflict.resourceKind,\n resourceId: conflict.resourceId,\n baseActionLogId: conflict.baseActionLogId,\n incomingActionLogId: conflict.incomingActionLogId,\n allowIncomingOverride,\n canOverrideIncoming,\n resolutionOptions: canOverrideIncoming ? ['accept_mine'] : [],\n changes,\n }\n }\n}\n\nexport function createRecordLockService(deps: RecordLockServiceDeps): RecordLockService {\n return new RecordLockService(deps)\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": ["lock"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/enterprise",
|
|
3
|
-
"version": "0.4.6-
|
|
3
|
+
"version": "0.4.6-main-24e64eef39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@open-mercato/core": "0.4.6-
|
|
68
|
-
"@open-mercato/shared": "0.4.6-
|
|
69
|
-
"@open-mercato/ui": "0.4.6-
|
|
67
|
+
"@open-mercato/core": "0.4.6-main-24e64eef39",
|
|
68
|
+
"@open-mercato/shared": "0.4.6-main-24e64eef39",
|
|
69
|
+
"@open-mercato/ui": "0.4.6-main-24e64eef39"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/jest": "^30.0.0",
|
|
@@ -463,7 +463,7 @@ export class RecordLockService {
|
|
|
463
463
|
if (!this.moduleConfigService) return settings
|
|
464
464
|
|
|
465
465
|
await this.moduleConfigService.setValue(RECORD_LOCKS_MODULE_ID, RECORD_LOCKS_SETTINGS_NAME, settings)
|
|
466
|
-
return settings
|
|
466
|
+
return settings
|
|
467
467
|
}
|
|
468
468
|
|
|
469
469
|
async acquire(input: RecordLockAcquireInput): Promise<RecordLockAcquireResult | RecordLockAcquireFailure> {
|