@open-mercato/core 0.6.6-develop.6395.1.f7f5283bc9 → 0.6.6-develop.6397.1.c078d218fa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/configs/lib/system-status.js +36 -0
- package/dist/modules/configs/lib/system-status.js.map +2 -2
- package/dist/modules/query_index/lib/coverage.js +51 -4
- package/dist/modules/query_index/lib/coverage.js.map +2 -2
- package/dist/modules/query_index/subscribers/coverage_warmup.js +99 -17
- package/dist/modules/query_index/subscribers/coverage_warmup.js.map +2 -2
- package/dist/modules/query_index/subscribers/delete_one.js +14 -1
- package/dist/modules/query_index/subscribers/delete_one.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/configs/i18n/de.json +8 -0
- package/src/modules/configs/i18n/en.json +8 -0
- package/src/modules/configs/i18n/es.json +8 -0
- package/src/modules/configs/i18n/pl.json +8 -0
- package/src/modules/configs/lib/system-status.ts +36 -0
- package/src/modules/query_index/lib/coverage.ts +85 -10
- package/src/modules/query_index/subscribers/coverage_warmup.ts +142 -16
- package/src/modules/query_index/subscribers/delete_one.ts +24 -1
|
@@ -236,6 +236,42 @@ const SYSTEM_STATUS_VARIABLES = [
|
|
|
236
236
|
docUrl: `${SYSTEM_STATUS_DOC_BASE}#force_query_index_on_partial_indexes`,
|
|
237
237
|
defaultValue: "false"
|
|
238
238
|
},
|
|
239
|
+
{
|
|
240
|
+
key: "QUERY_INDEX_WARMUP_ENABLED",
|
|
241
|
+
category: "query_index",
|
|
242
|
+
kind: "boolean",
|
|
243
|
+
labelKey: "configs.systemStatus.variables.queryIndexWarmupEnabled.label",
|
|
244
|
+
descriptionKey: "configs.systemStatus.variables.queryIndexWarmupEnabled.description",
|
|
245
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_enabled`,
|
|
246
|
+
defaultValue: "true"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
key: "QUERY_INDEX_WARMUP_THROTTLE_MS",
|
|
250
|
+
category: "query_index",
|
|
251
|
+
kind: "string",
|
|
252
|
+
labelKey: "configs.systemStatus.variables.queryIndexWarmupThrottleMs.label",
|
|
253
|
+
descriptionKey: "configs.systemStatus.variables.queryIndexWarmupThrottleMs.description",
|
|
254
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_throttle_ms`,
|
|
255
|
+
defaultValue: String(5 * 60 * 1e3)
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
key: "QUERY_INDEX_WARMUP_CONCURRENCY",
|
|
259
|
+
category: "query_index",
|
|
260
|
+
kind: "string",
|
|
261
|
+
labelKey: "configs.systemStatus.variables.queryIndexWarmupConcurrency.label",
|
|
262
|
+
descriptionKey: "configs.systemStatus.variables.queryIndexWarmupConcurrency.description",
|
|
263
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_concurrency`,
|
|
264
|
+
defaultValue: "10"
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
key: "QUERY_INDEX_WARMUP_STAGGER_MS",
|
|
268
|
+
category: "query_index",
|
|
269
|
+
kind: "string",
|
|
270
|
+
labelKey: "configs.systemStatus.variables.queryIndexWarmupStaggerMs.label",
|
|
271
|
+
descriptionKey: "configs.systemStatus.variables.queryIndexWarmupStaggerMs.description",
|
|
272
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_stagger_ms`,
|
|
273
|
+
defaultValue: "0"
|
|
274
|
+
},
|
|
239
275
|
{
|
|
240
276
|
key: "ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM",
|
|
241
277
|
category: "entities",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/configs/lib/system-status.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n SystemStatusCategory,\n SystemStatusCategoryKey,\n SystemStatusItem,\n SystemStatusSnapshot,\n SystemStatusVariableKind,\n SystemStatusState,\n SystemStatusRuntimeMode,\n} from './system-status.types'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype SystemStatusVariableDefinition = {\n key: string\n category: SystemStatusCategoryKey\n kind: SystemStatusVariableKind\n labelKey: string\n descriptionKey: string\n docUrl: string | null\n defaultValue: string | null\n valueTransform?: (raw: string | undefined) => string | undefined\n}\n\nconst CATEGORY_ORDER: SystemStatusCategoryKey[] = [\n 'profiling',\n 'logging',\n 'security',\n 'caching',\n 'query_index',\n 'entities',\n]\n\nconst CATEGORY_METADATA: Record<\n SystemStatusCategoryKey,\n { labelKey: string; descriptionKey: string | null }\n> = {\n profiling: {\n labelKey: 'configs.systemStatus.categories.profiling',\n descriptionKey: 'configs.systemStatus.categories.profilingDescription',\n },\n logging: {\n labelKey: 'configs.systemStatus.categories.logging',\n descriptionKey: 'configs.systemStatus.categories.loggingDescription',\n },\n security: {\n labelKey: 'configs.systemStatus.categories.security',\n descriptionKey: 'configs.systemStatus.categories.securityDescription',\n },\n caching: {\n labelKey: 'configs.systemStatus.categories.caching',\n descriptionKey: 'configs.systemStatus.categories.cachingDescription',\n },\n query_index: {\n labelKey: 'configs.systemStatus.categories.queryIndex',\n descriptionKey: 'configs.systemStatus.categories.queryIndexDescription',\n },\n entities: {\n labelKey: 'configs.systemStatus.categories.entities',\n descriptionKey: 'configs.systemStatus.categories.entitiesDescription',\n },\n}\n\nconst SYSTEM_STATUS_DOC_BASE = 'https://docs.openmercato.com/docs/framework/operations/system-status'\nconst DEFAULT_SQLITE_CACHE_PATH = './.mercato/cache/cache.db'\n\nfunction maskConnectionCredentials(raw: string | undefined): string | undefined {\n if (typeof raw !== 'string') return raw\n const trimmed = raw.trim()\n if (!trimmed) return trimmed\n\n const maskAuthorityLikeCredentials = (value: string): string => {\n const schemeIndex = value.indexOf('://')\n if (schemeIndex < 0) return value\n\n const userInfoStart = schemeIndex + 3\n const queryIndex = value.indexOf('?', userInfoStart)\n const fragmentIndex = value.indexOf('#', userInfoStart)\n const searchEnd =\n queryIndex >= 0 && fragmentIndex >= 0\n ? Math.min(queryIndex, fragmentIndex)\n : queryIndex >= 0\n ? queryIndex\n : fragmentIndex >= 0\n ? fragmentIndex\n : value.length\n\n const authorityLikeSegment = value.slice(userInfoStart, searchEnd)\n const lastAtIndex = authorityLikeSegment.lastIndexOf('@')\n if (lastAtIndex < 0) return value\n\n return `${value.slice(0, userInfoStart)}${authorityLikeSegment.slice(lastAtIndex + 1)}${value.slice(searchEnd)}`\n }\n\n try {\n const parsed = new URL(trimmed)\n if (!parsed.username && !parsed.password) {\n return trimmed\n }\n parsed.username = ''\n parsed.password = ''\n return parsed.toString()\n } catch {\n return maskAuthorityLikeCredentials(trimmed)\n }\n}\n\nexport const SYSTEM_STATUS_VARIABLES: SystemStatusVariableDefinition[] = [\n {\n key: 'OM_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_profile`,\n defaultValue: '',\n },\n {\n key: 'NEXT_PUBLIC_OM_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.nextPublicOmProfile.label',\n descriptionKey: 'configs.systemStatus.variables.nextPublicOmProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#next_public_om_profile`,\n defaultValue: '',\n },\n {\n key: 'OM_CRUD_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omCrudProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omCrudProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_crud_profile`,\n defaultValue: '',\n },\n {\n key: 'OM_QE_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omQeProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omQeProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_qe_profile`,\n defaultValue: '',\n },\n {\n key: 'QUERY_ENGINE_DEBUG_SQL',\n category: 'logging',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.queryEngineDebugSql.label',\n descriptionKey: 'configs.systemStatus.variables.queryEngineDebugSql.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_engine_debug_sql`,\n defaultValue: 'false',\n },\n {\n key: 'LOG_VERBOSITY',\n category: 'logging',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.logVerbosity.label',\n descriptionKey: 'configs.systemStatus.variables.logVerbosity.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_verbosity`,\n defaultValue: '',\n },\n {\n key: 'LOG_LEVEL',\n category: 'logging',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.logLevel.label',\n descriptionKey: 'configs.systemStatus.variables.logLevel.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_level`,\n defaultValue: '',\n },\n {\n key: 'OM_PASSWORD_MIN_LENGTH',\n category: 'security',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.passwordMinLength.label',\n descriptionKey: 'configs.systemStatus.variables.passwordMinLength.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_min_length`,\n defaultValue: '6',\n },\n {\n key: 'DATABASE_URL',\n category: 'security',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.databaseUrl.label',\n descriptionKey: 'configs.systemStatus.variables.databaseUrl.description',\n docUrl: null,\n defaultValue: null,\n valueTransform: maskConnectionCredentials,\n },\n {\n key: 'OM_PASSWORD_REQUIRE_DIGIT',\n category: 'security',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.passwordRequireDigit.label',\n descriptionKey: 'configs.systemStatus.variables.passwordRequireDigit.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_digit`,\n defaultValue: 'true',\n },\n {\n key: 'OM_PASSWORD_REQUIRE_UPPERCASE',\n category: 'security',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.passwordRequireUppercase.label',\n descriptionKey: 'configs.systemStatus.variables.passwordRequireUppercase.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_uppercase`,\n defaultValue: 'true',\n },\n {\n key: 'OM_PASSWORD_REQUIRE_SPECIAL',\n category: 'security',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.passwordRequireSpecial.label',\n descriptionKey: 'configs.systemStatus.variables.passwordRequireSpecial.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_special`,\n defaultValue: 'true',\n },\n {\n key: 'ENABLE_CRUD_API_CACHE',\n category: 'caching',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.enableCrudApiCache.label',\n descriptionKey: 'configs.systemStatus.variables.enableCrudApiCache.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#enable_crud_api_cache`,\n defaultValue: 'false',\n },\n {\n key: 'CACHE_STRATEGY',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheStrategy.label',\n descriptionKey: 'configs.systemStatus.variables.cacheStrategy.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_strategy`,\n defaultValue: 'memory',\n },\n {\n key: 'CACHE_TTL',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheTtl.label',\n descriptionKey: 'configs.systemStatus.variables.cacheTtl.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_ttl`,\n defaultValue: '',\n },\n {\n key: 'CACHE_SQLITE_PATH',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheSqlitePath.label',\n descriptionKey: 'configs.systemStatus.variables.cacheSqlitePath.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_sqlite_path`,\n defaultValue: DEFAULT_SQLITE_CACHE_PATH,\n },\n {\n key: 'SCHEDULE_AUTO_REINDEX',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.scheduleAutoReindex.label',\n descriptionKey: 'configs.systemStatus.variables.scheduleAutoReindex.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#schedule_auto_reindex`,\n defaultValue: 'true',\n },\n {\n key: 'OPTIMIZE_INDEX_COVERAGE_STATS',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.optimizeIndexCoverageStats.label',\n descriptionKey: 'configs.systemStatus.variables.optimizeIndexCoverageStats.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#optimize_index_coverage_stats`,\n defaultValue: 'false',\n },\n {\n key: 'FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.forceQueryIndexOnPartialIndexes.label',\n descriptionKey: 'configs.systemStatus.variables.forceQueryIndexOnPartialIndexes.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#force_query_index_on_partial_indexes`,\n defaultValue: 'false',\n },\n {\n key: 'ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM',\n category: 'entities',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.entitiesBackcompatEav.label',\n descriptionKey: 'configs.systemStatus.variables.entitiesBackcompatEav.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#entities_backcompat_eav_for_custom`,\n defaultValue: 'false',\n },\n]\n\ntype AnalyzedValue = { state: SystemStatusState; value: string | null; normalizedValue: string | null }\n\nfunction analyzeBooleanValue(raw: string | undefined): AnalyzedValue {\n if (typeof raw !== 'string') {\n return { state: 'unset', value: null, normalizedValue: null }\n }\n const trimmed = raw.trim()\n if (!trimmed) return { state: 'unset', value: null, normalizedValue: null }\n const parsed = parseBooleanToken(trimmed)\n if (parsed === true) {\n return { state: 'enabled', value: trimmed, normalizedValue: 'true' }\n }\n if (parsed === false) {\n return { state: 'disabled', value: trimmed, normalizedValue: 'false' }\n }\n return { state: 'unknown', value: trimmed, normalizedValue: trimmed }\n}\n\nfunction analyzeStringValue(raw: string | undefined): AnalyzedValue {\n if (typeof raw !== 'string') {\n return { state: 'unset', value: null, normalizedValue: null }\n }\n const trimmed = raw.trim()\n if (!trimmed) return { state: 'unset', value: null, normalizedValue: null }\n return { state: 'set', value: trimmed, normalizedValue: trimmed }\n}\n\nfunction toItem(definition: SystemStatusVariableDefinition, env: Record<string, string | undefined>): SystemStatusItem {\n const raw = definition.valueTransform ? definition.valueTransform(env[definition.key]) : env[definition.key]\n const analyzed = definition.kind === 'boolean' ? analyzeBooleanValue(raw) : analyzeStringValue(raw)\n return {\n key: definition.key,\n category: definition.category,\n kind: definition.kind,\n labelKey: definition.labelKey,\n descriptionKey: definition.descriptionKey,\n docUrl: definition.docUrl,\n defaultValue: definition.defaultValue,\n state: analyzed.state,\n value: analyzed.value,\n normalizedValue: analyzed.normalizedValue,\n }\n}\n\nfunction buildCategorySnapshot(\n key: SystemStatusCategoryKey,\n items: SystemStatusItem[],\n): SystemStatusCategory {\n const metadata = CATEGORY_METADATA[key]\n return {\n key,\n labelKey: metadata.labelKey,\n descriptionKey: metadata.descriptionKey,\n items,\n }\n}\n\nfunction resolveRuntimeMode(env: Record<string, string | undefined>): SystemStatusRuntimeMode {\n const raw = env.NODE_ENV\n if (typeof raw !== 'string') return 'unknown'\n const value = raw.trim().toLowerCase()\n if (value === 'development') return 'development'\n if (value === 'production') return 'production'\n if (value === 'test') return 'test'\n return 'unknown'\n}\n\nexport function buildSystemStatusSnapshot(\n env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,\n): SystemStatusSnapshot {\n const byCategory = new Map<SystemStatusCategoryKey, SystemStatusItem[]>()\n for (const definition of SYSTEM_STATUS_VARIABLES) {\n const bucket = byCategory.get(definition.category)\n const item = toItem(definition, env)\n if (bucket) {\n bucket.push(item)\n } else {\n byCategory.set(definition.category, [item])\n }\n }\n\n const categories: SystemStatusCategory[] = []\n for (const categoryKey of CATEGORY_ORDER) {\n const items = byCategory.get(categoryKey) ?? []\n categories.push(buildCategorySnapshot(categoryKey, items))\n }\n\n return {\n generatedAt: new Date().toISOString(),\n runtimeMode: resolveRuntimeMode(env),\n categories,\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type {\n SystemStatusCategory,\n SystemStatusCategoryKey,\n SystemStatusItem,\n SystemStatusSnapshot,\n SystemStatusVariableKind,\n SystemStatusState,\n SystemStatusRuntimeMode,\n} from './system-status.types'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\n\ntype SystemStatusVariableDefinition = {\n key: string\n category: SystemStatusCategoryKey\n kind: SystemStatusVariableKind\n labelKey: string\n descriptionKey: string\n docUrl: string | null\n defaultValue: string | null\n valueTransform?: (raw: string | undefined) => string | undefined\n}\n\nconst CATEGORY_ORDER: SystemStatusCategoryKey[] = [\n 'profiling',\n 'logging',\n 'security',\n 'caching',\n 'query_index',\n 'entities',\n]\n\nconst CATEGORY_METADATA: Record<\n SystemStatusCategoryKey,\n { labelKey: string; descriptionKey: string | null }\n> = {\n profiling: {\n labelKey: 'configs.systemStatus.categories.profiling',\n descriptionKey: 'configs.systemStatus.categories.profilingDescription',\n },\n logging: {\n labelKey: 'configs.systemStatus.categories.logging',\n descriptionKey: 'configs.systemStatus.categories.loggingDescription',\n },\n security: {\n labelKey: 'configs.systemStatus.categories.security',\n descriptionKey: 'configs.systemStatus.categories.securityDescription',\n },\n caching: {\n labelKey: 'configs.systemStatus.categories.caching',\n descriptionKey: 'configs.systemStatus.categories.cachingDescription',\n },\n query_index: {\n labelKey: 'configs.systemStatus.categories.queryIndex',\n descriptionKey: 'configs.systemStatus.categories.queryIndexDescription',\n },\n entities: {\n labelKey: 'configs.systemStatus.categories.entities',\n descriptionKey: 'configs.systemStatus.categories.entitiesDescription',\n },\n}\n\nconst SYSTEM_STATUS_DOC_BASE = 'https://docs.openmercato.com/docs/framework/operations/system-status'\nconst DEFAULT_SQLITE_CACHE_PATH = './.mercato/cache/cache.db'\n\nfunction maskConnectionCredentials(raw: string | undefined): string | undefined {\n if (typeof raw !== 'string') return raw\n const trimmed = raw.trim()\n if (!trimmed) return trimmed\n\n const maskAuthorityLikeCredentials = (value: string): string => {\n const schemeIndex = value.indexOf('://')\n if (schemeIndex < 0) return value\n\n const userInfoStart = schemeIndex + 3\n const queryIndex = value.indexOf('?', userInfoStart)\n const fragmentIndex = value.indexOf('#', userInfoStart)\n const searchEnd =\n queryIndex >= 0 && fragmentIndex >= 0\n ? Math.min(queryIndex, fragmentIndex)\n : queryIndex >= 0\n ? queryIndex\n : fragmentIndex >= 0\n ? fragmentIndex\n : value.length\n\n const authorityLikeSegment = value.slice(userInfoStart, searchEnd)\n const lastAtIndex = authorityLikeSegment.lastIndexOf('@')\n if (lastAtIndex < 0) return value\n\n return `${value.slice(0, userInfoStart)}${authorityLikeSegment.slice(lastAtIndex + 1)}${value.slice(searchEnd)}`\n }\n\n try {\n const parsed = new URL(trimmed)\n if (!parsed.username && !parsed.password) {\n return trimmed\n }\n parsed.username = ''\n parsed.password = ''\n return parsed.toString()\n } catch {\n return maskAuthorityLikeCredentials(trimmed)\n }\n}\n\nexport const SYSTEM_STATUS_VARIABLES: SystemStatusVariableDefinition[] = [\n {\n key: 'OM_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_profile`,\n defaultValue: '',\n },\n {\n key: 'NEXT_PUBLIC_OM_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.nextPublicOmProfile.label',\n descriptionKey: 'configs.systemStatus.variables.nextPublicOmProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#next_public_om_profile`,\n defaultValue: '',\n },\n {\n key: 'OM_CRUD_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omCrudProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omCrudProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_crud_profile`,\n defaultValue: '',\n },\n {\n key: 'OM_QE_PROFILE',\n category: 'profiling',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.omQeProfile.label',\n descriptionKey: 'configs.systemStatus.variables.omQeProfile.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_qe_profile`,\n defaultValue: '',\n },\n {\n key: 'QUERY_ENGINE_DEBUG_SQL',\n category: 'logging',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.queryEngineDebugSql.label',\n descriptionKey: 'configs.systemStatus.variables.queryEngineDebugSql.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_engine_debug_sql`,\n defaultValue: 'false',\n },\n {\n key: 'LOG_VERBOSITY',\n category: 'logging',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.logVerbosity.label',\n descriptionKey: 'configs.systemStatus.variables.logVerbosity.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_verbosity`,\n defaultValue: '',\n },\n {\n key: 'LOG_LEVEL',\n category: 'logging',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.logLevel.label',\n descriptionKey: 'configs.systemStatus.variables.logLevel.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#log_level`,\n defaultValue: '',\n },\n {\n key: 'OM_PASSWORD_MIN_LENGTH',\n category: 'security',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.passwordMinLength.label',\n descriptionKey: 'configs.systemStatus.variables.passwordMinLength.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_min_length`,\n defaultValue: '6',\n },\n {\n key: 'DATABASE_URL',\n category: 'security',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.databaseUrl.label',\n descriptionKey: 'configs.systemStatus.variables.databaseUrl.description',\n docUrl: null,\n defaultValue: null,\n valueTransform: maskConnectionCredentials,\n },\n {\n key: 'OM_PASSWORD_REQUIRE_DIGIT',\n category: 'security',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.passwordRequireDigit.label',\n descriptionKey: 'configs.systemStatus.variables.passwordRequireDigit.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_digit`,\n defaultValue: 'true',\n },\n {\n key: 'OM_PASSWORD_REQUIRE_UPPERCASE',\n category: 'security',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.passwordRequireUppercase.label',\n descriptionKey: 'configs.systemStatus.variables.passwordRequireUppercase.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_uppercase`,\n defaultValue: 'true',\n },\n {\n key: 'OM_PASSWORD_REQUIRE_SPECIAL',\n category: 'security',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.passwordRequireSpecial.label',\n descriptionKey: 'configs.systemStatus.variables.passwordRequireSpecial.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#om_password_require_special`,\n defaultValue: 'true',\n },\n {\n key: 'ENABLE_CRUD_API_CACHE',\n category: 'caching',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.enableCrudApiCache.label',\n descriptionKey: 'configs.systemStatus.variables.enableCrudApiCache.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#enable_crud_api_cache`,\n defaultValue: 'false',\n },\n {\n key: 'CACHE_STRATEGY',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheStrategy.label',\n descriptionKey: 'configs.systemStatus.variables.cacheStrategy.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_strategy`,\n defaultValue: 'memory',\n },\n {\n key: 'CACHE_TTL',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheTtl.label',\n descriptionKey: 'configs.systemStatus.variables.cacheTtl.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_ttl`,\n defaultValue: '',\n },\n {\n key: 'CACHE_SQLITE_PATH',\n category: 'caching',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.cacheSqlitePath.label',\n descriptionKey: 'configs.systemStatus.variables.cacheSqlitePath.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#cache_sqlite_path`,\n defaultValue: DEFAULT_SQLITE_CACHE_PATH,\n },\n {\n key: 'SCHEDULE_AUTO_REINDEX',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.scheduleAutoReindex.label',\n descriptionKey: 'configs.systemStatus.variables.scheduleAutoReindex.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#schedule_auto_reindex`,\n defaultValue: 'true',\n },\n {\n key: 'OPTIMIZE_INDEX_COVERAGE_STATS',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.optimizeIndexCoverageStats.label',\n descriptionKey: 'configs.systemStatus.variables.optimizeIndexCoverageStats.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#optimize_index_coverage_stats`,\n defaultValue: 'false',\n },\n {\n key: 'FORCE_QUERY_INDEX_ON_PARTIAL_INDEXES',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.forceQueryIndexOnPartialIndexes.label',\n descriptionKey: 'configs.systemStatus.variables.forceQueryIndexOnPartialIndexes.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#force_query_index_on_partial_indexes`,\n defaultValue: 'false',\n },\n {\n key: 'QUERY_INDEX_WARMUP_ENABLED',\n category: 'query_index',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.queryIndexWarmupEnabled.label',\n descriptionKey: 'configs.systemStatus.variables.queryIndexWarmupEnabled.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_enabled`,\n defaultValue: 'true',\n },\n {\n key: 'QUERY_INDEX_WARMUP_THROTTLE_MS',\n category: 'query_index',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.queryIndexWarmupThrottleMs.label',\n descriptionKey: 'configs.systemStatus.variables.queryIndexWarmupThrottleMs.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_throttle_ms`,\n defaultValue: String(5 * 60 * 1000),\n },\n {\n key: 'QUERY_INDEX_WARMUP_CONCURRENCY',\n category: 'query_index',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.queryIndexWarmupConcurrency.label',\n descriptionKey: 'configs.systemStatus.variables.queryIndexWarmupConcurrency.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_concurrency`,\n defaultValue: '10',\n },\n {\n key: 'QUERY_INDEX_WARMUP_STAGGER_MS',\n category: 'query_index',\n kind: 'string',\n labelKey: 'configs.systemStatus.variables.queryIndexWarmupStaggerMs.label',\n descriptionKey: 'configs.systemStatus.variables.queryIndexWarmupStaggerMs.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_stagger_ms`,\n defaultValue: '0',\n },\n {\n key: 'ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM',\n category: 'entities',\n kind: 'boolean',\n labelKey: 'configs.systemStatus.variables.entitiesBackcompatEav.label',\n descriptionKey: 'configs.systemStatus.variables.entitiesBackcompatEav.description',\n docUrl: `${SYSTEM_STATUS_DOC_BASE}#entities_backcompat_eav_for_custom`,\n defaultValue: 'false',\n },\n]\n\ntype AnalyzedValue = { state: SystemStatusState; value: string | null; normalizedValue: string | null }\n\nfunction analyzeBooleanValue(raw: string | undefined): AnalyzedValue {\n if (typeof raw !== 'string') {\n return { state: 'unset', value: null, normalizedValue: null }\n }\n const trimmed = raw.trim()\n if (!trimmed) return { state: 'unset', value: null, normalizedValue: null }\n const parsed = parseBooleanToken(trimmed)\n if (parsed === true) {\n return { state: 'enabled', value: trimmed, normalizedValue: 'true' }\n }\n if (parsed === false) {\n return { state: 'disabled', value: trimmed, normalizedValue: 'false' }\n }\n return { state: 'unknown', value: trimmed, normalizedValue: trimmed }\n}\n\nfunction analyzeStringValue(raw: string | undefined): AnalyzedValue {\n if (typeof raw !== 'string') {\n return { state: 'unset', value: null, normalizedValue: null }\n }\n const trimmed = raw.trim()\n if (!trimmed) return { state: 'unset', value: null, normalizedValue: null }\n return { state: 'set', value: trimmed, normalizedValue: trimmed }\n}\n\nfunction toItem(definition: SystemStatusVariableDefinition, env: Record<string, string | undefined>): SystemStatusItem {\n const raw = definition.valueTransform ? definition.valueTransform(env[definition.key]) : env[definition.key]\n const analyzed = definition.kind === 'boolean' ? analyzeBooleanValue(raw) : analyzeStringValue(raw)\n return {\n key: definition.key,\n category: definition.category,\n kind: definition.kind,\n labelKey: definition.labelKey,\n descriptionKey: definition.descriptionKey,\n docUrl: definition.docUrl,\n defaultValue: definition.defaultValue,\n state: analyzed.state,\n value: analyzed.value,\n normalizedValue: analyzed.normalizedValue,\n }\n}\n\nfunction buildCategorySnapshot(\n key: SystemStatusCategoryKey,\n items: SystemStatusItem[],\n): SystemStatusCategory {\n const metadata = CATEGORY_METADATA[key]\n return {\n key,\n labelKey: metadata.labelKey,\n descriptionKey: metadata.descriptionKey,\n items,\n }\n}\n\nfunction resolveRuntimeMode(env: Record<string, string | undefined>): SystemStatusRuntimeMode {\n const raw = env.NODE_ENV\n if (typeof raw !== 'string') return 'unknown'\n const value = raw.trim().toLowerCase()\n if (value === 'development') return 'development'\n if (value === 'production') return 'production'\n if (value === 'test') return 'test'\n return 'unknown'\n}\n\nexport function buildSystemStatusSnapshot(\n env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,\n): SystemStatusSnapshot {\n const byCategory = new Map<SystemStatusCategoryKey, SystemStatusItem[]>()\n for (const definition of SYSTEM_STATUS_VARIABLES) {\n const bucket = byCategory.get(definition.category)\n const item = toItem(definition, env)\n if (bucket) {\n bucket.push(item)\n } else {\n byCategory.set(definition.category, [item])\n }\n }\n\n const categories: SystemStatusCategory[] = []\n for (const categoryKey of CATEGORY_ORDER) {\n const items = byCategory.get(categoryKey) ?? []\n categories.push(buildCategorySnapshot(categoryKey, items))\n }\n\n return {\n generatedAt: new Date().toISOString(),\n runtimeMode: resolveRuntimeMode(env),\n categories,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sql } from "kysely";
|
|
2
2
|
import { resolveEntityTableName } from "@open-mercato/shared/lib/query/engine";
|
|
3
3
|
const COLUMN_CACHE = /* @__PURE__ */ new Map();
|
|
4
|
+
const COLUMN_CACHE_PENDING = /* @__PURE__ */ new Map();
|
|
4
5
|
const GLOBAL_ORGANIZATION_PLACEHOLDER = "00000000-0000-0000-0000-000000000000";
|
|
5
6
|
const COVERAGE_ORG_PLACEHOLDER = GLOBAL_ORGANIZATION_PLACEHOLDER;
|
|
6
7
|
function toCount(value) {
|
|
@@ -158,10 +159,55 @@ async function deleteCoverageForEntity(db, entityType) {
|
|
|
158
159
|
async function tableHasColumn(db, table, column) {
|
|
159
160
|
const key = `${table}.${column}`;
|
|
160
161
|
if (COLUMN_CACHE.has(key)) return COLUMN_CACHE.get(key);
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
162
|
+
const pending = COLUMN_CACHE_PENDING.get(key);
|
|
163
|
+
if (pending) return pending;
|
|
164
|
+
const promise = (async () => {
|
|
165
|
+
const exists = await db.selectFrom("information_schema.columns").select(sql`1`.as("present")).where(sql`table_schema = current_schema()`).where("table_name", "=", table).where("column_name", "=", column).executeTakeFirst();
|
|
166
|
+
const present = !!exists;
|
|
167
|
+
COLUMN_CACHE.set(key, present);
|
|
168
|
+
return present;
|
|
169
|
+
})();
|
|
170
|
+
COLUMN_CACHE_PENDING.set(key, promise);
|
|
171
|
+
try {
|
|
172
|
+
return await promise;
|
|
173
|
+
} finally {
|
|
174
|
+
COLUMN_CACHE_PENDING.delete(key);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function primeColumnCache(db, checks) {
|
|
178
|
+
const missing = [];
|
|
179
|
+
const seen = /* @__PURE__ */ new Set();
|
|
180
|
+
for (const check of checks) {
|
|
181
|
+
const table = String(check?.table || "");
|
|
182
|
+
const column = String(check?.column || "");
|
|
183
|
+
if (!table || !column) continue;
|
|
184
|
+
const key = `${table}.${column}`;
|
|
185
|
+
if (seen.has(key)) continue;
|
|
186
|
+
seen.add(key);
|
|
187
|
+
if (COLUMN_CACHE.has(key) || COLUMN_CACHE_PENDING.has(key)) continue;
|
|
188
|
+
missing.push({ table, column, key });
|
|
189
|
+
}
|
|
190
|
+
if (!missing.length) return;
|
|
191
|
+
const tables = Array.from(new Set(missing.map((entry) => entry.table)));
|
|
192
|
+
const columns = Array.from(new Set(missing.map((entry) => entry.column)));
|
|
193
|
+
const batchPromise = (async () => {
|
|
194
|
+
const rows = await db.selectFrom("information_schema.columns").select(["table_name", "column_name"]).where(sql`table_schema = current_schema()`).where("table_name", "in", tables).where("column_name", "in", columns).execute();
|
|
195
|
+
return new Set(rows.map((row) => `${row.table_name}.${row.column_name}`));
|
|
196
|
+
})();
|
|
197
|
+
for (const entry of missing) {
|
|
198
|
+
const entryPromise = batchPromise.then((present) => {
|
|
199
|
+
const value = present.has(entry.key);
|
|
200
|
+
COLUMN_CACHE.set(entry.key, value);
|
|
201
|
+
return value;
|
|
202
|
+
});
|
|
203
|
+
entryPromise.catch(() => void 0);
|
|
204
|
+
COLUMN_CACHE_PENDING.set(entry.key, entryPromise);
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await batchPromise;
|
|
208
|
+
} finally {
|
|
209
|
+
for (const entry of missing) COLUMN_CACHE_PENDING.delete(entry.key);
|
|
210
|
+
}
|
|
165
211
|
}
|
|
166
212
|
async function refreshCoverageSnapshot(em, scope) {
|
|
167
213
|
const entityType = String(scope.entityType || "");
|
|
@@ -298,6 +344,7 @@ export {
|
|
|
298
344
|
applyCoverageAdjustments,
|
|
299
345
|
createCoverageAdjustments,
|
|
300
346
|
deleteCoverageForEntity,
|
|
347
|
+
primeColumnCache,
|
|
301
348
|
readCoverageSnapshot,
|
|
302
349
|
readCoverageSnapshots,
|
|
303
350
|
refreshCoverageSnapshot,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/lib/coverage.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\n\nexport type CoverageScope = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n}\n\ntype CoverageRow = {\n base_count: unknown\n indexed_count: unknown\n vector_indexed_count: unknown\n refreshed_at: Date | string | null\n}\n\nexport type CoverageSnapshot = CoverageRow & {\n baseCount: number\n indexedCount: number\n vectorIndexedCount: number\n}\n\nexport type CoverageBatchScope = {\n entityTypes: readonly string[]\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n}\n\nexport type CoverageAdjustment = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n withDeleted?: boolean\n deltaBase: number\n deltaIndex: number\n deltaVector?: number\n}\n\nexport type CoverageDeltaInput = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n withDeleted?: boolean\n baseDelta: number\n indexDelta: number\n vectorDelta?: number\n}\n\nconst COLUMN_CACHE = new Map<string, boolean>()\nconst GLOBAL_ORGANIZATION_PLACEHOLDER = '00000000-0000-0000-0000-000000000000'\nexport const COVERAGE_ORG_PLACEHOLDER = GLOBAL_ORGANIZATION_PLACEHOLDER\n\nfunction toCount(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n if (value != null && typeof (value as { valueOf: () => number }).valueOf === 'function') {\n const parsed = Number((value as { valueOf: () => number }).valueOf())\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\nfunction normalizeOrganizationForStore(orgId: string | null | undefined): string {\n return orgId ?? GLOBAL_ORGANIZATION_PLACEHOLDER\n}\n\nfunction applyOrganizationCondition<QB extends { where: (...args: any[]) => QB }>(\n qb: QB,\n column: string,\n organizationId: string | null | undefined,\n): QB {\n const stored = normalizeOrganizationForStore(organizationId ?? null)\n if (stored === GLOBAL_ORGANIZATION_PLACEHOLDER) {\n return qb.where((eb: any) => eb.or([\n eb(column as any, 'is', null),\n eb(column as any, '=', GLOBAL_ORGANIZATION_PLACEHOLDER),\n ]))\n }\n return qb.where(column as any, '=', stored)\n}\n\nasync function fetchCoverageRow(\n db: Kysely<any>,\n scope: CoverageScope\n): Promise<(CoverageRow & { organization_id: string | null }) | null> {\n const { entityType, tenantId, organizationId, withDeleted } = scope\n let query = db\n .selectFrom('entity_index_coverage' as any)\n .select([\n 'base_count' as any,\n 'indexed_count' as any,\n 'vector_indexed_count' as any,\n 'refreshed_at' as any,\n 'organization_id' as any,\n ])\n .where('entity_type' as any, '=', entityType)\n .where('with_deleted' as any, '=', withDeleted === true)\n .orderBy('refreshed_at' as any, 'desc')\n query = tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', organizationId ?? null)\n const row = await query.executeTakeFirst() as (CoverageRow & { organization_id: string | null }) | undefined\n return row ?? null\n}\n\nasync function pruneDuplicateCoverageRows(\n db: Kysely<any>,\n scope: CoverageScope,\n keepId: string | null\n): Promise<void> {\n let query = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n query = scope.tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', scope.tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', scope.organizationId ?? null)\n if (keepId) {\n query = query.where('id' as any, '!=', keepId)\n }\n await query.execute()\n}\n\nasync function upsertCoverageRow(\n db: Kysely<any>,\n scope: CoverageScope,\n counts: { baseCount: number; indexedCount: number; vectorIndexedCount: number }\n): Promise<void> {\n const storedOrgId = normalizeOrganizationForStore(scope.organizationId ?? null)\n if (scope.organizationId == null) {\n let purge = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n .where('organization_id' as any, 'is', null as any)\n purge = scope.tenantId == null\n ? purge.where('tenant_id' as any, 'is', null as any)\n : purge.where('tenant_id' as any, '=', scope.tenantId)\n await purge.execute()\n }\n\n const rows = await db\n .insertInto('entity_index_coverage' as any)\n .values({\n entity_type: scope.entityType,\n tenant_id: scope.tenantId ?? null,\n organization_id: storedOrgId,\n with_deleted: scope.withDeleted === true,\n base_count: counts.baseCount,\n indexed_count: counts.indexedCount,\n vector_indexed_count: counts.vectorIndexedCount,\n refreshed_at: sql`now()`,\n } as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'tenant_id', 'organization_id', 'with_deleted'])\n .doUpdateSet({\n base_count: counts.baseCount,\n indexed_count: counts.indexedCount,\n vector_indexed_count: counts.vectorIndexedCount,\n refreshed_at: sql`now()`,\n } as any))\n .returning(['id' as any])\n .execute() as Array<{ id: string }>\n\n const keepId = rows?.[0]?.id ?? null\n await pruneDuplicateCoverageRows(db, scope, keepId)\n}\n\nexport async function readCoverageSnapshot(\n db: Kysely<any>,\n scope: CoverageScope\n): Promise<(CoverageRow & { baseCount: number; indexedCount: number; vectorIndexedCount: number }) | null> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return null\n const row = await fetchCoverageRow(db, {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n withDeleted: scope.withDeleted === true,\n })\n if (!row) return null\n const refreshedAt = row.refreshed_at instanceof Date ? row.refreshed_at : (row.refreshed_at ? new Date(row.refreshed_at) : null)\n return {\n base_count: row.base_count,\n indexed_count: row.indexed_count,\n vector_indexed_count: row.vector_indexed_count,\n refreshed_at: refreshedAt ?? null,\n baseCount: toCount(row.base_count),\n indexedCount: toCount(row.indexed_count),\n vectorIndexedCount: toCount(row.vector_indexed_count),\n }\n}\n\nexport async function readCoverageSnapshots(\n db: Kysely<any>,\n batch: CoverageBatchScope\n): Promise<Map<string, CoverageSnapshot>> {\n const entityTypes = Array.from(\n new Set((batch.entityTypes ?? []).map((id) => String(id || '')).filter((id) => id.length > 0))\n )\n const result = new Map<string, CoverageSnapshot>()\n if (entityTypes.length === 0) return result\n\n const withDeleted = batch.withDeleted === true\n let query = db\n .selectFrom('entity_index_coverage' as any)\n .select([\n 'entity_type' as any,\n 'base_count' as any,\n 'indexed_count' as any,\n 'vector_indexed_count' as any,\n 'refreshed_at' as any,\n 'organization_id' as any,\n ])\n .where('entity_type' as any, 'in', entityTypes)\n .where('with_deleted' as any, '=', withDeleted)\n .orderBy('refreshed_at' as any, 'desc')\n query = batch.tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', batch.tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', batch.organizationId ?? null)\n\n const rows = await query.execute() as Array<CoverageRow & { entity_type: string }>\n for (const row of rows ?? []) {\n const entityType = String(row.entity_type || '')\n // Rows are ordered by refreshed_at desc, so the first row seen per entity is the latest.\n if (!entityType || result.has(entityType)) continue\n const refreshedAt = row.refreshed_at instanceof Date\n ? row.refreshed_at\n : (row.refreshed_at ? new Date(row.refreshed_at) : null)\n result.set(entityType, {\n base_count: row.base_count,\n indexed_count: row.indexed_count,\n vector_indexed_count: row.vector_indexed_count,\n refreshed_at: refreshedAt ?? null,\n baseCount: toCount(row.base_count),\n indexedCount: toCount(row.indexed_count),\n vectorIndexedCount: toCount(row.vector_indexed_count),\n })\n }\n return result\n}\n\nexport async function applyCoverageAdjustments(\n em: EntityManager,\n adjustments: CoverageAdjustment[]\n): Promise<void> {\n if (!adjustments.length) return\n const db = (em as any).getKysely() as Kysely<any>\n const aggregated = aggregateAdjustments(adjustments)\n for (const entry of aggregated) {\n const scope = entry.scope\n const existing = await fetchCoverageRow(db, scope)\n const currentBase = existing ? toCount(existing.base_count) : 0\n const currentIndex = existing ? toCount(existing.indexed_count) : 0\n const currentVector = existing ? toCount(existing.vector_indexed_count) : 0\n const nextBase = Math.max(currentBase + entry.deltaBase, 0)\n const nextIndex = Math.max(currentIndex + entry.deltaIndex, 0)\n const nextVector = Math.max(currentVector + entry.deltaVector, 0)\n\n await upsertCoverageRow(db, scope, {\n baseCount: nextBase,\n indexedCount: nextIndex,\n vectorIndexedCount: nextVector,\n })\n }\n}\n\nexport async function deleteCoverageForEntity(db: Kysely<any>, entityType: string): Promise<void> {\n if (!entityType) return\n await db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', entityType)\n .execute()\n}\n\nasync function tableHasColumn(db: Kysely<any>, table: string, column: string): Promise<boolean> {\n const key = `${table}.${column}`\n if (COLUMN_CACHE.has(key)) return COLUMN_CACHE.get(key)!\n const exists = await db\n .selectFrom('information_schema.columns' as any)\n .select(sql<number>`1`.as('present'))\n .where(sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', table)\n .where('column_name' as any, '=', column)\n .executeTakeFirst()\n const present = !!exists\n COLUMN_CACHE.set(key, present)\n return present\n}\n\nexport async function refreshCoverageSnapshot(\n em: EntityManager,\n scope: CoverageScope,\n): Promise<void> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n const withDeleted = scope.withDeleted === true\n\n const db = (em as any).getKysely() as Kysely<any>\n const baseTable = resolveEntityTableName(em, entityType)\n\n const hasOrg = await tableHasColumn(db, baseTable, 'organization_id')\n const hasTenant = await tableHasColumn(db, baseTable, 'tenant_id')\n const hasDeleted = await tableHasColumn(db, baseTable, 'deleted_at')\n\n if (organizationId !== null && !hasOrg) return\n if (tenantId !== null && !hasTenant) return\n\n let baseQuery = db\n .selectFrom(`${baseTable} as b` as any)\n .select(sql`count(*)`.as('count'))\n if (organizationId !== null && hasOrg) baseQuery = baseQuery.where('b.organization_id' as any, '=', organizationId)\n if (tenantId !== null && hasTenant) baseQuery = baseQuery.where('b.tenant_id' as any, '=', tenantId)\n if (!withDeleted && hasDeleted) baseQuery = baseQuery.where('b.deleted_at' as any, 'is', null as any)\n\n let indexQuery = db\n .selectFrom('entity_indexes as ei' as any)\n .select(sql`count(*)`.as('count'))\n .where('ei.entity_type' as any, '=', entityType)\n if (organizationId !== null) indexQuery = indexQuery.where('ei.organization_id' as any, '=', organizationId)\n if (tenantId !== null) indexQuery = indexQuery.where('ei.tenant_id' as any, '=', tenantId)\n if (!withDeleted) indexQuery = indexQuery.where('ei.deleted_at' as any, 'is', null as any)\n\n const vectorCountPromise = (async (): Promise<number | undefined> => {\n const hasVectorTable = await tableHasColumn(db, 'vector_search', 'entity_id')\n if (!hasVectorTable || typeof tenantId !== 'string' || tenantId.length === 0) return undefined\n\n try {\n let vectorQuery = db\n .selectFrom('vector_search' as any)\n .select(sql`count(*)`.as('count'))\n .where('entity_id' as any, '=', entityType)\n .where('tenant_id' as any, '=', tenantId)\n if (organizationId !== null) {\n vectorQuery = vectorQuery.where('organization_id' as any, '=', organizationId)\n }\n const vectorRow = await vectorQuery.executeTakeFirst() as { count: unknown } | undefined\n return toCount(vectorRow?.count)\n } catch (err) {\n console.warn('[query_index] Failed to resolve vector count for coverage snapshot', {\n entityType,\n tenantId,\n organizationId,\n error: err instanceof Error ? err.message : err,\n })\n return undefined\n }\n })()\n\n const [baseRow, indexRow, vectorCount] = await Promise.all([\n baseQuery.executeTakeFirst() as Promise<{ count: unknown } | undefined>,\n indexQuery.executeTakeFirst() as Promise<{ count: unknown } | undefined>,\n vectorCountPromise,\n ])\n\n const baseCount = toCount(baseRow?.count)\n const indexCount = toCount(indexRow?.count)\n\n await writeCoverageCounts(em, { entityType, tenantId, organizationId, withDeleted }, {\n baseCount,\n indexedCount: indexCount,\n vectorCount,\n })\n}\n\nexport async function writeCoverageCounts(\n em: EntityManager,\n scope: CoverageScope,\n counts: { baseCount?: number; indexedCount?: number; vectorCount?: number }\n): Promise<void> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return\n const db = (em as any).getKysely() as Kysely<any>\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n const withDeleted = scope.withDeleted === true\n const existing = await fetchCoverageRow(db, {\n entityType,\n tenantId,\n organizationId,\n withDeleted,\n })\n const baseCount = counts.baseCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.baseCount)))\n : Math.max(0, Math.trunc(toCount(existing?.base_count)))\n const indexCount = counts.indexedCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.indexedCount)))\n : Math.max(0, Math.trunc(toCount(existing?.indexed_count)))\n const vectorCount = counts.vectorCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.vectorCount)))\n : Math.max(0, Math.trunc(toCount(existing?.vector_indexed_count)))\n await upsertCoverageRow(db, { entityType, tenantId, organizationId, withDeleted }, {\n baseCount,\n indexedCount: indexCount,\n vectorIndexedCount: vectorCount,\n })\n}\n\ntype AggregatedAdjustment = {\n scope: CoverageScope\n deltaBase: number\n deltaIndex: number\n deltaVector: number\n}\n\nfunction aggregateAdjustments(adjustments: CoverageAdjustment[]): AggregatedAdjustment[] {\n const map = new Map<string, AggregatedAdjustment>()\n for (const adj of adjustments) {\n if (!adj?.entityType) continue\n const deltaBase = Number.isFinite(adj.deltaBase) ? adj.deltaBase : 0\n const deltaIndex = Number.isFinite(adj.deltaIndex) ? adj.deltaIndex : 0\n const deltaVector = Number.isFinite(adj.deltaVector) ? adj.deltaVector! : 0\n if (deltaBase === 0 && deltaIndex === 0 && deltaVector === 0) continue\n const scope: CoverageScope = {\n entityType: adj.entityType,\n tenantId: adj.tenantId ?? null,\n organizationId: adj.organizationId ?? null,\n withDeleted: adj.withDeleted === true,\n }\n const key = scopeKey(scope)\n const existing = map.get(key)\n if (existing) {\n existing.deltaBase += deltaBase\n existing.deltaIndex += deltaIndex\n existing.deltaVector += deltaVector\n } else {\n map.set(key, { scope, deltaBase, deltaIndex, deltaVector })\n }\n }\n return Array.from(map.values())\n}\n\nfunction scopeKey(scope: CoverageScope): string {\n const tenant = scope.tenantId ?? '__tenant_null__'\n const org = normalizeOrganizationForStore(scope.organizationId ?? null)\n const deleted = scope.withDeleted === true ? '1' : '0'\n return `${scope.entityType}|${tenant}|${org}|${deleted}`\n}\n\nexport function createCoverageAdjustments(input: CoverageDeltaInput): CoverageAdjustment[] {\n const entityType = String(input.entityType || '')\n if (!entityType) return []\n const baseDelta = Number.isFinite(input.baseDelta) ? input.baseDelta : 0\n const indexDelta = Number.isFinite(input.indexDelta) ? input.indexDelta : 0\n const vectorDelta = Number.isFinite(input.vectorDelta) ? input.vectorDelta! : 0\n if (baseDelta === 0 && indexDelta === 0 && vectorDelta === 0) return []\n const withDeleted = input.withDeleted === true\n const tenantId = input.tenantId ?? null\n const organizationId = input.organizationId ?? null\n return [\n {\n entityType,\n tenantId,\n organizationId,\n withDeleted,\n deltaBase: baseDelta,\n deltaIndex: indexDelta,\n deltaVector: vectorDelta,\n },\n ]\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\n\nexport type CoverageScope = {\n entityType: string\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n}\n\ntype CoverageRow = {\n base_count: unknown\n indexed_count: unknown\n vector_indexed_count: unknown\n refreshed_at: Date | string | null\n}\n\nexport type CoverageSnapshot = CoverageRow & {\n baseCount: number\n indexedCount: number\n vectorIndexedCount: number\n}\n\nexport type CoverageBatchScope = {\n entityTypes: readonly string[]\n tenantId?: string | null\n organizationId?: string | null\n withDeleted?: boolean\n}\n\nexport type CoverageAdjustment = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n withDeleted?: boolean\n deltaBase: number\n deltaIndex: number\n deltaVector?: number\n}\n\nexport type CoverageDeltaInput = {\n entityType: string\n tenantId: string | null\n organizationId: string | null\n withDeleted?: boolean\n baseDelta: number\n indexDelta: number\n vectorDelta?: number\n}\n\nconst COLUMN_CACHE = new Map<string, boolean>()\n// In-flight de-dup: without this, N concurrent `tableHasColumn` callers for the same\n// (table, column) \u2014 e.g. every entity type's `refreshCoverageSnapshot` asking about\n// `vector_search.entity_id` \u2014 would each see a cold cache and fire their own identical\n// `information_schema.columns` query, since the cache is only populated after a query\n// resolves. Tracking the in-flight promise lets late arrivals await the first one instead.\nconst COLUMN_CACHE_PENDING = new Map<string, Promise<boolean>>()\nconst GLOBAL_ORGANIZATION_PLACEHOLDER = '00000000-0000-0000-0000-000000000000'\nexport const COVERAGE_ORG_PLACEHOLDER = GLOBAL_ORGANIZATION_PLACEHOLDER\n\nfunction toCount(value: unknown): number {\n if (typeof value === 'number') return Number.isFinite(value) ? value : 0\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isFinite(parsed) ? parsed : 0\n }\n if (value != null && typeof (value as { valueOf: () => number }).valueOf === 'function') {\n const parsed = Number((value as { valueOf: () => number }).valueOf())\n return Number.isFinite(parsed) ? parsed : 0\n }\n return 0\n}\n\nfunction normalizeOrganizationForStore(orgId: string | null | undefined): string {\n return orgId ?? GLOBAL_ORGANIZATION_PLACEHOLDER\n}\n\nfunction applyOrganizationCondition<QB extends { where: (...args: any[]) => QB }>(\n qb: QB,\n column: string,\n organizationId: string | null | undefined,\n): QB {\n const stored = normalizeOrganizationForStore(organizationId ?? null)\n if (stored === GLOBAL_ORGANIZATION_PLACEHOLDER) {\n return qb.where((eb: any) => eb.or([\n eb(column as any, 'is', null),\n eb(column as any, '=', GLOBAL_ORGANIZATION_PLACEHOLDER),\n ]))\n }\n return qb.where(column as any, '=', stored)\n}\n\nasync function fetchCoverageRow(\n db: Kysely<any>,\n scope: CoverageScope\n): Promise<(CoverageRow & { organization_id: string | null }) | null> {\n const { entityType, tenantId, organizationId, withDeleted } = scope\n let query = db\n .selectFrom('entity_index_coverage' as any)\n .select([\n 'base_count' as any,\n 'indexed_count' as any,\n 'vector_indexed_count' as any,\n 'refreshed_at' as any,\n 'organization_id' as any,\n ])\n .where('entity_type' as any, '=', entityType)\n .where('with_deleted' as any, '=', withDeleted === true)\n .orderBy('refreshed_at' as any, 'desc')\n query = tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', organizationId ?? null)\n const row = await query.executeTakeFirst() as (CoverageRow & { organization_id: string | null }) | undefined\n return row ?? null\n}\n\nasync function pruneDuplicateCoverageRows(\n db: Kysely<any>,\n scope: CoverageScope,\n keepId: string | null\n): Promise<void> {\n let query = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n query = scope.tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', scope.tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', scope.organizationId ?? null)\n if (keepId) {\n query = query.where('id' as any, '!=', keepId)\n }\n await query.execute()\n}\n\nasync function upsertCoverageRow(\n db: Kysely<any>,\n scope: CoverageScope,\n counts: { baseCount: number; indexedCount: number; vectorIndexedCount: number }\n): Promise<void> {\n const storedOrgId = normalizeOrganizationForStore(scope.organizationId ?? null)\n if (scope.organizationId == null) {\n let purge = db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', scope.entityType)\n .where('with_deleted' as any, '=', scope.withDeleted === true)\n .where('organization_id' as any, 'is', null as any)\n purge = scope.tenantId == null\n ? purge.where('tenant_id' as any, 'is', null as any)\n : purge.where('tenant_id' as any, '=', scope.tenantId)\n await purge.execute()\n }\n\n const rows = await db\n .insertInto('entity_index_coverage' as any)\n .values({\n entity_type: scope.entityType,\n tenant_id: scope.tenantId ?? null,\n organization_id: storedOrgId,\n with_deleted: scope.withDeleted === true,\n base_count: counts.baseCount,\n indexed_count: counts.indexedCount,\n vector_indexed_count: counts.vectorIndexedCount,\n refreshed_at: sql`now()`,\n } as any)\n .onConflict((oc: any) => oc\n .columns(['entity_type', 'tenant_id', 'organization_id', 'with_deleted'])\n .doUpdateSet({\n base_count: counts.baseCount,\n indexed_count: counts.indexedCount,\n vector_indexed_count: counts.vectorIndexedCount,\n refreshed_at: sql`now()`,\n } as any))\n .returning(['id' as any])\n .execute() as Array<{ id: string }>\n\n const keepId = rows?.[0]?.id ?? null\n await pruneDuplicateCoverageRows(db, scope, keepId)\n}\n\nexport async function readCoverageSnapshot(\n db: Kysely<any>,\n scope: CoverageScope\n): Promise<(CoverageRow & { baseCount: number; indexedCount: number; vectorIndexedCount: number }) | null> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return null\n const row = await fetchCoverageRow(db, {\n entityType,\n tenantId: scope.tenantId ?? null,\n organizationId: scope.organizationId ?? null,\n withDeleted: scope.withDeleted === true,\n })\n if (!row) return null\n const refreshedAt = row.refreshed_at instanceof Date ? row.refreshed_at : (row.refreshed_at ? new Date(row.refreshed_at) : null)\n return {\n base_count: row.base_count,\n indexed_count: row.indexed_count,\n vector_indexed_count: row.vector_indexed_count,\n refreshed_at: refreshedAt ?? null,\n baseCount: toCount(row.base_count),\n indexedCount: toCount(row.indexed_count),\n vectorIndexedCount: toCount(row.vector_indexed_count),\n }\n}\n\nexport async function readCoverageSnapshots(\n db: Kysely<any>,\n batch: CoverageBatchScope\n): Promise<Map<string, CoverageSnapshot>> {\n const entityTypes = Array.from(\n new Set((batch.entityTypes ?? []).map((id) => String(id || '')).filter((id) => id.length > 0))\n )\n const result = new Map<string, CoverageSnapshot>()\n if (entityTypes.length === 0) return result\n\n const withDeleted = batch.withDeleted === true\n let query = db\n .selectFrom('entity_index_coverage' as any)\n .select([\n 'entity_type' as any,\n 'base_count' as any,\n 'indexed_count' as any,\n 'vector_indexed_count' as any,\n 'refreshed_at' as any,\n 'organization_id' as any,\n ])\n .where('entity_type' as any, 'in', entityTypes)\n .where('with_deleted' as any, '=', withDeleted)\n .orderBy('refreshed_at' as any, 'desc')\n query = batch.tenantId == null\n ? query.where('tenant_id' as any, 'is', null as any)\n : query.where('tenant_id' as any, '=', batch.tenantId)\n query = applyOrganizationCondition(query as any, 'organization_id', batch.organizationId ?? null)\n\n const rows = await query.execute() as Array<CoverageRow & { entity_type: string }>\n for (const row of rows ?? []) {\n const entityType = String(row.entity_type || '')\n // Rows are ordered by refreshed_at desc, so the first row seen per entity is the latest.\n if (!entityType || result.has(entityType)) continue\n const refreshedAt = row.refreshed_at instanceof Date\n ? row.refreshed_at\n : (row.refreshed_at ? new Date(row.refreshed_at) : null)\n result.set(entityType, {\n base_count: row.base_count,\n indexed_count: row.indexed_count,\n vector_indexed_count: row.vector_indexed_count,\n refreshed_at: refreshedAt ?? null,\n baseCount: toCount(row.base_count),\n indexedCount: toCount(row.indexed_count),\n vectorIndexedCount: toCount(row.vector_indexed_count),\n })\n }\n return result\n}\n\nexport async function applyCoverageAdjustments(\n em: EntityManager,\n adjustments: CoverageAdjustment[]\n): Promise<void> {\n if (!adjustments.length) return\n const db = (em as any).getKysely() as Kysely<any>\n const aggregated = aggregateAdjustments(adjustments)\n for (const entry of aggregated) {\n const scope = entry.scope\n const existing = await fetchCoverageRow(db, scope)\n const currentBase = existing ? toCount(existing.base_count) : 0\n const currentIndex = existing ? toCount(existing.indexed_count) : 0\n const currentVector = existing ? toCount(existing.vector_indexed_count) : 0\n const nextBase = Math.max(currentBase + entry.deltaBase, 0)\n const nextIndex = Math.max(currentIndex + entry.deltaIndex, 0)\n const nextVector = Math.max(currentVector + entry.deltaVector, 0)\n\n await upsertCoverageRow(db, scope, {\n baseCount: nextBase,\n indexedCount: nextIndex,\n vectorIndexedCount: nextVector,\n })\n }\n}\n\nexport async function deleteCoverageForEntity(db: Kysely<any>, entityType: string): Promise<void> {\n if (!entityType) return\n await db\n .deleteFrom('entity_index_coverage' as any)\n .where('entity_type' as any, '=', entityType)\n .execute()\n}\n\nasync function tableHasColumn(db: Kysely<any>, table: string, column: string): Promise<boolean> {\n const key = `${table}.${column}`\n if (COLUMN_CACHE.has(key)) return COLUMN_CACHE.get(key)!\n const pending = COLUMN_CACHE_PENDING.get(key)\n if (pending) return pending\n const promise = (async () => {\n const exists = await db\n .selectFrom('information_schema.columns' as any)\n .select(sql<number>`1`.as('present'))\n .where(sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, '=', table)\n .where('column_name' as any, '=', column)\n .executeTakeFirst()\n const present = !!exists\n COLUMN_CACHE.set(key, present)\n return present\n })()\n COLUMN_CACHE_PENDING.set(key, promise)\n try {\n return await promise\n } finally {\n COLUMN_CACHE_PENDING.delete(key)\n }\n}\n\nexport type ColumnCheck = { table: string; column: string }\n\n// Batches the `information_schema.columns` introspection used by `refreshCoverageSnapshot`\n// into a single query for a whole set of (table, column) pairs, and pre-populates\n// `COLUMN_CACHE_PENDING` for every pair before that query even runs. Callers of\n// `coverage_warmup.ts` use this so its many concurrently-dispatched `coverage.refresh`\n// subscribers hit an already-primed (or in-flight) cache instead of each doing their own\n// per-table introspection round trip.\nexport async function primeColumnCache(db: Kysely<any>, checks: ColumnCheck[]): Promise<void> {\n const missing: Array<{ table: string; column: string; key: string }> = []\n const seen = new Set<string>()\n for (const check of checks) {\n const table = String(check?.table || '')\n const column = String(check?.column || '')\n if (!table || !column) continue\n const key = `${table}.${column}`\n if (seen.has(key)) continue\n seen.add(key)\n if (COLUMN_CACHE.has(key) || COLUMN_CACHE_PENDING.has(key)) continue\n missing.push({ table, column, key })\n }\n if (!missing.length) return\n\n const tables = Array.from(new Set(missing.map((entry) => entry.table)))\n const columns = Array.from(new Set(missing.map((entry) => entry.column)))\n\n const batchPromise = (async (): Promise<Set<string>> => {\n const rows = await db\n .selectFrom('information_schema.columns' as any)\n .select(['table_name' as any, 'column_name' as any])\n .where(sql<boolean>`table_schema = current_schema()`)\n .where('table_name' as any, 'in', tables)\n .where('column_name' as any, 'in', columns)\n .execute() as Array<{ table_name: string; column_name: string }>\n return new Set(rows.map((row) => `${row.table_name}.${row.column_name}`))\n })()\n\n for (const entry of missing) {\n const entryPromise = batchPromise.then((present) => {\n const value = present.has(entry.key)\n COLUMN_CACHE.set(entry.key, value)\n return value\n })\n // Mark the stored promise as handled: when the batch query fails and no\n // `tableHasColumn` caller has adopted this entry yet (the common case \u2014 the warmup\n // awaits priming before dispatching any refresh), an orphaned rejection would\n // otherwise crash a plain-Node event worker via unhandledRejection. Awaiting\n // callers still observe the rejection through the stored reference.\n entryPromise.catch(() => undefined)\n COLUMN_CACHE_PENDING.set(entry.key, entryPromise)\n }\n\n try {\n await batchPromise\n } finally {\n for (const entry of missing) COLUMN_CACHE_PENDING.delete(entry.key)\n }\n}\n\nexport async function refreshCoverageSnapshot(\n em: EntityManager,\n scope: CoverageScope,\n): Promise<void> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n const withDeleted = scope.withDeleted === true\n\n const db = (em as any).getKysely() as Kysely<any>\n const baseTable = resolveEntityTableName(em, entityType)\n\n const hasOrg = await tableHasColumn(db, baseTable, 'organization_id')\n const hasTenant = await tableHasColumn(db, baseTable, 'tenant_id')\n const hasDeleted = await tableHasColumn(db, baseTable, 'deleted_at')\n\n if (organizationId !== null && !hasOrg) return\n if (tenantId !== null && !hasTenant) return\n\n let baseQuery = db\n .selectFrom(`${baseTable} as b` as any)\n .select(sql`count(*)`.as('count'))\n if (organizationId !== null && hasOrg) baseQuery = baseQuery.where('b.organization_id' as any, '=', organizationId)\n if (tenantId !== null && hasTenant) baseQuery = baseQuery.where('b.tenant_id' as any, '=', tenantId)\n if (!withDeleted && hasDeleted) baseQuery = baseQuery.where('b.deleted_at' as any, 'is', null as any)\n\n let indexQuery = db\n .selectFrom('entity_indexes as ei' as any)\n .select(sql`count(*)`.as('count'))\n .where('ei.entity_type' as any, '=', entityType)\n if (organizationId !== null) indexQuery = indexQuery.where('ei.organization_id' as any, '=', organizationId)\n if (tenantId !== null) indexQuery = indexQuery.where('ei.tenant_id' as any, '=', tenantId)\n if (!withDeleted) indexQuery = indexQuery.where('ei.deleted_at' as any, 'is', null as any)\n\n const vectorCountPromise = (async (): Promise<number | undefined> => {\n const hasVectorTable = await tableHasColumn(db, 'vector_search', 'entity_id')\n if (!hasVectorTable || typeof tenantId !== 'string' || tenantId.length === 0) return undefined\n\n try {\n let vectorQuery = db\n .selectFrom('vector_search' as any)\n .select(sql`count(*)`.as('count'))\n .where('entity_id' as any, '=', entityType)\n .where('tenant_id' as any, '=', tenantId)\n if (organizationId !== null) {\n vectorQuery = vectorQuery.where('organization_id' as any, '=', organizationId)\n }\n const vectorRow = await vectorQuery.executeTakeFirst() as { count: unknown } | undefined\n return toCount(vectorRow?.count)\n } catch (err) {\n console.warn('[query_index] Failed to resolve vector count for coverage snapshot', {\n entityType,\n tenantId,\n organizationId,\n error: err instanceof Error ? err.message : err,\n })\n return undefined\n }\n })()\n\n const [baseRow, indexRow, vectorCount] = await Promise.all([\n baseQuery.executeTakeFirst() as Promise<{ count: unknown } | undefined>,\n indexQuery.executeTakeFirst() as Promise<{ count: unknown } | undefined>,\n vectorCountPromise,\n ])\n\n const baseCount = toCount(baseRow?.count)\n const indexCount = toCount(indexRow?.count)\n\n await writeCoverageCounts(em, { entityType, tenantId, organizationId, withDeleted }, {\n baseCount,\n indexedCount: indexCount,\n vectorCount,\n })\n}\n\nexport async function writeCoverageCounts(\n em: EntityManager,\n scope: CoverageScope,\n counts: { baseCount?: number; indexedCount?: number; vectorCount?: number }\n): Promise<void> {\n const entityType = String(scope.entityType || '')\n if (!entityType) return\n const db = (em as any).getKysely() as Kysely<any>\n const tenantId = scope.tenantId ?? null\n const organizationId = scope.organizationId ?? null\n const withDeleted = scope.withDeleted === true\n const existing = await fetchCoverageRow(db, {\n entityType,\n tenantId,\n organizationId,\n withDeleted,\n })\n const baseCount = counts.baseCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.baseCount)))\n : Math.max(0, Math.trunc(toCount(existing?.base_count)))\n const indexCount = counts.indexedCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.indexedCount)))\n : Math.max(0, Math.trunc(toCount(existing?.indexed_count)))\n const vectorCount = counts.vectorCount !== undefined\n ? Math.max(0, Math.trunc(toCount(counts.vectorCount)))\n : Math.max(0, Math.trunc(toCount(existing?.vector_indexed_count)))\n await upsertCoverageRow(db, { entityType, tenantId, organizationId, withDeleted }, {\n baseCount,\n indexedCount: indexCount,\n vectorIndexedCount: vectorCount,\n })\n}\n\ntype AggregatedAdjustment = {\n scope: CoverageScope\n deltaBase: number\n deltaIndex: number\n deltaVector: number\n}\n\nfunction aggregateAdjustments(adjustments: CoverageAdjustment[]): AggregatedAdjustment[] {\n const map = new Map<string, AggregatedAdjustment>()\n for (const adj of adjustments) {\n if (!adj?.entityType) continue\n const deltaBase = Number.isFinite(adj.deltaBase) ? adj.deltaBase : 0\n const deltaIndex = Number.isFinite(adj.deltaIndex) ? adj.deltaIndex : 0\n const deltaVector = Number.isFinite(adj.deltaVector) ? adj.deltaVector! : 0\n if (deltaBase === 0 && deltaIndex === 0 && deltaVector === 0) continue\n const scope: CoverageScope = {\n entityType: adj.entityType,\n tenantId: adj.tenantId ?? null,\n organizationId: adj.organizationId ?? null,\n withDeleted: adj.withDeleted === true,\n }\n const key = scopeKey(scope)\n const existing = map.get(key)\n if (existing) {\n existing.deltaBase += deltaBase\n existing.deltaIndex += deltaIndex\n existing.deltaVector += deltaVector\n } else {\n map.set(key, { scope, deltaBase, deltaIndex, deltaVector })\n }\n }\n return Array.from(map.values())\n}\n\nfunction scopeKey(scope: CoverageScope): string {\n const tenant = scope.tenantId ?? '__tenant_null__'\n const org = normalizeOrganizationForStore(scope.organizationId ?? null)\n const deleted = scope.withDeleted === true ? '1' : '0'\n return `${scope.entityType}|${tenant}|${org}|${deleted}`\n}\n\nexport function createCoverageAdjustments(input: CoverageDeltaInput): CoverageAdjustment[] {\n const entityType = String(input.entityType || '')\n if (!entityType) return []\n const baseDelta = Number.isFinite(input.baseDelta) ? input.baseDelta : 0\n const indexDelta = Number.isFinite(input.indexDelta) ? input.indexDelta : 0\n const vectorDelta = Number.isFinite(input.vectorDelta) ? input.vectorDelta! : 0\n if (baseDelta === 0 && indexDelta === 0 && vectorDelta === 0) return []\n const withDeleted = input.withDeleted === true\n const tenantId = input.tenantId ?? null\n const organizationId = input.organizationId ?? null\n return [\n {\n entityType,\n tenantId,\n organizationId,\n withDeleted,\n deltaBase: baseDelta,\n deltaIndex: indexDelta,\n deltaVector: vectorDelta,\n },\n ]\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,16 +1,62 @@
|
|
|
1
1
|
import { getEntityIds } from "@open-mercato/shared/lib/encryption/entityIds";
|
|
2
2
|
import { flattenSystemEntityIds } from "@open-mercato/shared/lib/entities/system-entities";
|
|
3
|
+
import { parseBooleanWithDefault } from "@open-mercato/shared/lib/boolean";
|
|
4
|
+
import { resolveEntityTableName } from "@open-mercato/shared/lib/query/engine";
|
|
5
|
+
import { readCoverageSnapshots, primeColumnCache } from "../lib/coverage.js";
|
|
3
6
|
const metadata = { event: "query_index.coverage.warmup", persistent: false };
|
|
4
|
-
const
|
|
5
|
-
const
|
|
7
|
+
const DEFAULT_WARMUP_THROTTLE_MS = 5 * 60 * 1e3;
|
|
8
|
+
const DEFAULT_WARMUP_REFRESH_CONCURRENCY = 10;
|
|
9
|
+
const DEFAULT_WARMUP_STAGGER_MS = 0;
|
|
6
10
|
const lastWarmupAt = /* @__PURE__ */ new Map();
|
|
7
11
|
function scopeKey(entityType, tenantId) {
|
|
8
12
|
return `${entityType}|${tenantId ?? "__null__"}`;
|
|
9
13
|
}
|
|
14
|
+
function isWarmupEnabled() {
|
|
15
|
+
return parseBooleanWithDefault(process.env.QUERY_INDEX_WARMUP_ENABLED, true);
|
|
16
|
+
}
|
|
17
|
+
function resolveWarmupThrottleMs() {
|
|
18
|
+
const raw = Number.parseInt(process.env.QUERY_INDEX_WARMUP_THROTTLE_MS ?? "", 10);
|
|
19
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_WARMUP_THROTTLE_MS;
|
|
20
|
+
}
|
|
21
|
+
function resolveWarmupConcurrency() {
|
|
22
|
+
const raw = Number.parseInt(process.env.QUERY_INDEX_WARMUP_CONCURRENCY ?? "", 10);
|
|
23
|
+
return Number.isFinite(raw) && raw >= 1 ? raw : DEFAULT_WARMUP_REFRESH_CONCURRENCY;
|
|
24
|
+
}
|
|
25
|
+
function resolveWarmupStaggerMs() {
|
|
26
|
+
const raw = Number.parseInt(process.env.QUERY_INDEX_WARMUP_STAGGER_MS ?? "", 10);
|
|
27
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_WARMUP_STAGGER_MS;
|
|
28
|
+
}
|
|
10
29
|
function getEntityIdList() {
|
|
11
30
|
return flattenSystemEntityIds(getEntityIds());
|
|
12
31
|
}
|
|
32
|
+
async function filterStaleEntityTypes(db, candidates, tenantId, throttleMs, now) {
|
|
33
|
+
if (!db) {
|
|
34
|
+
for (const entityType of candidates) lastWarmupAt.set(scopeKey(entityType, tenantId), now);
|
|
35
|
+
return candidates;
|
|
36
|
+
}
|
|
37
|
+
const snapshots = await readCoverageSnapshots(db, {
|
|
38
|
+
entityTypes: candidates,
|
|
39
|
+
tenantId,
|
|
40
|
+
organizationId: null,
|
|
41
|
+
withDeleted: false
|
|
42
|
+
}).catch(() => /* @__PURE__ */ new Map());
|
|
43
|
+
const stale = [];
|
|
44
|
+
for (const entityType of candidates) {
|
|
45
|
+
const snapshot = snapshots.get(entityType);
|
|
46
|
+
const refreshedAt = snapshot?.refreshed_at instanceof Date ? snapshot.refreshed_at.getTime() : null;
|
|
47
|
+
if (refreshedAt !== null && now - refreshedAt < throttleMs) {
|
|
48
|
+
lastWarmupAt.set(scopeKey(entityType, tenantId), refreshedAt);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
lastWarmupAt.set(scopeKey(entityType, tenantId), now);
|
|
52
|
+
stale.push(entityType);
|
|
53
|
+
}
|
|
54
|
+
return stale;
|
|
55
|
+
}
|
|
13
56
|
async function handle(payload, ctx) {
|
|
57
|
+
if (!isWarmupEnabled()) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
14
60
|
const entityIds = getEntityIdList();
|
|
15
61
|
if (!entityIds.length) {
|
|
16
62
|
return;
|
|
@@ -25,30 +71,66 @@ async function handle(payload, ctx) {
|
|
|
25
71
|
if (!eventBus) {
|
|
26
72
|
return;
|
|
27
73
|
}
|
|
74
|
+
let em = null;
|
|
75
|
+
let db = null;
|
|
76
|
+
try {
|
|
77
|
+
em = ctx.resolve("em");
|
|
78
|
+
db = typeof em?.getKysely === "function" ? em.getKysely() : null;
|
|
79
|
+
} catch {
|
|
80
|
+
em = null;
|
|
81
|
+
db = null;
|
|
82
|
+
}
|
|
83
|
+
const throttleMs = resolveWarmupThrottleMs();
|
|
28
84
|
const now = Date.now();
|
|
29
|
-
const
|
|
85
|
+
const candidates = [];
|
|
30
86
|
for (const entityType of entityIds) {
|
|
31
87
|
const key = scopeKey(entityType, tenantId);
|
|
32
88
|
const last = lastWarmupAt.get(key) ?? 0;
|
|
33
|
-
if (now - last <
|
|
89
|
+
if (now - last < throttleMs) {
|
|
34
90
|
continue;
|
|
35
91
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
92
|
+
candidates.push(entityType);
|
|
93
|
+
}
|
|
94
|
+
if (!candidates.length) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const staleEntityTypes = await filterStaleEntityTypes(db, candidates, tenantId, throttleMs, now);
|
|
98
|
+
if (!staleEntityTypes.length) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (db && em) {
|
|
102
|
+
const checks = [{ table: "vector_search", column: "entity_id" }];
|
|
103
|
+
for (const entityType of staleEntityTypes) {
|
|
104
|
+
const table = resolveEntityTableName(em, entityType);
|
|
105
|
+
checks.push(
|
|
106
|
+
{ table, column: "organization_id" },
|
|
107
|
+
{ table, column: "tenant_id" },
|
|
108
|
+
{ table, column: "deleted_at" }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
await primeColumnCache(db, checks).catch(() => void 0);
|
|
112
|
+
}
|
|
113
|
+
const staggerMs = resolveWarmupStaggerMs();
|
|
114
|
+
const concurrency = resolveWarmupConcurrency();
|
|
115
|
+
const chunkPromises = [];
|
|
116
|
+
for (let i = 0; i < staleEntityTypes.length; i += concurrency) {
|
|
117
|
+
const chunkIndex = i / concurrency;
|
|
118
|
+
const chunk = staleEntityTypes.slice(i, i + concurrency);
|
|
119
|
+
const delayMs = chunkIndex * staggerMs;
|
|
120
|
+
chunkPromises.push(
|
|
121
|
+
Promise.allSettled(
|
|
122
|
+
chunk.map(
|
|
123
|
+
(entityType) => eventBus.emit("query_index.coverage.refresh", {
|
|
124
|
+
entityType,
|
|
125
|
+
tenantId,
|
|
126
|
+
organizationId: null,
|
|
127
|
+
delayMs
|
|
128
|
+
}).catch(() => void 0)
|
|
129
|
+
)
|
|
49
130
|
)
|
|
50
131
|
);
|
|
51
132
|
}
|
|
133
|
+
await Promise.allSettled(chunkPromises);
|
|
52
134
|
}
|
|
53
135
|
export {
|
|
54
136
|
handle as default,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/subscribers/coverage_warmup.ts"],
|
|
4
|
-
"sourcesContent": ["import { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport type { EventBus } from '@open-mercato/events/types'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\n\nexport const metadata = { event: 'query_index.coverage.warmup', persistent: false }\n\ntype Payload = {\n tenantId?: string | null\n}\n\nconst
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { Kysely } from 'kysely'\nimport { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'\nimport type { EventBus } from '@open-mercato/events/types'\nimport { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'\nimport { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'\nimport { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { readCoverageSnapshots, primeColumnCache, type ColumnCheck } from '../lib/coverage'\n\nexport const metadata = { event: 'query_index.coverage.warmup', persistent: false }\n\ntype Payload = {\n tenantId?: string | null\n}\n\nconst DEFAULT_WARMUP_THROTTLE_MS = 5 * 60 * 1000\nconst DEFAULT_WARMUP_REFRESH_CONCURRENCY = 10\nconst DEFAULT_WARMUP_STAGGER_MS = 0\nconst lastWarmupAt = new Map<string, number>()\n\nfunction scopeKey(entityType: string, tenantId: string | null): string {\n return `${entityType}|${tenantId ?? '__null__'}`\n}\n\nfunction isWarmupEnabled(): boolean {\n return parseBooleanWithDefault(process.env.QUERY_INDEX_WARMUP_ENABLED, true)\n}\n\nfunction resolveWarmupThrottleMs(): number {\n const raw = Number.parseInt(process.env.QUERY_INDEX_WARMUP_THROTTLE_MS ?? '', 10)\n return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_WARMUP_THROTTLE_MS\n}\n\nfunction resolveWarmupConcurrency(): number {\n const raw = Number.parseInt(process.env.QUERY_INDEX_WARMUP_CONCURRENCY ?? '', 10)\n return Number.isFinite(raw) && raw >= 1 ? raw : DEFAULT_WARMUP_REFRESH_CONCURRENCY\n}\n\nfunction resolveWarmupStaggerMs(): number {\n const raw = Number.parseInt(process.env.QUERY_INDEX_WARMUP_STAGGER_MS ?? '', 10)\n return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_WARMUP_STAGGER_MS\n}\n\nfunction getEntityIdList(): string[] {\n return flattenSystemEntityIds(getEntityIds() as Record<string, Record<string, string>>)\n}\n\n// Durable staleness check against `entity_index_coverage.refreshed_at`. The in-memory\n// `lastWarmupAt` Map only throttles within one process, so a fresh restart forgets\n// every scope and re-runs the full sweep even for entity types whose persisted snapshot\n// was refreshed moments earlier (by a prior warmup, a lazy query-engine refresh, or an\n// incremental CRUD delta via `applyCoverageAdjustments`, which also bumps `refreshed_at`).\n// This check is batched (one query for all candidates) and replaces process memory with\n// the same source of truth the query engine itself reads.\nasync function filterStaleEntityTypes(\n db: Kysely<any> | null,\n candidates: string[],\n tenantId: string | null,\n throttleMs: number,\n now: number,\n): Promise<string[]> {\n if (!db) {\n for (const entityType of candidates) lastWarmupAt.set(scopeKey(entityType, tenantId), now)\n return candidates\n }\n\n const snapshots = await readCoverageSnapshots(db, {\n entityTypes: candidates,\n tenantId,\n organizationId: null,\n withDeleted: false,\n }).catch(() => new Map())\n\n const stale: string[] = []\n for (const entityType of candidates) {\n const snapshot = snapshots.get(entityType)\n const refreshedAt = snapshot?.refreshed_at instanceof Date ? snapshot.refreshed_at.getTime() : null\n if (refreshedAt !== null && now - refreshedAt < throttleMs) {\n lastWarmupAt.set(scopeKey(entityType, tenantId), refreshedAt)\n continue\n }\n lastWarmupAt.set(scopeKey(entityType, tenantId), now)\n stale.push(entityType)\n }\n return stale\n}\n\nexport default async function handle(payload: Payload, ctx: { resolve: <T = any>(name: string) => T }) {\n if (!isWarmupEnabled()) {\n return\n }\n\n const entityIds = getEntityIdList()\n if (!entityIds.length) {\n return\n }\n\n const tenantId = payload?.tenantId ?? null\n let eventBus: EventBus | null = null\n try {\n eventBus = ctx.resolve<EventBus>('eventBus')\n } catch {\n eventBus = null\n }\n\n if (!eventBus) {\n return\n }\n\n let em: any = null\n let db: Kysely<any> | null = null\n try {\n em = ctx.resolve<any>('em')\n db = typeof em?.getKysely === 'function' ? em.getKysely() : null\n } catch {\n em = null\n db = null\n }\n\n const throttleMs = resolveWarmupThrottleMs()\n const now = Date.now()\n\n // First pass: cheap in-process throttle so back-to-back warmups within the same\n // process (e.g. rapid logins, or entities still pending a staggered refresh) don't\n // even reach the DB.\n const candidates: string[] = []\n for (const entityType of entityIds) {\n const key = scopeKey(entityType, tenantId)\n const last = lastWarmupAt.get(key) ?? 0\n if (now - last < throttleMs) {\n continue\n }\n candidates.push(entityType)\n }\n if (!candidates.length) {\n return\n }\n\n // Second pass: durable check against the persisted coverage snapshot (see\n // `filterStaleEntityTypes`) \u2014 this is what actually fixes the \"every restart repeats\n // the full sweep\" problem, since the in-memory Map above is empty after a restart.\n const staleEntityTypes = await filterStaleEntityTypes(db, candidates, tenantId, throttleMs, now)\n if (!staleEntityTypes.length) {\n return\n }\n\n // Pre-warm the shared column-existence cache (`lib/coverage.ts`'s `COLUMN_CACHE`) for\n // every table this batch is about to check, in one query, before dispatching any\n // `coverage.refresh` events. Without this, each of the (possibly many) concurrently\n // dispatched refreshes independently asks `information_schema.columns` about its own\n // table's org/tenant/deleted columns plus the shared `vector_search.entity_id` check \u2014\n // the latter identical across every entity type, so on a cold cache they'd otherwise\n // all race and each fire the same redundant introspection query.\n if (db && em) {\n const checks: ColumnCheck[] = [{ table: 'vector_search', column: 'entity_id' }]\n for (const entityType of staleEntityTypes) {\n const table = resolveEntityTableName(em, entityType)\n checks.push(\n { table, column: 'organization_id' },\n { table, column: 'tenant_id' },\n { table, column: 'deleted_at' },\n )\n }\n await primeColumnCache(db, checks).catch(() => undefined)\n }\n\n // Spread the burst: give each chunk an increasing `delayMs` and fire every chunk's\n // emissions up front instead of blocking per chunk. `coverage_refresh.ts` already defers\n // `delayMs > 0` work via `setTimeout(...).unref()`, so this only lowers the peak concurrent\n // count of in-flight forked EntityManagers/connections \u2014 the total query count is unchanged.\n // A suggested non-zero `QUERY_INDEX_WARMUP_STAGGER_MS` (e.g. 2000) de-spikes memory-constrained\n // dev/staging; the default stays 0 so no deployment's behavior silently changes.\n const staggerMs = resolveWarmupStaggerMs()\n const concurrency = resolveWarmupConcurrency()\n const chunkPromises: Promise<unknown>[] = []\n for (let i = 0; i < staleEntityTypes.length; i += concurrency) {\n const chunkIndex = i / concurrency\n const chunk = staleEntityTypes.slice(i, i + concurrency)\n const delayMs = chunkIndex * staggerMs\n chunkPromises.push(\n Promise.allSettled(\n chunk.map((entityType) =>\n eventBus.emit('query_index.coverage.refresh', {\n entityType,\n tenantId,\n organizationId: null,\n delayMs,\n }).catch(() => undefined)\n )\n )\n )\n }\n await Promise.allSettled(chunkPromises)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -5,6 +5,17 @@ import { markDeleted } from "../lib/indexer.js";
|
|
|
5
5
|
import { applyCoverageAdjustments, createCoverageAdjustments } from "../lib/coverage.js";
|
|
6
6
|
import { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from "../lib/subscriber-scope.js";
|
|
7
7
|
const metadata = { event: "query_index.delete_one", persistent: false };
|
|
8
|
+
const DELETE_COVERAGE_THROTTLE_MS = 5 * 60 * 1e3;
|
|
9
|
+
const lastDeleteCoverageRefreshAt = /* @__PURE__ */ new Map();
|
|
10
|
+
function shouldAllowDeleteCoverageRefresh(entityType, tenantId) {
|
|
11
|
+
if (!entityType) return false;
|
|
12
|
+
const key = `${entityType}|${tenantId ?? "__null__"}`;
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const last = lastDeleteCoverageRefreshAt.get(key) ?? 0;
|
|
15
|
+
if (now - last < DELETE_COVERAGE_THROTTLE_MS) return false;
|
|
16
|
+
lastDeleteCoverageRefreshAt.set(key, now);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
8
19
|
async function handle(payload, ctx) {
|
|
9
20
|
const baseEm = ctx.resolve("em");
|
|
10
21
|
const em = typeof baseEm?.fork === "function" ? baseEm.fork() : baseEm;
|
|
@@ -59,7 +70,9 @@ async function handle(payload, ctx) {
|
|
|
59
70
|
await applyCoverageAdjustments(em, adjustments);
|
|
60
71
|
}
|
|
61
72
|
}
|
|
62
|
-
const
|
|
73
|
+
const suppressCoverage = payload?.suppressCoverage === true;
|
|
74
|
+
const explicitDelayRequested = typeof payload?.coverageDelayMs === "number";
|
|
75
|
+
const shouldRefreshCoverage = !suppressCoverage && (coverageDelayMs === void 0 || coverageDelayMs >= 0) && (explicitDelayRequested || shouldAllowDeleteCoverageRefresh(entityType, tenantId));
|
|
63
76
|
const coverageRefreshDelay = coverageDelayMs ?? 0;
|
|
64
77
|
void (async () => {
|
|
65
78
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/subscribers/delete_one.ts"],
|
|
4
|
-
"sourcesContent": ["import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { sql } from 'kysely'\nimport { markDeleted } from '../lib/indexer'\nimport { applyCoverageAdjustments, createCoverageAdjustments } from '../lib/coverage'\nimport { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from '../lib/subscriber-scope'\n\nexport const metadata = { event: 'query_index.delete_one', persistent: false }\n\nexport default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {\n // Forked EntityManager \u2014 this awaited subscriber runs synchronously on the request\n // `em`; isolating it prevents our queries/writes from resetting the originating CRUD\n // write's UnitOfWork and dropping its pending changes. See upsert_one.ts for detail.\n const baseEm = ctx.resolve<any>('em')\n const em = typeof baseEm?.fork === 'function' ? baseEm.fork() : baseEm\n const entityType = String(payload?.entityType || '')\n const recordId = String(payload?.recordId || '')\n if (!entityType || !recordId) return\n let organizationId: string | null = payload?.organizationId ?? null\n let tenantId: string | null = payload?.tenantId ?? null\n const coverageDelayMs = typeof payload?.coverageDelayMs === 'number' ? payload.coverageDelayMs : undefined\n try {\n const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'organizationId')\n const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'tenantId')\n const rowScope = await loadQueryIndexRowScope(em, entityType, recordId).catch(() => null)\n const resolvedScope = resolveQueryIndexRecordScope({\n payloadOrganizationId: payload?.organizationId,\n payloadTenantId: payload?.tenantId,\n hasPayloadOrganizationId,\n hasPayloadTenantId,\n rowScope,\n })\n organizationId = resolvedScope.organizationId\n tenantId = resolvedScope.tenantId\n\n const { wasActive } = await markDeleted(em, { entityType, recordId, organizationId, tenantId })\n\n let baseDelta = 0\n let baseCheckSucceeded = false\n try {\n const db = (em as any).getKysely()\n const table = resolveEntityTableName(em, entityType)\n const row = await db\n .selectFrom(table as any)\n .select(['deleted_at' as any])\n .where('id' as any, '=', recordId)\n .where('organization_id' as any, organizationId === null ? 'is' : '=', organizationId as any)\n .where(sql`tenant_id is not distinct from ${tenantId}`)\n .executeTakeFirst() as { deleted_at: Date | null } | undefined\n const baseMissing = !row\n const baseDeleted = baseMissing || (row && row.deleted_at != null)\n baseCheckSucceeded = true\n if (baseDeleted) baseDelta = -1\n } catch {}\n if (!baseCheckSucceeded) baseDelta = -1\n\n const baseDeltaOverride =\n typeof payload?.coverageBaseDelta === 'number' ? payload.coverageBaseDelta : undefined\n const indexDeltaOverride =\n typeof payload?.coverageIndexDelta === 'number' ? payload.coverageIndexDelta : undefined\n let effectiveBaseDelta = baseDeltaOverride ?? baseDelta\n let effectiveIndexDelta = indexDeltaOverride ?? (wasActive ? -1 : 0)\n\n if (!Number.isFinite(effectiveBaseDelta)) effectiveBaseDelta = 0\n if (!Number.isFinite(effectiveIndexDelta)) effectiveIndexDelta = 0\n\n if (effectiveBaseDelta !== 0 || effectiveIndexDelta !== 0) {\n const adjustments = createCoverageAdjustments({\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n baseDelta: effectiveBaseDelta,\n indexDelta: effectiveIndexDelta,\n })\n if (adjustments.length) {\n await applyCoverageAdjustments(em, adjustments)\n }\n }\n\n // The projection row + token removal above are synchronous (the data engine\n // awaits this subscriber) so list reads are consistent immediately. The coverage\n // recompute (a COUNT, run inline when delayMs is 0) and the fulltext delete are\n // secondary, so defer them fire-and-forget to keep write/bulk-delete latency bounded.\n const
|
|
5
|
-
"mappings": "AAAA,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,WAAW;AACpB,SAAS,mBAAmB;AAC5B,SAAS,0BAA0B,iCAAiC;AACpE,SAAS,wBAAwB,oCAAoC;AAE9D,MAAM,WAAW,EAAE,OAAO,0BAA0B,YAAY,MAAM;
|
|
4
|
+
"sourcesContent": ["import { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'\nimport { sql } from 'kysely'\nimport { markDeleted } from '../lib/indexer'\nimport { applyCoverageAdjustments, createCoverageAdjustments } from '../lib/coverage'\nimport { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from '../lib/subscriber-scope'\n\nexport const metadata = { event: 'query_index.delete_one', persistent: false }\n\n// Mirrors `shouldTriggerCoverageRefresh()` in `@open-mercato/shared/lib/data/engine.ts`\n// as a small local throttle so per-record deletes stop firing an unconditional, immediate\n// full recount. Note: this Map and the shared engine's own throttle Map are independent,\n// so a rare race right after a 5-minute window resets on both could double-fire once \u2014\n// harmless (the recompute is idempotent) and far cheaper than today's per-delete cost.\nconst DELETE_COVERAGE_THROTTLE_MS = 5 * 60 * 1000\nconst lastDeleteCoverageRefreshAt = new Map<string, number>()\n\nfunction shouldAllowDeleteCoverageRefresh(entityType: string, tenantId: string | null): boolean {\n if (!entityType) return false\n const key = `${entityType}|${tenantId ?? '__null__'}`\n const now = Date.now()\n const last = lastDeleteCoverageRefreshAt.get(key) ?? 0\n if (now - last < DELETE_COVERAGE_THROTTLE_MS) return false\n lastDeleteCoverageRefreshAt.set(key, now)\n return true\n}\n\nexport default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {\n // Forked EntityManager \u2014 this awaited subscriber runs synchronously on the request\n // `em`; isolating it prevents our queries/writes from resetting the originating CRUD\n // write's UnitOfWork and dropping its pending changes. See upsert_one.ts for detail.\n const baseEm = ctx.resolve<any>('em')\n const em = typeof baseEm?.fork === 'function' ? baseEm.fork() : baseEm\n const entityType = String(payload?.entityType || '')\n const recordId = String(payload?.recordId || '')\n if (!entityType || !recordId) return\n let organizationId: string | null = payload?.organizationId ?? null\n let tenantId: string | null = payload?.tenantId ?? null\n const coverageDelayMs = typeof payload?.coverageDelayMs === 'number' ? payload.coverageDelayMs : undefined\n try {\n const hasPayloadOrganizationId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'organizationId')\n const hasPayloadTenantId = Object.prototype.hasOwnProperty.call(payload ?? {}, 'tenantId')\n const rowScope = await loadQueryIndexRowScope(em, entityType, recordId).catch(() => null)\n const resolvedScope = resolveQueryIndexRecordScope({\n payloadOrganizationId: payload?.organizationId,\n payloadTenantId: payload?.tenantId,\n hasPayloadOrganizationId,\n hasPayloadTenantId,\n rowScope,\n })\n organizationId = resolvedScope.organizationId\n tenantId = resolvedScope.tenantId\n\n const { wasActive } = await markDeleted(em, { entityType, recordId, organizationId, tenantId })\n\n let baseDelta = 0\n let baseCheckSucceeded = false\n try {\n const db = (em as any).getKysely()\n const table = resolveEntityTableName(em, entityType)\n const row = await db\n .selectFrom(table as any)\n .select(['deleted_at' as any])\n .where('id' as any, '=', recordId)\n .where('organization_id' as any, organizationId === null ? 'is' : '=', organizationId as any)\n .where(sql`tenant_id is not distinct from ${tenantId}`)\n .executeTakeFirst() as { deleted_at: Date | null } | undefined\n const baseMissing = !row\n const baseDeleted = baseMissing || (row && row.deleted_at != null)\n baseCheckSucceeded = true\n if (baseDeleted) baseDelta = -1\n } catch {}\n if (!baseCheckSucceeded) baseDelta = -1\n\n const baseDeltaOverride =\n typeof payload?.coverageBaseDelta === 'number' ? payload.coverageBaseDelta : undefined\n const indexDeltaOverride =\n typeof payload?.coverageIndexDelta === 'number' ? payload.coverageIndexDelta : undefined\n let effectiveBaseDelta = baseDeltaOverride ?? baseDelta\n let effectiveIndexDelta = indexDeltaOverride ?? (wasActive ? -1 : 0)\n\n if (!Number.isFinite(effectiveBaseDelta)) effectiveBaseDelta = 0\n if (!Number.isFinite(effectiveIndexDelta)) effectiveIndexDelta = 0\n\n if (effectiveBaseDelta !== 0 || effectiveIndexDelta !== 0) {\n const adjustments = createCoverageAdjustments({\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n baseDelta: effectiveBaseDelta,\n indexDelta: effectiveIndexDelta,\n })\n if (adjustments.length) {\n await applyCoverageAdjustments(em, adjustments)\n }\n }\n\n // The projection row + token removal above are synchronous (the data engine\n // awaits this subscriber) so list reads are consistent immediately. The coverage\n // recompute (a COUNT, run inline when delayMs is 0) and the fulltext delete are\n // secondary, so defer them fire-and-forget to keep write/bulk-delete latency bounded.\n const suppressCoverage = payload?.suppressCoverage === true\n const explicitDelayRequested = typeof payload?.coverageDelayMs === 'number'\n const shouldRefreshCoverage =\n !suppressCoverage &&\n (coverageDelayMs === undefined || coverageDelayMs >= 0) &&\n (explicitDelayRequested || shouldAllowDeleteCoverageRefresh(entityType, tenantId))\n const coverageRefreshDelay = coverageDelayMs ?? 0\n void (async () => {\n try {\n const bus = ctx.resolve<any>('eventBus')\n if (shouldRefreshCoverage) {\n await bus.emitEvent('query_index.coverage.refresh', {\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n delayMs: coverageRefreshDelay,\n })\n }\n await bus.emitEvent('search.delete_record', { entityId: entityType, recordId, organizationId, tenantId })\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.delete_one:coverage_search',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n ).catch(() => {})\n }\n })()\n } catch (error) {\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.delete_one',\n error,\n entityType,\n recordId,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n )\n throw error\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.6-develop.
|
|
3
|
+
"version": "0.6.6-develop.6397.1.c078d218fa",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -248,16 +248,16 @@
|
|
|
248
248
|
"zod": "^4.4.3"
|
|
249
249
|
},
|
|
250
250
|
"peerDependencies": {
|
|
251
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
252
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
253
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
251
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6397.1.c078d218fa",
|
|
252
|
+
"@open-mercato/shared": "0.6.6-develop.6397.1.c078d218fa",
|
|
253
|
+
"@open-mercato/ui": "0.6.6-develop.6397.1.c078d218fa",
|
|
254
254
|
"react": "^19.0.0",
|
|
255
255
|
"react-dom": "^19.0.0"
|
|
256
256
|
},
|
|
257
257
|
"devDependencies": {
|
|
258
|
-
"@open-mercato/ai-assistant": "0.6.6-develop.
|
|
259
|
-
"@open-mercato/shared": "0.6.6-develop.
|
|
260
|
-
"@open-mercato/ui": "0.6.6-develop.
|
|
258
|
+
"@open-mercato/ai-assistant": "0.6.6-develop.6397.1.c078d218fa",
|
|
259
|
+
"@open-mercato/shared": "0.6.6-develop.6397.1.c078d218fa",
|
|
260
|
+
"@open-mercato/ui": "0.6.6-develop.6397.1.c078d218fa",
|
|
261
261
|
"@testing-library/dom": "^10.4.1",
|
|
262
262
|
"@testing-library/jest-dom": "^6.9.1",
|
|
263
263
|
"@testing-library/react": "^16.3.1",
|
|
@@ -179,6 +179,14 @@
|
|
|
179
179
|
"configs.systemStatus.variables.passwordRequireUppercase.label": "Passwort erfordert Großbuchstabe",
|
|
180
180
|
"configs.systemStatus.variables.queryEngineDebugSql.description": "Roh-SQL der Hybrid-Query-Engine protokollieren.",
|
|
181
181
|
"configs.systemStatus.variables.queryEngineDebugSql.label": "Query-Engine-SQL-Debug",
|
|
182
|
+
"configs.systemStatus.variables.queryIndexWarmupConcurrency.description": "Anzahl der Coverage-Refreshes, die pro Block während des Warmups nach der Anmeldung ausgelöst werden.",
|
|
183
|
+
"configs.systemStatus.variables.queryIndexWarmupConcurrency.label": "Warmup-Blockgröße",
|
|
184
|
+
"configs.systemStatus.variables.queryIndexWarmupEnabled.description": "Schalter zum Deaktivieren des Coverage-Warmups nach der Anmeldung.",
|
|
185
|
+
"configs.systemStatus.variables.queryIndexWarmupEnabled.label": "Coverage-Warmup aktiviert",
|
|
186
|
+
"configs.systemStatus.variables.queryIndexWarmupStaggerMs.description": "Verzögerung in Millisekunden zwischen Warmup-Blöcken, um die Spitzenlast bei Speicher/Verbindungen zu verteilen.",
|
|
187
|
+
"configs.systemStatus.variables.queryIndexWarmupStaggerMs.label": "Warmup-Staffelung",
|
|
188
|
+
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.description": "Mindestabstand in Millisekunden zwischen Warmup-Durchläufen für denselben Entitätstyp und Mandanten.",
|
|
189
|
+
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Warmup-Drosselfenster",
|
|
182
190
|
"configs.systemStatus.variables.scheduleAutoReindex.description": "Automatisch Query-Index-Neuindizierungsaufträge bei Lücken planen.",
|
|
183
191
|
"configs.systemStatus.variables.scheduleAutoReindex.label": "Automatische Neuindizierung",
|
|
184
192
|
"configs.systemStatus.viewDocs": "Dokumentation anzeigen"
|
|
@@ -179,6 +179,14 @@
|
|
|
179
179
|
"configs.systemStatus.variables.passwordRequireUppercase.label": "Password requires uppercase",
|
|
180
180
|
"configs.systemStatus.variables.queryEngineDebugSql.description": "Log raw SQL generated by the hybrid query engine.",
|
|
181
181
|
"configs.systemStatus.variables.queryEngineDebugSql.label": "Query engine SQL debug",
|
|
182
|
+
"configs.systemStatus.variables.queryIndexWarmupConcurrency.description": "Number of coverage refreshes dispatched per chunk during the post-login warmup sweep.",
|
|
183
|
+
"configs.systemStatus.variables.queryIndexWarmupConcurrency.label": "Warmup chunk size",
|
|
184
|
+
"configs.systemStatus.variables.queryIndexWarmupEnabled.description": "Kill switch for the post-login coverage warmup sweep.",
|
|
185
|
+
"configs.systemStatus.variables.queryIndexWarmupEnabled.label": "Coverage warmup enabled",
|
|
186
|
+
"configs.systemStatus.variables.queryIndexWarmupStaggerMs.description": "Delay in milliseconds added between warmup chunks to spread out peak memory/connection usage.",
|
|
187
|
+
"configs.systemStatus.variables.queryIndexWarmupStaggerMs.label": "Warmup stagger",
|
|
188
|
+
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.description": "Minimum time in milliseconds between warmup sweeps for the same entity type and tenant.",
|
|
189
|
+
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Warmup throttle window",
|
|
182
190
|
"configs.systemStatus.variables.scheduleAutoReindex.description": "Automatically schedule query index reindex jobs when gaps appear.",
|
|
183
191
|
"configs.systemStatus.variables.scheduleAutoReindex.label": "Auto reindex",
|
|
184
192
|
"configs.systemStatus.viewDocs": "View documentation"
|
|
@@ -179,6 +179,14 @@
|
|
|
179
179
|
"configs.systemStatus.variables.passwordRequireUppercase.label": "Contraseña requiere mayúscula",
|
|
180
180
|
"configs.systemStatus.variables.queryEngineDebugSql.description": "Registrar el SQL sin procesar generado por el motor de consultas híbrido.",
|
|
181
181
|
"configs.systemStatus.variables.queryEngineDebugSql.label": "Debug SQL del motor de consultas",
|
|
182
|
+
"configs.systemStatus.variables.queryIndexWarmupConcurrency.description": "Número de actualizaciones de cobertura enviadas por lote durante el calentamiento posterior al inicio de sesión.",
|
|
183
|
+
"configs.systemStatus.variables.queryIndexWarmupConcurrency.label": "Tamaño de lote de calentamiento",
|
|
184
|
+
"configs.systemStatus.variables.queryIndexWarmupEnabled.description": "Interruptor para desactivar el calentamiento de cobertura posterior al inicio de sesión.",
|
|
185
|
+
"configs.systemStatus.variables.queryIndexWarmupEnabled.label": "Calentamiento de cobertura activado",
|
|
186
|
+
"configs.systemStatus.variables.queryIndexWarmupStaggerMs.description": "Retraso en milisegundos añadido entre lotes de calentamiento para repartir el uso máximo de memoria/conexiones.",
|
|
187
|
+
"configs.systemStatus.variables.queryIndexWarmupStaggerMs.label": "Escalonamiento del calentamiento",
|
|
188
|
+
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.description": "Tiempo mínimo en milisegundos entre calentamientos para el mismo tipo de entidad y tenant.",
|
|
189
|
+
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Ventana de limitación del calentamiento",
|
|
182
190
|
"configs.systemStatus.variables.scheduleAutoReindex.description": "Programar automáticamente trabajos de reindexación cuando se detecten brechas.",
|
|
183
191
|
"configs.systemStatus.variables.scheduleAutoReindex.label": "Reindexación automática",
|
|
184
192
|
"configs.systemStatus.viewDocs": "Ver documentación"
|
|
@@ -179,6 +179,14 @@
|
|
|
179
179
|
"configs.systemStatus.variables.passwordRequireUppercase.label": "Hasło wymaga wielkiej litery",
|
|
180
180
|
"configs.systemStatus.variables.queryEngineDebugSql.description": "Loguje surowe zapytania SQL generowane przez hybrydowy silnik zapytań.",
|
|
181
181
|
"configs.systemStatus.variables.queryEngineDebugSql.label": "Debugowanie SQL silnika zapytań",
|
|
182
|
+
"configs.systemStatus.variables.queryIndexWarmupConcurrency.description": "Liczba odświeżeń pokrycia wysyłanych w jednej partii podczas rozgrzewki po zalogowaniu.",
|
|
183
|
+
"configs.systemStatus.variables.queryIndexWarmupConcurrency.label": "Rozmiar partii rozgrzewki",
|
|
184
|
+
"configs.systemStatus.variables.queryIndexWarmupEnabled.description": "Wyłącznik rozgrzewki pokrycia indeksu uruchamianej po zalogowaniu.",
|
|
185
|
+
"configs.systemStatus.variables.queryIndexWarmupEnabled.label": "Rozgrzewka pokrycia włączona",
|
|
186
|
+
"configs.systemStatus.variables.queryIndexWarmupStaggerMs.description": "Opóźnienie w milisekundach dodawane między partiami rozgrzewki, aby rozłożyć szczytowe zużycie pamięci/połączeń.",
|
|
187
|
+
"configs.systemStatus.variables.queryIndexWarmupStaggerMs.label": "Rozłożenie rozgrzewki w czasie",
|
|
188
|
+
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.description": "Minimalny odstęp czasu w milisekundach między rozgrzewkami dla tego samego typu encji i najemcy.",
|
|
189
|
+
"configs.systemStatus.variables.queryIndexWarmupThrottleMs.label": "Okno throttlingu rozgrzewki",
|
|
182
190
|
"configs.systemStatus.variables.scheduleAutoReindex.description": "Automatycznie uruchamia przebudowę indeksu zapytań, gdy wykryte zostaną luki.",
|
|
183
191
|
"configs.systemStatus.variables.scheduleAutoReindex.label": "Automatyczna przebudowa indeksu",
|
|
184
192
|
"configs.systemStatus.viewDocs": "Zobacz dokumentację"
|
|
@@ -276,6 +276,42 @@ export const SYSTEM_STATUS_VARIABLES: SystemStatusVariableDefinition[] = [
|
|
|
276
276
|
docUrl: `${SYSTEM_STATUS_DOC_BASE}#force_query_index_on_partial_indexes`,
|
|
277
277
|
defaultValue: 'false',
|
|
278
278
|
},
|
|
279
|
+
{
|
|
280
|
+
key: 'QUERY_INDEX_WARMUP_ENABLED',
|
|
281
|
+
category: 'query_index',
|
|
282
|
+
kind: 'boolean',
|
|
283
|
+
labelKey: 'configs.systemStatus.variables.queryIndexWarmupEnabled.label',
|
|
284
|
+
descriptionKey: 'configs.systemStatus.variables.queryIndexWarmupEnabled.description',
|
|
285
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_enabled`,
|
|
286
|
+
defaultValue: 'true',
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
key: 'QUERY_INDEX_WARMUP_THROTTLE_MS',
|
|
290
|
+
category: 'query_index',
|
|
291
|
+
kind: 'string',
|
|
292
|
+
labelKey: 'configs.systemStatus.variables.queryIndexWarmupThrottleMs.label',
|
|
293
|
+
descriptionKey: 'configs.systemStatus.variables.queryIndexWarmupThrottleMs.description',
|
|
294
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_throttle_ms`,
|
|
295
|
+
defaultValue: String(5 * 60 * 1000),
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
key: 'QUERY_INDEX_WARMUP_CONCURRENCY',
|
|
299
|
+
category: 'query_index',
|
|
300
|
+
kind: 'string',
|
|
301
|
+
labelKey: 'configs.systemStatus.variables.queryIndexWarmupConcurrency.label',
|
|
302
|
+
descriptionKey: 'configs.systemStatus.variables.queryIndexWarmupConcurrency.description',
|
|
303
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_concurrency`,
|
|
304
|
+
defaultValue: '10',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
key: 'QUERY_INDEX_WARMUP_STAGGER_MS',
|
|
308
|
+
category: 'query_index',
|
|
309
|
+
kind: 'string',
|
|
310
|
+
labelKey: 'configs.systemStatus.variables.queryIndexWarmupStaggerMs.label',
|
|
311
|
+
descriptionKey: 'configs.systemStatus.variables.queryIndexWarmupStaggerMs.description',
|
|
312
|
+
docUrl: `${SYSTEM_STATUS_DOC_BASE}#query_index_warmup_stagger_ms`,
|
|
313
|
+
defaultValue: '0',
|
|
314
|
+
},
|
|
279
315
|
{
|
|
280
316
|
key: 'ENTITIES_BACKCOMPAT_EAV_FOR_CUSTOM',
|
|
281
317
|
category: 'entities',
|
|
@@ -50,6 +50,12 @@ export type CoverageDeltaInput = {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
const COLUMN_CACHE = new Map<string, boolean>()
|
|
53
|
+
// In-flight de-dup: without this, N concurrent `tableHasColumn` callers for the same
|
|
54
|
+
// (table, column) — e.g. every entity type's `refreshCoverageSnapshot` asking about
|
|
55
|
+
// `vector_search.entity_id` — would each see a cold cache and fire their own identical
|
|
56
|
+
// `information_schema.columns` query, since the cache is only populated after a query
|
|
57
|
+
// resolves. Tracking the in-flight promise lets late arrivals await the first one instead.
|
|
58
|
+
const COLUMN_CACHE_PENDING = new Map<string, Promise<boolean>>()
|
|
53
59
|
const GLOBAL_ORGANIZATION_PLACEHOLDER = '00000000-0000-0000-0000-000000000000'
|
|
54
60
|
export const COVERAGE_ORG_PLACEHOLDER = GLOBAL_ORGANIZATION_PLACEHOLDER
|
|
55
61
|
|
|
@@ -285,16 +291,85 @@ export async function deleteCoverageForEntity(db: Kysely<any>, entityType: strin
|
|
|
285
291
|
async function tableHasColumn(db: Kysely<any>, table: string, column: string): Promise<boolean> {
|
|
286
292
|
const key = `${table}.${column}`
|
|
287
293
|
if (COLUMN_CACHE.has(key)) return COLUMN_CACHE.get(key)!
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
const pending = COLUMN_CACHE_PENDING.get(key)
|
|
295
|
+
if (pending) return pending
|
|
296
|
+
const promise = (async () => {
|
|
297
|
+
const exists = await db
|
|
298
|
+
.selectFrom('information_schema.columns' as any)
|
|
299
|
+
.select(sql<number>`1`.as('present'))
|
|
300
|
+
.where(sql<boolean>`table_schema = current_schema()`)
|
|
301
|
+
.where('table_name' as any, '=', table)
|
|
302
|
+
.where('column_name' as any, '=', column)
|
|
303
|
+
.executeTakeFirst()
|
|
304
|
+
const present = !!exists
|
|
305
|
+
COLUMN_CACHE.set(key, present)
|
|
306
|
+
return present
|
|
307
|
+
})()
|
|
308
|
+
COLUMN_CACHE_PENDING.set(key, promise)
|
|
309
|
+
try {
|
|
310
|
+
return await promise
|
|
311
|
+
} finally {
|
|
312
|
+
COLUMN_CACHE_PENDING.delete(key)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export type ColumnCheck = { table: string; column: string }
|
|
317
|
+
|
|
318
|
+
// Batches the `information_schema.columns` introspection used by `refreshCoverageSnapshot`
|
|
319
|
+
// into a single query for a whole set of (table, column) pairs, and pre-populates
|
|
320
|
+
// `COLUMN_CACHE_PENDING` for every pair before that query even runs. Callers of
|
|
321
|
+
// `coverage_warmup.ts` use this so its many concurrently-dispatched `coverage.refresh`
|
|
322
|
+
// subscribers hit an already-primed (or in-flight) cache instead of each doing their own
|
|
323
|
+
// per-table introspection round trip.
|
|
324
|
+
export async function primeColumnCache(db: Kysely<any>, checks: ColumnCheck[]): Promise<void> {
|
|
325
|
+
const missing: Array<{ table: string; column: string; key: string }> = []
|
|
326
|
+
const seen = new Set<string>()
|
|
327
|
+
for (const check of checks) {
|
|
328
|
+
const table = String(check?.table || '')
|
|
329
|
+
const column = String(check?.column || '')
|
|
330
|
+
if (!table || !column) continue
|
|
331
|
+
const key = `${table}.${column}`
|
|
332
|
+
if (seen.has(key)) continue
|
|
333
|
+
seen.add(key)
|
|
334
|
+
if (COLUMN_CACHE.has(key) || COLUMN_CACHE_PENDING.has(key)) continue
|
|
335
|
+
missing.push({ table, column, key })
|
|
336
|
+
}
|
|
337
|
+
if (!missing.length) return
|
|
338
|
+
|
|
339
|
+
const tables = Array.from(new Set(missing.map((entry) => entry.table)))
|
|
340
|
+
const columns = Array.from(new Set(missing.map((entry) => entry.column)))
|
|
341
|
+
|
|
342
|
+
const batchPromise = (async (): Promise<Set<string>> => {
|
|
343
|
+
const rows = await db
|
|
344
|
+
.selectFrom('information_schema.columns' as any)
|
|
345
|
+
.select(['table_name' as any, 'column_name' as any])
|
|
346
|
+
.where(sql<boolean>`table_schema = current_schema()`)
|
|
347
|
+
.where('table_name' as any, 'in', tables)
|
|
348
|
+
.where('column_name' as any, 'in', columns)
|
|
349
|
+
.execute() as Array<{ table_name: string; column_name: string }>
|
|
350
|
+
return new Set(rows.map((row) => `${row.table_name}.${row.column_name}`))
|
|
351
|
+
})()
|
|
352
|
+
|
|
353
|
+
for (const entry of missing) {
|
|
354
|
+
const entryPromise = batchPromise.then((present) => {
|
|
355
|
+
const value = present.has(entry.key)
|
|
356
|
+
COLUMN_CACHE.set(entry.key, value)
|
|
357
|
+
return value
|
|
358
|
+
})
|
|
359
|
+
// Mark the stored promise as handled: when the batch query fails and no
|
|
360
|
+
// `tableHasColumn` caller has adopted this entry yet (the common case — the warmup
|
|
361
|
+
// awaits priming before dispatching any refresh), an orphaned rejection would
|
|
362
|
+
// otherwise crash a plain-Node event worker via unhandledRejection. Awaiting
|
|
363
|
+
// callers still observe the rejection through the stored reference.
|
|
364
|
+
entryPromise.catch(() => undefined)
|
|
365
|
+
COLUMN_CACHE_PENDING.set(entry.key, entryPromise)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
await batchPromise
|
|
370
|
+
} finally {
|
|
371
|
+
for (const entry of missing) COLUMN_CACHE_PENDING.delete(entry.key)
|
|
372
|
+
}
|
|
298
373
|
}
|
|
299
374
|
|
|
300
375
|
export async function refreshCoverageSnapshot(
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { Kysely } from 'kysely'
|
|
1
2
|
import { getEntityIds } from '@open-mercato/shared/lib/encryption/entityIds'
|
|
2
3
|
import type { EventBus } from '@open-mercato/events/types'
|
|
3
4
|
import { flattenSystemEntityIds } from '@open-mercato/shared/lib/entities/system-entities'
|
|
5
|
+
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
6
|
+
import { resolveEntityTableName } from '@open-mercato/shared/lib/query/engine'
|
|
7
|
+
import { readCoverageSnapshots, primeColumnCache, type ColumnCheck } from '../lib/coverage'
|
|
4
8
|
|
|
5
9
|
export const metadata = { event: 'query_index.coverage.warmup', persistent: false }
|
|
6
10
|
|
|
@@ -8,19 +12,83 @@ type Payload = {
|
|
|
8
12
|
tenantId?: string | null
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
15
|
+
const DEFAULT_WARMUP_THROTTLE_MS = 5 * 60 * 1000
|
|
16
|
+
const DEFAULT_WARMUP_REFRESH_CONCURRENCY = 10
|
|
17
|
+
const DEFAULT_WARMUP_STAGGER_MS = 0
|
|
13
18
|
const lastWarmupAt = new Map<string, number>()
|
|
14
19
|
|
|
15
20
|
function scopeKey(entityType: string, tenantId: string | null): string {
|
|
16
21
|
return `${entityType}|${tenantId ?? '__null__'}`
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
function isWarmupEnabled(): boolean {
|
|
25
|
+
return parseBooleanWithDefault(process.env.QUERY_INDEX_WARMUP_ENABLED, true)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveWarmupThrottleMs(): number {
|
|
29
|
+
const raw = Number.parseInt(process.env.QUERY_INDEX_WARMUP_THROTTLE_MS ?? '', 10)
|
|
30
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_WARMUP_THROTTLE_MS
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolveWarmupConcurrency(): number {
|
|
34
|
+
const raw = Number.parseInt(process.env.QUERY_INDEX_WARMUP_CONCURRENCY ?? '', 10)
|
|
35
|
+
return Number.isFinite(raw) && raw >= 1 ? raw : DEFAULT_WARMUP_REFRESH_CONCURRENCY
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveWarmupStaggerMs(): number {
|
|
39
|
+
const raw = Number.parseInt(process.env.QUERY_INDEX_WARMUP_STAGGER_MS ?? '', 10)
|
|
40
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_WARMUP_STAGGER_MS
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
function getEntityIdList(): string[] {
|
|
20
44
|
return flattenSystemEntityIds(getEntityIds() as Record<string, Record<string, string>>)
|
|
21
45
|
}
|
|
22
46
|
|
|
47
|
+
// Durable staleness check against `entity_index_coverage.refreshed_at`. The in-memory
|
|
48
|
+
// `lastWarmupAt` Map only throttles within one process, so a fresh restart forgets
|
|
49
|
+
// every scope and re-runs the full sweep even for entity types whose persisted snapshot
|
|
50
|
+
// was refreshed moments earlier (by a prior warmup, a lazy query-engine refresh, or an
|
|
51
|
+
// incremental CRUD delta via `applyCoverageAdjustments`, which also bumps `refreshed_at`).
|
|
52
|
+
// This check is batched (one query for all candidates) and replaces process memory with
|
|
53
|
+
// the same source of truth the query engine itself reads.
|
|
54
|
+
async function filterStaleEntityTypes(
|
|
55
|
+
db: Kysely<any> | null,
|
|
56
|
+
candidates: string[],
|
|
57
|
+
tenantId: string | null,
|
|
58
|
+
throttleMs: number,
|
|
59
|
+
now: number,
|
|
60
|
+
): Promise<string[]> {
|
|
61
|
+
if (!db) {
|
|
62
|
+
for (const entityType of candidates) lastWarmupAt.set(scopeKey(entityType, tenantId), now)
|
|
63
|
+
return candidates
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const snapshots = await readCoverageSnapshots(db, {
|
|
67
|
+
entityTypes: candidates,
|
|
68
|
+
tenantId,
|
|
69
|
+
organizationId: null,
|
|
70
|
+
withDeleted: false,
|
|
71
|
+
}).catch(() => new Map())
|
|
72
|
+
|
|
73
|
+
const stale: string[] = []
|
|
74
|
+
for (const entityType of candidates) {
|
|
75
|
+
const snapshot = snapshots.get(entityType)
|
|
76
|
+
const refreshedAt = snapshot?.refreshed_at instanceof Date ? snapshot.refreshed_at.getTime() : null
|
|
77
|
+
if (refreshedAt !== null && now - refreshedAt < throttleMs) {
|
|
78
|
+
lastWarmupAt.set(scopeKey(entityType, tenantId), refreshedAt)
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
lastWarmupAt.set(scopeKey(entityType, tenantId), now)
|
|
82
|
+
stale.push(entityType)
|
|
83
|
+
}
|
|
84
|
+
return stale
|
|
85
|
+
}
|
|
86
|
+
|
|
23
87
|
export default async function handle(payload: Payload, ctx: { resolve: <T = any>(name: string) => T }) {
|
|
88
|
+
if (!isWarmupEnabled()) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
24
92
|
const entityIds = getEntityIdList()
|
|
25
93
|
if (!entityIds.length) {
|
|
26
94
|
return
|
|
@@ -38,30 +106,88 @@ export default async function handle(payload: Payload, ctx: { resolve: <T = any>
|
|
|
38
106
|
return
|
|
39
107
|
}
|
|
40
108
|
|
|
109
|
+
let em: any = null
|
|
110
|
+
let db: Kysely<any> | null = null
|
|
111
|
+
try {
|
|
112
|
+
em = ctx.resolve<any>('em')
|
|
113
|
+
db = typeof em?.getKysely === 'function' ? em.getKysely() : null
|
|
114
|
+
} catch {
|
|
115
|
+
em = null
|
|
116
|
+
db = null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const throttleMs = resolveWarmupThrottleMs()
|
|
41
120
|
const now = Date.now()
|
|
42
|
-
|
|
121
|
+
|
|
122
|
+
// First pass: cheap in-process throttle so back-to-back warmups within the same
|
|
123
|
+
// process (e.g. rapid logins, or entities still pending a staggered refresh) don't
|
|
124
|
+
// even reach the DB.
|
|
125
|
+
const candidates: string[] = []
|
|
43
126
|
for (const entityType of entityIds) {
|
|
44
127
|
const key = scopeKey(entityType, tenantId)
|
|
45
128
|
const last = lastWarmupAt.get(key) ?? 0
|
|
46
|
-
if (now - last <
|
|
129
|
+
if (now - last < throttleMs) {
|
|
47
130
|
continue
|
|
48
131
|
}
|
|
132
|
+
candidates.push(entityType)
|
|
133
|
+
}
|
|
134
|
+
if (!candidates.length) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Second pass: durable check against the persisted coverage snapshot (see
|
|
139
|
+
// `filterStaleEntityTypes`) — this is what actually fixes the "every restart repeats
|
|
140
|
+
// the full sweep" problem, since the in-memory Map above is empty after a restart.
|
|
141
|
+
const staleEntityTypes = await filterStaleEntityTypes(db, candidates, tenantId, throttleMs, now)
|
|
142
|
+
if (!staleEntityTypes.length) {
|
|
143
|
+
return
|
|
144
|
+
}
|
|
49
145
|
|
|
50
|
-
|
|
51
|
-
|
|
146
|
+
// Pre-warm the shared column-existence cache (`lib/coverage.ts`'s `COLUMN_CACHE`) for
|
|
147
|
+
// every table this batch is about to check, in one query, before dispatching any
|
|
148
|
+
// `coverage.refresh` events. Without this, each of the (possibly many) concurrently
|
|
149
|
+
// dispatched refreshes independently asks `information_schema.columns` about its own
|
|
150
|
+
// table's org/tenant/deleted columns plus the shared `vector_search.entity_id` check —
|
|
151
|
+
// the latter identical across every entity type, so on a cold cache they'd otherwise
|
|
152
|
+
// all race and each fire the same redundant introspection query.
|
|
153
|
+
if (db && em) {
|
|
154
|
+
const checks: ColumnCheck[] = [{ table: 'vector_search', column: 'entity_id' }]
|
|
155
|
+
for (const entityType of staleEntityTypes) {
|
|
156
|
+
const table = resolveEntityTableName(em, entityType)
|
|
157
|
+
checks.push(
|
|
158
|
+
{ table, column: 'organization_id' },
|
|
159
|
+
{ table, column: 'tenant_id' },
|
|
160
|
+
{ table, column: 'deleted_at' },
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
await primeColumnCache(db, checks).catch(() => undefined)
|
|
52
164
|
}
|
|
53
165
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
166
|
+
// Spread the burst: give each chunk an increasing `delayMs` and fire every chunk's
|
|
167
|
+
// emissions up front instead of blocking per chunk. `coverage_refresh.ts` already defers
|
|
168
|
+
// `delayMs > 0` work via `setTimeout(...).unref()`, so this only lowers the peak concurrent
|
|
169
|
+
// count of in-flight forked EntityManagers/connections — the total query count is unchanged.
|
|
170
|
+
// A suggested non-zero `QUERY_INDEX_WARMUP_STAGGER_MS` (e.g. 2000) de-spikes memory-constrained
|
|
171
|
+
// dev/staging; the default stays 0 so no deployment's behavior silently changes.
|
|
172
|
+
const staggerMs = resolveWarmupStaggerMs()
|
|
173
|
+
const concurrency = resolveWarmupConcurrency()
|
|
174
|
+
const chunkPromises: Promise<unknown>[] = []
|
|
175
|
+
for (let i = 0; i < staleEntityTypes.length; i += concurrency) {
|
|
176
|
+
const chunkIndex = i / concurrency
|
|
177
|
+
const chunk = staleEntityTypes.slice(i, i + concurrency)
|
|
178
|
+
const delayMs = chunkIndex * staggerMs
|
|
179
|
+
chunkPromises.push(
|
|
180
|
+
Promise.allSettled(
|
|
181
|
+
chunk.map((entityType) =>
|
|
182
|
+
eventBus.emit('query_index.coverage.refresh', {
|
|
183
|
+
entityType,
|
|
184
|
+
tenantId,
|
|
185
|
+
organizationId: null,
|
|
186
|
+
delayMs,
|
|
187
|
+
}).catch(() => undefined)
|
|
188
|
+
)
|
|
64
189
|
)
|
|
65
190
|
)
|
|
66
191
|
}
|
|
192
|
+
await Promise.allSettled(chunkPromises)
|
|
67
193
|
}
|
|
@@ -7,6 +7,24 @@ import { loadQueryIndexRowScope, resolveQueryIndexRecordScope } from '../lib/sub
|
|
|
7
7
|
|
|
8
8
|
export const metadata = { event: 'query_index.delete_one', persistent: false }
|
|
9
9
|
|
|
10
|
+
// Mirrors `shouldTriggerCoverageRefresh()` in `@open-mercato/shared/lib/data/engine.ts`
|
|
11
|
+
// as a small local throttle so per-record deletes stop firing an unconditional, immediate
|
|
12
|
+
// full recount. Note: this Map and the shared engine's own throttle Map are independent,
|
|
13
|
+
// so a rare race right after a 5-minute window resets on both could double-fire once —
|
|
14
|
+
// harmless (the recompute is idempotent) and far cheaper than today's per-delete cost.
|
|
15
|
+
const DELETE_COVERAGE_THROTTLE_MS = 5 * 60 * 1000
|
|
16
|
+
const lastDeleteCoverageRefreshAt = new Map<string, number>()
|
|
17
|
+
|
|
18
|
+
function shouldAllowDeleteCoverageRefresh(entityType: string, tenantId: string | null): boolean {
|
|
19
|
+
if (!entityType) return false
|
|
20
|
+
const key = `${entityType}|${tenantId ?? '__null__'}`
|
|
21
|
+
const now = Date.now()
|
|
22
|
+
const last = lastDeleteCoverageRefreshAt.get(key) ?? 0
|
|
23
|
+
if (now - last < DELETE_COVERAGE_THROTTLE_MS) return false
|
|
24
|
+
lastDeleteCoverageRefreshAt.set(key, now)
|
|
25
|
+
return true
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
export default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {
|
|
11
29
|
// Forked EntityManager — this awaited subscriber runs synchronously on the request
|
|
12
30
|
// `em`; isolating it prevents our queries/writes from resetting the originating CRUD
|
|
@@ -81,7 +99,12 @@ export default async function handle(payload: any, ctx: { resolve: <T=any>(name:
|
|
|
81
99
|
// awaits this subscriber) so list reads are consistent immediately. The coverage
|
|
82
100
|
// recompute (a COUNT, run inline when delayMs is 0) and the fulltext delete are
|
|
83
101
|
// secondary, so defer them fire-and-forget to keep write/bulk-delete latency bounded.
|
|
84
|
-
const
|
|
102
|
+
const suppressCoverage = payload?.suppressCoverage === true
|
|
103
|
+
const explicitDelayRequested = typeof payload?.coverageDelayMs === 'number'
|
|
104
|
+
const shouldRefreshCoverage =
|
|
105
|
+
!suppressCoverage &&
|
|
106
|
+
(coverageDelayMs === undefined || coverageDelayMs >= 0) &&
|
|
107
|
+
(explicitDelayRequested || shouldAllowDeleteCoverageRefresh(entityType, tenantId))
|
|
85
108
|
const coverageRefreshDelay = coverageDelayMs ?? 0
|
|
86
109
|
void (async () => {
|
|
87
110
|
try {
|