@open-mercato/core 0.4.6-develop-6953d75a91 → 0.4.6-develop-90c3eb0e8a
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/notifications/events.js +30 -0
- package/dist/modules/notifications/events.js.map +7 -0
- package/dist/modules/notifications/frontend/NotificationInboxPageClient.js +2 -3
- package/dist/modules/notifications/frontend/NotificationInboxPageClient.js.map +2 -2
- package/dist/modules/notifications/lib/events.js +6 -1
- package/dist/modules/notifications/lib/events.js.map +2 -2
- package/dist/modules/notifications/lib/notificationMapper.js +10 -1
- package/dist/modules/notifications/lib/notificationMapper.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +26 -1
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/progress/events.js +6 -6
- package/dist/modules/progress/events.js.map +2 -2
- package/dist/modules/progress/lib/events.js.map +1 -1
- package/dist/modules/progress/lib/progressServiceImpl.js +38 -29
- package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
- package/dist/modules/query_index/api/reindex.js +3 -0
- package/dist/modules/query_index/api/reindex.js.map +2 -2
- package/dist/modules/query_index/components/QueryIndexesTable.js +8 -10
- package/dist/modules/query_index/components/QueryIndexesTable.js.map +2 -2
- package/dist/modules/query_index/subscribers/reindex.js +89 -1
- package/dist/modules/query_index/subscribers/reindex.js.map +2 -2
- package/package.json +2 -2
- package/src/modules/notifications/README.md +21 -0
- package/src/modules/notifications/events.ts +28 -0
- package/src/modules/notifications/frontend/NotificationInboxPageClient.tsx +2 -3
- package/src/modules/notifications/lib/events.ts +5 -0
- package/src/modules/notifications/lib/notificationMapper.ts +12 -1
- package/src/modules/notifications/lib/notificationService.ts +33 -1
- package/src/modules/progress/events.ts +6 -6
- package/src/modules/progress/lib/events.ts +60 -0
- package/src/modules/progress/lib/progressServiceImpl.ts +32 -22
- package/src/modules/query_index/api/reindex.ts +3 -0
- package/src/modules/query_index/components/QueryIndexesTable.tsx +8 -10
- package/src/modules/query_index/subscribers/reindex.ts +99 -0
|
@@ -64,6 +64,9 @@ async function POST(req) {
|
|
|
64
64
|
if (auth.orgId !== void 0) {
|
|
65
65
|
payload.organizationId = auth.orgId ?? null;
|
|
66
66
|
}
|
|
67
|
+
if (typeof auth.sub === "string" && auth.sub.length > 0) {
|
|
68
|
+
payload.requestedByUserId = auth.sub;
|
|
69
|
+
}
|
|
67
70
|
return bus.emitEvent(
|
|
68
71
|
"query_index.reindex",
|
|
69
72
|
payload,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/api/reindex.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { queryIndexTag, queryIndexErrorSchema, queryIndexOkSchema, queryIndexReindexRequestSchema } from './openapi'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['query_index.reindex'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const body = await req.json().catch(() => ({})) as any\n const entityType = String(body?.entityType || '')\n if (!entityType) return NextResponse.json({ error: 'Missing entityType' }, { status: 400 })\n const force = Boolean(body?.force)\n const batchSize = Number.isFinite(body?.batchSize) ? Math.max(1, Math.trunc(body.batchSize)) : undefined\n const partitionCountInput = Number(body?.partitionCount)\n const partitionCount = Number.isFinite(partitionCountInput)\n ? Math.max(1, Math.trunc(partitionCountInput))\n : 1\n const partitionIndexInput = Number(body?.partitionIndex)\n const partitionIndex = Number.isFinite(partitionIndexInput) ? Math.max(0, Math.trunc(partitionIndexInput)) : undefined\n if (partitionIndex !== undefined && partitionIndex >= partitionCount) {\n return NextResponse.json({ error: 'partitionIndex must be < partitionCount' }, { status: 400 })\n }\n\n const { resolve } = await createRequestContainer()\n let em: any | null = null\n try {\n em = resolve('em')\n } catch {}\n const bus = resolve('eventBus') as any\n const partitions = partitionIndex !== undefined\n ? [partitionIndex]\n : Array.from({ length: partitionCount }, (_, idx) => idx)\n const firstPartition = partitions[0] ?? 0\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'query_index',\n handler: 'api:query_index.reindex',\n message: `Reindex requested for ${entityType}`,\n entityType,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: {\n force,\n batchSize: batchSize ?? null,\n partitionCount,\n partitionIndex: partitionIndex ?? null,\n },\n },\n ).catch(() => undefined)\n try {\n await Promise.all(\n partitions.map((part) => {\n const payload: Record<string, unknown> = {\n entityType,\n force,\n batchSize,\n partitionCount,\n partitionIndex: part,\n resetCoverage: part === firstPartition,\n }\n if (auth.tenantId !== undefined) {\n payload.tenantId = auth.tenantId ?? null\n }\n if (auth.orgId !== undefined) {\n payload.organizationId = auth.orgId ?? null\n }\n return bus.emitEvent(\n 'query_index.reindex',\n payload,\n { persistent: true },\n )\n }),\n )\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'query_index',\n handler: 'api:query_index.reindex',\n message: `Reindex queued for ${entityType}`,\n entityType,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: {\n force,\n batchSize: batchSize ?? null,\n partitionCount,\n partitionIndex: partitionIndex ?? null,\n },\n },\n ).catch(() => undefined)\n } catch (error) {\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'query_index',\n handler: 'api:query_index.reindex',\n level: 'warn',\n message: `Failed to queue reindex for ${entityType}`,\n entityType,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: {\n error: error instanceof Error ? error.message : String(error),\n },\n },\n ).catch(() => undefined)\n throw error\n }\n return NextResponse.json({ ok: true })\n}\n\nconst queryIndexReindexDoc: OpenApiMethodDoc = {\n summary: 'Trigger query index rebuild',\n description: 'Queues a reindex job for the specified entity type within the current tenant scope.',\n tags: [queryIndexTag],\n requestBody: {\n contentType: 'application/json',\n schema: queryIndexReindexRequestSchema,\n description: 'Entity identifier and optional force flag.',\n },\n responses: [\n { status: 200, description: 'Reindex job accepted.', schema: queryIndexOkSchema },\n ],\n errors: [\n { status: 400, description: 'Missing entity type', schema: queryIndexErrorSchema },\n { status: 401, description: 'Authentication required', schema: queryIndexErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: queryIndexTag,\n summary: 'Queue a query index rebuild',\n methods: {\n POST: queryIndexReindexDoc,\n },\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OpenApiMethodDoc, OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { queryIndexTag, queryIndexErrorSchema, queryIndexOkSchema, queryIndexReindexRequestSchema } from './openapi'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['query_index.reindex'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth || !auth.tenantId || !auth.orgId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n const body = await req.json().catch(() => ({})) as any\n const entityType = String(body?.entityType || '')\n if (!entityType) return NextResponse.json({ error: 'Missing entityType' }, { status: 400 })\n const force = Boolean(body?.force)\n const batchSize = Number.isFinite(body?.batchSize) ? Math.max(1, Math.trunc(body.batchSize)) : undefined\n const partitionCountInput = Number(body?.partitionCount)\n const partitionCount = Number.isFinite(partitionCountInput)\n ? Math.max(1, Math.trunc(partitionCountInput))\n : 1\n const partitionIndexInput = Number(body?.partitionIndex)\n const partitionIndex = Number.isFinite(partitionIndexInput) ? Math.max(0, Math.trunc(partitionIndexInput)) : undefined\n if (partitionIndex !== undefined && partitionIndex >= partitionCount) {\n return NextResponse.json({ error: 'partitionIndex must be < partitionCount' }, { status: 400 })\n }\n\n const { resolve } = await createRequestContainer()\n let em: any | null = null\n try {\n em = resolve('em')\n } catch {}\n const bus = resolve('eventBus') as any\n const partitions = partitionIndex !== undefined\n ? [partitionIndex]\n : Array.from({ length: partitionCount }, (_, idx) => idx)\n const firstPartition = partitions[0] ?? 0\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'query_index',\n handler: 'api:query_index.reindex',\n message: `Reindex requested for ${entityType}`,\n entityType,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: {\n force,\n batchSize: batchSize ?? null,\n partitionCount,\n partitionIndex: partitionIndex ?? null,\n },\n },\n ).catch(() => undefined)\n try {\n await Promise.all(\n partitions.map((part) => {\n const payload: Record<string, unknown> = {\n entityType,\n force,\n batchSize,\n partitionCount,\n partitionIndex: part,\n resetCoverage: part === firstPartition,\n }\n if (auth.tenantId !== undefined) {\n payload.tenantId = auth.tenantId ?? null\n }\n if (auth.orgId !== undefined) {\n payload.organizationId = auth.orgId ?? null\n }\n if (typeof auth.sub === 'string' && auth.sub.length > 0) {\n payload.requestedByUserId = auth.sub\n }\n return bus.emitEvent(\n 'query_index.reindex',\n payload,\n { persistent: true },\n )\n }),\n )\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'query_index',\n handler: 'api:query_index.reindex',\n message: `Reindex queued for ${entityType}`,\n entityType,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: {\n force,\n batchSize: batchSize ?? null,\n partitionCount,\n partitionIndex: partitionIndex ?? null,\n },\n },\n ).catch(() => undefined)\n } catch (error) {\n await recordIndexerLog(\n { em: em ?? undefined },\n {\n source: 'query_index',\n handler: 'api:query_index.reindex',\n level: 'warn',\n message: `Failed to queue reindex for ${entityType}`,\n entityType,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n details: {\n error: error instanceof Error ? error.message : String(error),\n },\n },\n ).catch(() => undefined)\n throw error\n }\n return NextResponse.json({ ok: true })\n}\n\nconst queryIndexReindexDoc: OpenApiMethodDoc = {\n summary: 'Trigger query index rebuild',\n description: 'Queues a reindex job for the specified entity type within the current tenant scope.',\n tags: [queryIndexTag],\n requestBody: {\n contentType: 'application/json',\n schema: queryIndexReindexRequestSchema,\n description: 'Entity identifier and optional force flag.',\n },\n responses: [\n { status: 200, description: 'Reindex job accepted.', schema: queryIndexOkSchema },\n ],\n errors: [\n { status: 400, description: 'Missing entity type', schema: queryIndexErrorSchema },\n { status: 401, description: 'Authentication required', schema: queryIndexErrorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n tag: queryIndexTag,\n summary: 'Queue a query index rebuild',\n methods: {\n POST: queryIndexReindexDoc,\n },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -192,16 +192,14 @@ function QueryIndexesTable() {
|
|
|
192
192
|
const actionLabel = action === "purge" ? t("query_index.table.actions.vectorPurge") : t("query_index.table.actions.vectorReindex");
|
|
193
193
|
const errorMessage = t("query_index.table.errors.actionFailed", { action: actionLabel });
|
|
194
194
|
try {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
await apiCallOrThrow(url, { method: "DELETE" }, { errorMessage });
|
|
204
|
-
}
|
|
195
|
+
await apiCallOrThrow("/api/search/embeddings/reindex", {
|
|
196
|
+
method: "POST",
|
|
197
|
+
headers: { "content-type": "application/json" },
|
|
198
|
+
body: JSON.stringify({
|
|
199
|
+
entityId,
|
|
200
|
+
purgeFirst: action === "purge"
|
|
201
|
+
})
|
|
202
|
+
}, { errorMessage });
|
|
205
203
|
} catch (err) {
|
|
206
204
|
console.error("query_index.table.vectorAction", err);
|
|
207
205
|
if (typeof window !== "undefined") {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/components/QueryIndexesTable.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\n\ntype Translator = (key: string, params?: Record<string, string | number>) => string\n\ntype PartitionStatus = {\n partitionIndex: number | null\n partitionCount: number | null\n status: 'reindexing' | 'purging' | 'stalled' | 'completed'\n processedCount?: number | null\n totalCount?: number | null\n heartbeatAt?: string | null\n startedAt?: string | null\n finishedAt?: string | null\n}\n\ntype JobStatus = {\n status: 'idle' | 'reindexing' | 'purging' | 'stalled'\n startedAt?: string | null\n finishedAt?: string | null\n heartbeatAt?: string | null\n processedCount?: number | null\n totalCount?: number | null\n partitions?: PartitionStatus[]\n scope?: {\n status?: 'reindexing' | 'purging' | 'stalled' | 'completed' | null\n processedCount?: number | null\n totalCount?: number | null\n } | null\n}\n\ntype Row = {\n entityId: string\n label: string\n baseCount: number | null\n indexCount: number | null\n vectorCount: number | null\n vectorEnabled: boolean\n fulltextCount: number | null\n fulltextEnabled: boolean\n ok: boolean\n job?: JobStatus\n}\n\ntype Resp = { items: Row[] }\n\nfunction formatCount(value: number | null): string {\n if (value == null) return '\u2014'\n return value.toLocaleString()\n}\n\nfunction formatNumeric(value: number | null | undefined): string | null {\n if (value == null) return null\n return Number(value).toLocaleString()\n}\n\nfunction formatProgressLabel(\n processed: number | null | undefined,\n total: number | null | undefined,\n t: Translator,\n): string | null {\n const processedText = formatNumeric(processed)\n if (!processedText) return null\n const totalText = formatNumeric(total)\n if (totalText) return t('query_index.table.status.progress', { processed: processedText, total: totalText })\n return t('query_index.table.status.progressSingle', { processed: processedText })\n}\n\nfunction translateJobStatus(t: Translator, status: JobStatus['status'] | undefined, ok: boolean): string {\n if (!status || status === 'idle') {\n return ok ? t('query_index.table.status.in_sync') : t('query_index.table.status.out_of_sync')\n }\n if (status === 'reindexing') return t('query_index.table.status.reindexing')\n if (status === 'purging') return t('query_index.table.status.purging')\n if (status === 'stalled') return t('query_index.table.status.stalled')\n return ok ? t('query_index.table.status.in_sync') : t('query_index.table.status.out_of_sync')\n}\n\nfunction translateScopeStatus(\n t: Translator,\n status: PartitionStatus['status'] | JobStatus['status'] | undefined | null,\n): string {\n if (status === 'reindexing') return t('query_index.table.status.scope.reindexing')\n if (status === 'purging') return t('query_index.table.status.scope.purging')\n if (status === 'stalled') return t('query_index.table.status.scope.stalled')\n return t('query_index.table.status.scope.completed')\n}\n\n\nfunction createColumns(t: Translator): ColumnDef<Row>[] {\n return [\n { id: 'entityId', header: () => t('query_index.table.columns.entity'), accessorKey: 'entityId', meta: { priority: 1 } },\n { id: 'label', header: () => t('query_index.table.columns.label'), accessorKey: 'label', meta: { priority: 2 } },\n {\n id: 'baseCount',\n header: () => t('query_index.table.columns.records'),\n accessorFn: (row) => row.baseCount ?? 0,\n cell: ({ row }) => <span>{formatCount(row.original.baseCount)}</span>,\n meta: { priority: 2 },\n },\n {\n id: 'indexCount',\n header: () => t('query_index.table.columns.indexed'),\n accessorFn: (row) => row.indexCount ?? 0,\n cell: ({ row }) => <span>{formatCount(row.original.indexCount)}</span>,\n meta: { priority: 2 },\n },\n {\n id: 'vectorCount',\n header: () => t('query_index.table.columns.vector'),\n accessorFn: (row) => (row.vectorEnabled ? row.vectorCount ?? 0 : -1),\n cell: ({ row }) => {\n const record = row.original\n if (!record.vectorEnabled) return <span>\u2014</span>\n const ok = record.vectorCount != null && record.baseCount != null && record.vectorCount === record.baseCount\n const display = formatCount(record.vectorCount)\n const className = ok ? 'text-green-600' : 'text-orange-600'\n return <span className={className}>{display}</span>\n },\n meta: { priority: 2 },\n },\n {\n id: 'fulltextCount',\n header: () => t('query_index.table.columns.fulltext'),\n accessorFn: (row) => (row.fulltextEnabled ? row.fulltextCount ?? 0 : -1),\n cell: ({ row }) => {\n const record = row.original\n if (!record.fulltextEnabled) return <span>\u2014</span>\n const ok = record.fulltextCount != null && record.baseCount != null && record.fulltextCount === record.baseCount\n const display = formatCount(record.fulltextCount)\n const className = ok ? 'text-green-600' : 'text-orange-600'\n return <span className={className}>{display}</span>\n },\n meta: { priority: 2 },\n },\n {\n id: 'status',\n header: () => t('query_index.table.columns.status'),\n cell: ({ row }) => {\n const record = row.original\n const job = record.job\n const partitions = job?.partitions ?? []\n const ok = record.ok && (!job || job.status === 'idle')\n const statusText = translateJobStatus(t, job?.status, ok)\n const jobProgress = job ? formatProgressLabel(job.processedCount ?? null, job.totalCount ?? null, t) : null\n const label = jobProgress\n ? t('query_index.table.status.withProgress', { status: statusText, progress: jobProgress })\n : statusText\n const className = job\n ? job.status === 'stalled'\n ? 'text-red-600'\n : job.status === 'reindexing' || job.status === 'purging'\n ? 'text-orange-600'\n : ok\n ? 'text-green-600'\n : 'text-muted-foreground'\n : ok\n ? 'text-green-600'\n : 'text-muted-foreground'\n\n const lines: string[] = []\n\n if (job?.scope && partitions.length <= 1) {\n const scopeStatus = translateScopeStatus(t, job.scope.status ?? null)\n const scopeProgress = formatProgressLabel(job.scope.processedCount ?? null, job.scope.totalCount ?? null, t)\n const scopeLabel = t('query_index.table.status.scopeLabel')\n lines.push(`${scopeLabel}: ${scopeStatus}${scopeProgress ? ` (${scopeProgress})` : ''}`)\n }\n\n if (partitions.length > 1) {\n for (const part of partitions) {\n const partitionLabel =\n part.partitionIndex != null\n ? t('query_index.table.status.partitionLabel', { index: Number(part.partitionIndex) + 1 })\n : t('query_index.table.status.scopeLabel')\n const partitionStatus = translateScopeStatus(t, part.status)\n const partitionProgress = formatProgressLabel(part.processedCount ?? null, part.totalCount ?? null, t)\n lines.push(`${partitionLabel}: ${partitionStatus}${partitionProgress ? ` (${partitionProgress})` : ''}`)\n }\n }\n\n if (record.vectorEnabled) {\n const vectorLabel = t('query_index.table.status.vectorLabel')\n const vectorCount = formatCount(record.vectorCount)\n const vectorTotal = record.baseCount != null ? formatCount(record.baseCount) : null\n const vectorValue = vectorTotal\n ? t('query_index.table.status.vectorValue', { count: vectorCount, total: vectorTotal })\n : vectorCount\n lines.push(`${vectorLabel}: ${vectorValue}`)\n }\n\n return (\n <div className=\"space-y-1\">\n <span className={className}>{label}</span>\n {lines.length > 0 && (\n <div className=\"text-xs text-muted-foreground\">\n {lines.map((line, idx) => (\n <div key={idx}>{line}</div>\n ))}\n </div>\n )}\n </div>\n )\n },\n meta: { priority: 1 },\n },\n ]\n}\n\nexport default function QueryIndexesTable() {\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'entityId', desc: false }])\n const [page, setPage] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const qc = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n const [refreshSeq, setRefreshSeq] = React.useState(0)\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const columns = React.useMemo(() => createColumns(t), [t])\n\n const { data, isLoading } = useQuery<Resp>({\n queryKey: ['query-index-status', scopeVersion, refreshSeq],\n queryFn: async () => {\n const baseUrl = '/api/query_index/status'\n const url = refreshSeq > 0 ? `${baseUrl}?refresh=${refreshSeq}` : baseUrl\n return readApiResultOrThrow<Resp>(\n url,\n undefined,\n { errorMessage: t('query_index.table.errors.loadFailed') },\n )\n },\n refetchInterval: 4000,\n })\n\n const rowsAll = data?.items || []\n const rows = React.useMemo(() => {\n if (!search) return rowsAll\n const q = search.toLowerCase()\n return rowsAll.filter((r) => r.entityId.toLowerCase().includes(q) || r.label.toLowerCase().includes(q))\n }, [rowsAll, search])\n\n const trigger = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string, opts?: { force?: boolean }) => {\n const body: Record<string, unknown> = { entityType: entityId }\n if (opts?.force) body.force = true\n const actionLabel =\n action === 'purge' ? t('query_index.table.actions.purge') : t('query_index.table.actions.reindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await apiCallOrThrow(`/api/query_index/${action}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n }, { errorMessage })\n } catch (err) {\n console.error('query_index.table.trigger', err)\n if (typeof window !== 'undefined') {\n const message = err instanceof Error ? err.message : errorMessage\n window.alert(message)\n }\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [qc, t],\n )\n\n const triggerVector = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string) => {\n if (action === 'purge') {\n const confirmed = await confirm({\n title: t('query_index.table.confirm.vectorPurge'),\n variant: 'destructive',\n })\n if (!confirmed) return\n }\n\n const actionLabel = action === 'purge'\n ? t('query_index.table.actions.vectorPurge')\n : t('query_index.table.actions.vectorReindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n if (action === 'reindex') {\n await apiCallOrThrow('/api/vector/reindex', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ entityId }),\n }, { errorMessage })\n } else {\n const url = `/api/vector/index?entityId=${encodeURIComponent(entityId)}`\n await apiCallOrThrow(url, { method: 'DELETE' }, { errorMessage })\n }\n } catch (err) {\n console.error('query_index.table.vectorAction', err)\n if (typeof window !== 'undefined') {\n const message = err instanceof Error ? err.message : errorMessage\n window.alert(message)\n }\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [confirm, qc, t],\n )\n\n const triggerFulltext = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string) => {\n if (action === 'purge') {\n const confirmed = await confirm({\n title: t('query_index.table.confirm.fulltextPurge'),\n variant: 'destructive',\n })\n if (!confirmed) return\n }\n\n const actionLabel = action === 'purge'\n ? t('query_index.table.actions.fulltextPurge')\n : t('query_index.table.actions.fulltextReindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await apiCallOrThrow('/api/search/reindex', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n action: action === 'purge' ? 'clear' : 'reindex',\n entityId,\n }),\n }, { errorMessage })\n } catch (err) {\n console.error('query_index.table.fulltextAction', err)\n if (typeof window !== 'undefined') {\n const message = err instanceof Error ? err.message : errorMessage\n window.alert(message)\n }\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [confirm, qc, t],\n )\n\n return (\n <>\n <DataTable\n title={t('query_index.nav.queryIndexes')}\n actions={(\n <>\n <Button\n variant=\"outline\"\n onClick={() => {\n setRefreshSeq((v) => v + 1)\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n }}\n >\n {t('query_index.table.refresh')}\n </Button>\n </>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n searchPlaceholder={t('query_index.table.searchPlaceholder')}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n sortable\n sorting={sorting}\n onSortingChange={setSorting}\n perspective={{ tableId: 'query_index.status.list' }}\n rowActions={(row) => {\n const items: Array<{ id: string; label: string; onSelect: () => void; destructive?: boolean }> = [\n { id: 'reindex', label: t('query_index.table.actions.reindex'), onSelect: () => void trigger('reindex', row.entityId) },\n {\n id: 'reindex-force',\n label: t('query_index.table.actions.reindexForce'),\n onSelect: () => void trigger('reindex', row.entityId, { force: true }),\n },\n {\n id: 'purge',\n label: t('query_index.table.actions.purge'),\n destructive: true,\n onSelect: () => void trigger('purge', row.entityId),\n },\n ]\n\n if (row.vectorEnabled) {\n items.push(\n {\n id: 'vector-reindex',\n label: t('query_index.table.actions.vectorReindex'),\n onSelect: () => void triggerVector('reindex', row.entityId),\n },\n {\n id: 'vector-purge',\n label: t('query_index.table.actions.vectorPurge'),\n destructive: true,\n onSelect: () => void triggerVector('purge', row.entityId),\n },\n )\n }\n\n if (row.fulltextEnabled) {\n items.push(\n {\n id: 'fulltext-reindex',\n label: t('query_index.table.actions.fulltextReindex'),\n onSelect: () => void triggerFulltext('reindex', row.entityId),\n },\n {\n id: 'fulltext-purge',\n label: t('query_index.table.actions.fulltextPurge'),\n destructive: true,\n onSelect: () => void triggerFulltext('purge', row.entityId),\n },\n )\n }\n\n return <RowActions items={items} />\n }}\n pagination={{ page, pageSize: 50, total: rows.length, totalPages: 1, onPageChange: setPage }}\n isLoading={isLoading}\n />\n {ConfirmDialogElement}\n </>\n )\n}\n"],
|
|
5
|
-
"mappings": ";AA0GyB,
|
|
4
|
+
"sourcesContent": ["\"use client\"\nimport * as React from 'react'\nimport { useQuery, useQueryClient } from '@tanstack/react-query'\nimport type { ColumnDef, SortingState } from '@tanstack/react-table'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { RowActions } from '@open-mercato/ui/backend/RowActions'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { apiCallOrThrow, readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'\n\ntype Translator = (key: string, params?: Record<string, string | number>) => string\n\ntype PartitionStatus = {\n partitionIndex: number | null\n partitionCount: number | null\n status: 'reindexing' | 'purging' | 'stalled' | 'completed'\n processedCount?: number | null\n totalCount?: number | null\n heartbeatAt?: string | null\n startedAt?: string | null\n finishedAt?: string | null\n}\n\ntype JobStatus = {\n status: 'idle' | 'reindexing' | 'purging' | 'stalled'\n startedAt?: string | null\n finishedAt?: string | null\n heartbeatAt?: string | null\n processedCount?: number | null\n totalCount?: number | null\n partitions?: PartitionStatus[]\n scope?: {\n status?: 'reindexing' | 'purging' | 'stalled' | 'completed' | null\n processedCount?: number | null\n totalCount?: number | null\n } | null\n}\n\ntype Row = {\n entityId: string\n label: string\n baseCount: number | null\n indexCount: number | null\n vectorCount: number | null\n vectorEnabled: boolean\n fulltextCount: number | null\n fulltextEnabled: boolean\n ok: boolean\n job?: JobStatus\n}\n\ntype Resp = { items: Row[] }\n\nfunction formatCount(value: number | null): string {\n if (value == null) return '\u2014'\n return value.toLocaleString()\n}\n\nfunction formatNumeric(value: number | null | undefined): string | null {\n if (value == null) return null\n return Number(value).toLocaleString()\n}\n\nfunction formatProgressLabel(\n processed: number | null | undefined,\n total: number | null | undefined,\n t: Translator,\n): string | null {\n const processedText = formatNumeric(processed)\n if (!processedText) return null\n const totalText = formatNumeric(total)\n if (totalText) return t('query_index.table.status.progress', { processed: processedText, total: totalText })\n return t('query_index.table.status.progressSingle', { processed: processedText })\n}\n\nfunction translateJobStatus(t: Translator, status: JobStatus['status'] | undefined, ok: boolean): string {\n if (!status || status === 'idle') {\n return ok ? t('query_index.table.status.in_sync') : t('query_index.table.status.out_of_sync')\n }\n if (status === 'reindexing') return t('query_index.table.status.reindexing')\n if (status === 'purging') return t('query_index.table.status.purging')\n if (status === 'stalled') return t('query_index.table.status.stalled')\n return ok ? t('query_index.table.status.in_sync') : t('query_index.table.status.out_of_sync')\n}\n\nfunction translateScopeStatus(\n t: Translator,\n status: PartitionStatus['status'] | JobStatus['status'] | undefined | null,\n): string {\n if (status === 'reindexing') return t('query_index.table.status.scope.reindexing')\n if (status === 'purging') return t('query_index.table.status.scope.purging')\n if (status === 'stalled') return t('query_index.table.status.scope.stalled')\n return t('query_index.table.status.scope.completed')\n}\n\n\nfunction createColumns(t: Translator): ColumnDef<Row>[] {\n return [\n { id: 'entityId', header: () => t('query_index.table.columns.entity'), accessorKey: 'entityId', meta: { priority: 1 } },\n { id: 'label', header: () => t('query_index.table.columns.label'), accessorKey: 'label', meta: { priority: 2 } },\n {\n id: 'baseCount',\n header: () => t('query_index.table.columns.records'),\n accessorFn: (row) => row.baseCount ?? 0,\n cell: ({ row }) => <span>{formatCount(row.original.baseCount)}</span>,\n meta: { priority: 2 },\n },\n {\n id: 'indexCount',\n header: () => t('query_index.table.columns.indexed'),\n accessorFn: (row) => row.indexCount ?? 0,\n cell: ({ row }) => <span>{formatCount(row.original.indexCount)}</span>,\n meta: { priority: 2 },\n },\n {\n id: 'vectorCount',\n header: () => t('query_index.table.columns.vector'),\n accessorFn: (row) => (row.vectorEnabled ? row.vectorCount ?? 0 : -1),\n cell: ({ row }) => {\n const record = row.original\n if (!record.vectorEnabled) return <span>\u2014</span>\n const ok = record.vectorCount != null && record.baseCount != null && record.vectorCount === record.baseCount\n const display = formatCount(record.vectorCount)\n const className = ok ? 'text-green-600' : 'text-orange-600'\n return <span className={className}>{display}</span>\n },\n meta: { priority: 2 },\n },\n {\n id: 'fulltextCount',\n header: () => t('query_index.table.columns.fulltext'),\n accessorFn: (row) => (row.fulltextEnabled ? row.fulltextCount ?? 0 : -1),\n cell: ({ row }) => {\n const record = row.original\n if (!record.fulltextEnabled) return <span>\u2014</span>\n const ok = record.fulltextCount != null && record.baseCount != null && record.fulltextCount === record.baseCount\n const display = formatCount(record.fulltextCount)\n const className = ok ? 'text-green-600' : 'text-orange-600'\n return <span className={className}>{display}</span>\n },\n meta: { priority: 2 },\n },\n {\n id: 'status',\n header: () => t('query_index.table.columns.status'),\n cell: ({ row }) => {\n const record = row.original\n const job = record.job\n const partitions = job?.partitions ?? []\n const ok = record.ok && (!job || job.status === 'idle')\n const statusText = translateJobStatus(t, job?.status, ok)\n const jobProgress = job ? formatProgressLabel(job.processedCount ?? null, job.totalCount ?? null, t) : null\n const label = jobProgress\n ? t('query_index.table.status.withProgress', { status: statusText, progress: jobProgress })\n : statusText\n const className = job\n ? job.status === 'stalled'\n ? 'text-red-600'\n : job.status === 'reindexing' || job.status === 'purging'\n ? 'text-orange-600'\n : ok\n ? 'text-green-600'\n : 'text-muted-foreground'\n : ok\n ? 'text-green-600'\n : 'text-muted-foreground'\n\n const lines: string[] = []\n\n if (job?.scope && partitions.length <= 1) {\n const scopeStatus = translateScopeStatus(t, job.scope.status ?? null)\n const scopeProgress = formatProgressLabel(job.scope.processedCount ?? null, job.scope.totalCount ?? null, t)\n const scopeLabel = t('query_index.table.status.scopeLabel')\n lines.push(`${scopeLabel}: ${scopeStatus}${scopeProgress ? ` (${scopeProgress})` : ''}`)\n }\n\n if (partitions.length > 1) {\n for (const part of partitions) {\n const partitionLabel =\n part.partitionIndex != null\n ? t('query_index.table.status.partitionLabel', { index: Number(part.partitionIndex) + 1 })\n : t('query_index.table.status.scopeLabel')\n const partitionStatus = translateScopeStatus(t, part.status)\n const partitionProgress = formatProgressLabel(part.processedCount ?? null, part.totalCount ?? null, t)\n lines.push(`${partitionLabel}: ${partitionStatus}${partitionProgress ? ` (${partitionProgress})` : ''}`)\n }\n }\n\n if (record.vectorEnabled) {\n const vectorLabel = t('query_index.table.status.vectorLabel')\n const vectorCount = formatCount(record.vectorCount)\n const vectorTotal = record.baseCount != null ? formatCount(record.baseCount) : null\n const vectorValue = vectorTotal\n ? t('query_index.table.status.vectorValue', { count: vectorCount, total: vectorTotal })\n : vectorCount\n lines.push(`${vectorLabel}: ${vectorValue}`)\n }\n\n return (\n <div className=\"space-y-1\">\n <span className={className}>{label}</span>\n {lines.length > 0 && (\n <div className=\"text-xs text-muted-foreground\">\n {lines.map((line, idx) => (\n <div key={idx}>{line}</div>\n ))}\n </div>\n )}\n </div>\n )\n },\n meta: { priority: 1 },\n },\n ]\n}\n\nexport default function QueryIndexesTable() {\n const [sorting, setSorting] = React.useState<SortingState>([{ id: 'entityId', desc: false }])\n const [page, setPage] = React.useState(1)\n const [search, setSearch] = React.useState('')\n const qc = useQueryClient()\n const scopeVersion = useOrganizationScopeVersion()\n const [refreshSeq, setRefreshSeq] = React.useState(0)\n const t = useT()\n const { confirm, ConfirmDialogElement } = useConfirmDialog()\n const columns = React.useMemo(() => createColumns(t), [t])\n\n const { data, isLoading } = useQuery<Resp>({\n queryKey: ['query-index-status', scopeVersion, refreshSeq],\n queryFn: async () => {\n const baseUrl = '/api/query_index/status'\n const url = refreshSeq > 0 ? `${baseUrl}?refresh=${refreshSeq}` : baseUrl\n return readApiResultOrThrow<Resp>(\n url,\n undefined,\n { errorMessage: t('query_index.table.errors.loadFailed') },\n )\n },\n refetchInterval: 4000,\n })\n\n const rowsAll = data?.items || []\n const rows = React.useMemo(() => {\n if (!search) return rowsAll\n const q = search.toLowerCase()\n return rowsAll.filter((r) => r.entityId.toLowerCase().includes(q) || r.label.toLowerCase().includes(q))\n }, [rowsAll, search])\n\n const trigger = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string, opts?: { force?: boolean }) => {\n const body: Record<string, unknown> = { entityType: entityId }\n if (opts?.force) body.force = true\n const actionLabel =\n action === 'purge' ? t('query_index.table.actions.purge') : t('query_index.table.actions.reindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await apiCallOrThrow(`/api/query_index/${action}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n }, { errorMessage })\n } catch (err) {\n console.error('query_index.table.trigger', err)\n if (typeof window !== 'undefined') {\n const message = err instanceof Error ? err.message : errorMessage\n window.alert(message)\n }\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [qc, t],\n )\n\n const triggerVector = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string) => {\n if (action === 'purge') {\n const confirmed = await confirm({\n title: t('query_index.table.confirm.vectorPurge'),\n variant: 'destructive',\n })\n if (!confirmed) return\n }\n\n const actionLabel = action === 'purge'\n ? t('query_index.table.actions.vectorPurge')\n : t('query_index.table.actions.vectorReindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await apiCallOrThrow('/api/search/embeddings/reindex', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n entityId,\n purgeFirst: action === 'purge',\n }),\n }, { errorMessage })\n } catch (err) {\n console.error('query_index.table.vectorAction', err)\n if (typeof window !== 'undefined') {\n const message = err instanceof Error ? err.message : errorMessage\n window.alert(message)\n }\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [confirm, qc, t],\n )\n\n const triggerFulltext = React.useCallback(\n async (action: 'reindex' | 'purge', entityId: string) => {\n if (action === 'purge') {\n const confirmed = await confirm({\n title: t('query_index.table.confirm.fulltextPurge'),\n variant: 'destructive',\n })\n if (!confirmed) return\n }\n\n const actionLabel = action === 'purge'\n ? t('query_index.table.actions.fulltextPurge')\n : t('query_index.table.actions.fulltextReindex')\n const errorMessage = t('query_index.table.errors.actionFailed', { action: actionLabel })\n try {\n await apiCallOrThrow('/api/search/reindex', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n action: action === 'purge' ? 'clear' : 'reindex',\n entityId,\n }),\n }, { errorMessage })\n } catch (err) {\n console.error('query_index.table.fulltextAction', err)\n if (typeof window !== 'undefined') {\n const message = err instanceof Error ? err.message : errorMessage\n window.alert(message)\n }\n }\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n },\n [confirm, qc, t],\n )\n\n return (\n <>\n <DataTable\n title={t('query_index.nav.queryIndexes')}\n actions={(\n <>\n <Button\n variant=\"outline\"\n onClick={() => {\n setRefreshSeq((v) => v + 1)\n qc.invalidateQueries({ queryKey: ['query-index-status'] })\n }}\n >\n {t('query_index.table.refresh')}\n </Button>\n </>\n )}\n columns={columns}\n data={rows}\n searchValue={search}\n searchPlaceholder={t('query_index.table.searchPlaceholder')}\n onSearchChange={(value) => {\n setSearch(value)\n setPage(1)\n }}\n sortable\n sorting={sorting}\n onSortingChange={setSorting}\n perspective={{ tableId: 'query_index.status.list' }}\n rowActions={(row) => {\n const items: Array<{ id: string; label: string; onSelect: () => void; destructive?: boolean }> = [\n { id: 'reindex', label: t('query_index.table.actions.reindex'), onSelect: () => void trigger('reindex', row.entityId) },\n {\n id: 'reindex-force',\n label: t('query_index.table.actions.reindexForce'),\n onSelect: () => void trigger('reindex', row.entityId, { force: true }),\n },\n {\n id: 'purge',\n label: t('query_index.table.actions.purge'),\n destructive: true,\n onSelect: () => void trigger('purge', row.entityId),\n },\n ]\n\n if (row.vectorEnabled) {\n items.push(\n {\n id: 'vector-reindex',\n label: t('query_index.table.actions.vectorReindex'),\n onSelect: () => void triggerVector('reindex', row.entityId),\n },\n {\n id: 'vector-purge',\n label: t('query_index.table.actions.vectorPurge'),\n destructive: true,\n onSelect: () => void triggerVector('purge', row.entityId),\n },\n )\n }\n\n if (row.fulltextEnabled) {\n items.push(\n {\n id: 'fulltext-reindex',\n label: t('query_index.table.actions.fulltextReindex'),\n onSelect: () => void triggerFulltext('reindex', row.entityId),\n },\n {\n id: 'fulltext-purge',\n label: t('query_index.table.actions.fulltextPurge'),\n destructive: true,\n onSelect: () => void triggerFulltext('purge', row.entityId),\n },\n )\n }\n\n return <RowActions items={items} />\n }}\n pagination={{ page, pageSize: 50, total: rows.length, totalPages: 1, onPageChange: setPage }}\n isLoading={isLoading}\n />\n {ConfirmDialogElement}\n </>\n )\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -20,6 +20,83 @@ async function handle(payload, ctx) {
|
|
|
20
20
|
const partitionCount = Number.isFinite(payload?.partitionCount) ? Math.max(1, Math.trunc(payload.partitionCount)) : void 0;
|
|
21
21
|
const partitionIndex = Number.isFinite(payload?.partitionIndex) ? Math.max(0, Math.trunc(payload.partitionIndex)) : void 0;
|
|
22
22
|
const resetCoverage = typeof payload?.resetCoverage === "boolean" ? payload.resetCoverage : void 0;
|
|
23
|
+
const requestedByUserId = typeof payload?.requestedByUserId === "string" ? payload.requestedByUserId : null;
|
|
24
|
+
const progressTenantId = typeof tenantId === "string" && tenantId.length > 0 ? tenantId : null;
|
|
25
|
+
const progressOrganizationId = typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
|
|
26
|
+
const progressPartitionIndex = Number.isFinite(partitionIndex) ? partitionIndex : null;
|
|
27
|
+
const progressPartitionCount = Number.isFinite(partitionCount) ? partitionCount : null;
|
|
28
|
+
let progressService = null;
|
|
29
|
+
let progressJobId = null;
|
|
30
|
+
let progressEnabled = false;
|
|
31
|
+
try {
|
|
32
|
+
progressService = ctx.resolve("progressService");
|
|
33
|
+
progressEnabled = progressService != null && progressTenantId != null;
|
|
34
|
+
} catch {
|
|
35
|
+
progressService = null;
|
|
36
|
+
progressEnabled = false;
|
|
37
|
+
}
|
|
38
|
+
const updateProgress = async (info, options) => {
|
|
39
|
+
if (!progressEnabled || !progressService || !progressTenantId) return;
|
|
40
|
+
const progressCtx = {
|
|
41
|
+
tenantId: progressTenantId,
|
|
42
|
+
organizationId: progressOrganizationId,
|
|
43
|
+
userId: requestedByUserId
|
|
44
|
+
};
|
|
45
|
+
const totalCount = Number.isFinite(info.total) ? Math.max(0, info.total) : 0;
|
|
46
|
+
const processedCount = Number.isFinite(info.processed) ? Math.max(0, info.processed) : 0;
|
|
47
|
+
const progressPercent = totalCount > 0 ? Math.min(100, Math.round(processedCount / totalCount * 100)) : 0;
|
|
48
|
+
try {
|
|
49
|
+
if (!progressJobId) {
|
|
50
|
+
const created = await progressService.createJob({
|
|
51
|
+
jobType: "query_index.reindex",
|
|
52
|
+
name: `Query index reindex: ${entityType}`,
|
|
53
|
+
description: progressPartitionCount && progressPartitionCount > 1 ? `Partition ${((progressPartitionIndex ?? 0) + 1).toString()} of ${progressPartitionCount.toString()}` : void 0,
|
|
54
|
+
totalCount: totalCount > 0 ? totalCount : void 0,
|
|
55
|
+
cancellable: false,
|
|
56
|
+
meta: {
|
|
57
|
+
entityType,
|
|
58
|
+
partitionIndex: progressPartitionIndex,
|
|
59
|
+
partitionCount: progressPartitionCount
|
|
60
|
+
},
|
|
61
|
+
partitionIndex: progressPartitionIndex ?? void 0,
|
|
62
|
+
partitionCount: progressPartitionCount ?? void 0
|
|
63
|
+
}, progressCtx);
|
|
64
|
+
progressJobId = created.id;
|
|
65
|
+
await progressService.startJob(progressJobId, progressCtx);
|
|
66
|
+
}
|
|
67
|
+
await progressService.updateProgress(
|
|
68
|
+
progressJobId,
|
|
69
|
+
{
|
|
70
|
+
processedCount,
|
|
71
|
+
totalCount: totalCount > 0 ? totalCount : void 0,
|
|
72
|
+
progressPercent
|
|
73
|
+
},
|
|
74
|
+
progressCtx
|
|
75
|
+
);
|
|
76
|
+
if (options?.complete) {
|
|
77
|
+
await progressService.completeJob(
|
|
78
|
+
progressJobId,
|
|
79
|
+
{
|
|
80
|
+
resultSummary: {
|
|
81
|
+
entityType,
|
|
82
|
+
processed: processedCount,
|
|
83
|
+
total: totalCount
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
progressCtx
|
|
87
|
+
);
|
|
88
|
+
} else if (options?.failed) {
|
|
89
|
+
await progressService.failJob(
|
|
90
|
+
progressJobId,
|
|
91
|
+
{
|
|
92
|
+
errorMessage: options.errorMessage ?? `Reindex failed for ${entityType}`
|
|
93
|
+
},
|
|
94
|
+
progressCtx
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
};
|
|
23
100
|
try {
|
|
24
101
|
await recordIndexerLog(
|
|
25
102
|
{ em },
|
|
@@ -50,8 +127,15 @@ async function handle(payload, ctx) {
|
|
|
50
127
|
partitionCount,
|
|
51
128
|
partitionIndex,
|
|
52
129
|
resetCoverage,
|
|
53
|
-
vectorService
|
|
130
|
+
vectorService,
|
|
131
|
+
onProgress: (info) => {
|
|
132
|
+
void updateProgress(info);
|
|
133
|
+
}
|
|
54
134
|
});
|
|
135
|
+
await updateProgress(
|
|
136
|
+
{ processed: result.processed, total: result.total },
|
|
137
|
+
{ complete: true }
|
|
138
|
+
);
|
|
55
139
|
await recordIndexerLog(
|
|
56
140
|
{ em },
|
|
57
141
|
{
|
|
@@ -70,6 +154,10 @@ async function handle(payload, ctx) {
|
|
|
70
154
|
}
|
|
71
155
|
);
|
|
72
156
|
} catch (error) {
|
|
157
|
+
await updateProgress(
|
|
158
|
+
{ processed: 0, total: 0 },
|
|
159
|
+
{ failed: true, errorMessage: error instanceof Error ? error.message : String(error) }
|
|
160
|
+
);
|
|
73
161
|
await recordIndexerLog(
|
|
74
162
|
{ em },
|
|
75
163
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/query_index/subscribers/reindex.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { reindexEntity } from '../lib/reindexer'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\n\nexport const metadata = { event: 'query_index.reindex', persistent: true }\n\nexport default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {\n const em = ctx.resolve<EntityManager>('em')\n const eventBus = ctx.resolve<any>('eventBus')\n let vectorService: VectorIndexService | null = null\n try {\n vectorService = ctx.resolve<VectorIndexService>('vectorIndexService')\n } catch {\n vectorService = null\n }\n const entityType = String(payload?.entityType || '')\n if (!entityType) return\n // Keep undefined to mean \"no filter\"; null to mean \"global-only\"\n const tenantId: string | null | undefined = payload?.tenantId\n const organizationId: string | null | undefined = payload?.organizationId\n const forceFull: boolean = Boolean(payload?.force)\n const batchSize = Number.isFinite(payload?.batchSize) ? Number(payload.batchSize) : undefined\n const partitionCount = Number.isFinite(payload?.partitionCount) ? Math.max(1, Math.trunc(payload.partitionCount)) : undefined\n const partitionIndex = Number.isFinite(payload?.partitionIndex) ? Math.max(0, Math.trunc(payload.partitionIndex)) : undefined\n const resetCoverage = typeof payload?.resetCoverage === 'boolean' ? payload.resetCoverage : undefined\n\n try {\n await recordIndexerLog(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.reindex',\n message: `Reindex started for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n force: forceFull,\n batchSize: batchSize ?? null,\n partitionCount: partitionCount ?? null,\n partitionIndex: partitionIndex ?? null,\n resetCoverage: resetCoverage ?? null,\n },\n },\n )\n const result = await reindexEntity(em, {\n entityType,\n tenantId,\n organizationId,\n force: forceFull,\n batchSize,\n eventBus,\n emitVectorizeEvents: true,\n partitionCount,\n partitionIndex,\n resetCoverage,\n vectorService,\n })\n await recordIndexerLog(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.reindex',\n message: `Reindex completed for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n processed: result.processed,\n total: result.total,\n tenantScopes: result.tenantScopes,\n scopes: result.scopes,\n },\n },\n )\n } catch (error) {\n await recordIndexerLog(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.reindex',\n level: 'warn',\n message: `Reindex failed for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n error: error instanceof Error ? error.message : String(error),\n },\n },\n ).catch(() => undefined)\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.reindex',\n error,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n payload,\n },\n )\n throw error\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,0BAA0B;AACnC,SAAS,wBAAwB;AACjC,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { recordIndexerError } from '@open-mercato/shared/lib/indexers/error-log'\nimport { recordIndexerLog } from '@open-mercato/shared/lib/indexers/status-log'\nimport { reindexEntity } from '../lib/reindexer'\nimport type { VectorIndexService } from '@open-mercato/search/vector'\nimport type { ProgressService } from '@open-mercato/core/modules/progress/lib/progressService'\n\nexport const metadata = { event: 'query_index.reindex', persistent: true }\n\nexport default async function handle(payload: any, ctx: { resolve: <T=any>(name: string) => T }) {\n const em = ctx.resolve<EntityManager>('em')\n const eventBus = ctx.resolve<any>('eventBus')\n let vectorService: VectorIndexService | null = null\n try {\n vectorService = ctx.resolve<VectorIndexService>('vectorIndexService')\n } catch {\n vectorService = null\n }\n const entityType = String(payload?.entityType || '')\n if (!entityType) return\n // Keep undefined to mean \"no filter\"; null to mean \"global-only\"\n const tenantId: string | null | undefined = payload?.tenantId\n const organizationId: string | null | undefined = payload?.organizationId\n const forceFull: boolean = Boolean(payload?.force)\n const batchSize = Number.isFinite(payload?.batchSize) ? Number(payload.batchSize) : undefined\n const partitionCount = Number.isFinite(payload?.partitionCount) ? Math.max(1, Math.trunc(payload.partitionCount)) : undefined\n const partitionIndex = Number.isFinite(payload?.partitionIndex) ? Math.max(0, Math.trunc(payload.partitionIndex)) : undefined\n const resetCoverage = typeof payload?.resetCoverage === 'boolean' ? payload.resetCoverage : undefined\n const requestedByUserId = typeof payload?.requestedByUserId === 'string' ? payload.requestedByUserId : null\n\n const progressTenantId = typeof tenantId === 'string' && tenantId.length > 0 ? tenantId : null\n const progressOrganizationId = typeof organizationId === 'string' && organizationId.length > 0 ? organizationId : null\n const progressPartitionIndex = Number.isFinite(partitionIndex) ? partitionIndex : null\n const progressPartitionCount = Number.isFinite(partitionCount) ? partitionCount : null\n let progressService: ProgressService | null = null\n let progressJobId: string | null = null\n let progressEnabled = false\n try {\n progressService = ctx.resolve<ProgressService>('progressService')\n progressEnabled = progressService != null && progressTenantId != null\n } catch {\n progressService = null\n progressEnabled = false\n }\n\n const updateProgress = async (\n info: { processed: number; total: number },\n options?: { complete?: boolean; failed?: boolean; errorMessage?: string },\n ): Promise<void> => {\n if (!progressEnabled || !progressService || !progressTenantId) return\n const progressCtx = {\n tenantId: progressTenantId,\n organizationId: progressOrganizationId,\n userId: requestedByUserId,\n }\n const totalCount = Number.isFinite(info.total) ? Math.max(0, info.total) : 0\n const processedCount = Number.isFinite(info.processed) ? Math.max(0, info.processed) : 0\n const progressPercent = totalCount > 0 ? Math.min(100, Math.round((processedCount / totalCount) * 100)) : 0\n try {\n if (!progressJobId) {\n const created = await progressService.createJob({\n jobType: 'query_index.reindex',\n name: `Query index reindex: ${entityType}`,\n description: progressPartitionCount && progressPartitionCount > 1\n ? `Partition ${((progressPartitionIndex ?? 0) + 1).toString()} of ${progressPartitionCount.toString()}`\n : undefined,\n totalCount: totalCount > 0 ? totalCount : undefined,\n cancellable: false,\n meta: {\n entityType,\n partitionIndex: progressPartitionIndex,\n partitionCount: progressPartitionCount,\n },\n partitionIndex: progressPartitionIndex ?? undefined,\n partitionCount: progressPartitionCount ?? undefined,\n }, progressCtx)\n progressJobId = created.id\n await progressService.startJob(progressJobId, progressCtx)\n }\n\n await progressService.updateProgress(\n progressJobId,\n {\n processedCount,\n totalCount: totalCount > 0 ? totalCount : undefined,\n progressPercent,\n },\n progressCtx,\n )\n\n if (options?.complete) {\n await progressService.completeJob(\n progressJobId,\n {\n resultSummary: {\n entityType,\n processed: processedCount,\n total: totalCount,\n },\n },\n progressCtx,\n )\n } else if (options?.failed) {\n await progressService.failJob(\n progressJobId,\n {\n errorMessage: options.errorMessage ?? `Reindex failed for ${entityType}`,\n },\n progressCtx,\n )\n }\n } catch {\n // Never block query_index subscriber execution because of progress tracking.\n }\n }\n\n try {\n await recordIndexerLog(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.reindex',\n message: `Reindex started for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n force: forceFull,\n batchSize: batchSize ?? null,\n partitionCount: partitionCount ?? null,\n partitionIndex: partitionIndex ?? null,\n resetCoverage: resetCoverage ?? null,\n },\n },\n )\n const result = await reindexEntity(em, {\n entityType,\n tenantId,\n organizationId,\n force: forceFull,\n batchSize,\n eventBus,\n emitVectorizeEvents: true,\n partitionCount,\n partitionIndex,\n resetCoverage,\n vectorService,\n onProgress: (info) => {\n void updateProgress(info)\n },\n })\n await updateProgress(\n { processed: result.processed, total: result.total },\n { complete: true },\n )\n await recordIndexerLog(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.reindex',\n message: `Reindex completed for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n processed: result.processed,\n total: result.total,\n tenantScopes: result.tenantScopes,\n scopes: result.scopes,\n },\n },\n )\n } catch (error) {\n await updateProgress(\n { processed: 0, total: 0 },\n { failed: true, errorMessage: error instanceof Error ? error.message : String(error) },\n )\n await recordIndexerLog(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.reindex',\n level: 'warn',\n message: `Reindex failed for ${entityType}`,\n entityType,\n tenantId: tenantId ?? null,\n organizationId: organizationId ?? null,\n details: {\n error: error instanceof Error ? error.message : String(error),\n },\n },\n ).catch(() => undefined)\n await recordIndexerError(\n { em },\n {\n source: 'query_index',\n handler: 'event:query_index.reindex',\n error,\n entityType,\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.4.6-develop-
|
|
3
|
+
"version": "0.4.6-develop-90c3eb0e8a",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.6-develop-
|
|
210
|
+
"@open-mercato/shared": "0.4.6-develop-90c3eb0e8a",
|
|
211
211
|
"@types/html-to-text": "^9.0.4",
|
|
212
212
|
"@types/semver": "^7.5.8",
|
|
213
213
|
"@xyflow/react": "^12.6.0",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Notifications Module
|
|
2
|
+
|
|
3
|
+
The notifications module provides in-app notifications and reactive client-side handling.
|
|
4
|
+
|
|
5
|
+
## SSE Delivery
|
|
6
|
+
|
|
7
|
+
Notification delivery to the UI is SSE-driven:
|
|
8
|
+
|
|
9
|
+
- Server emits `notifications.notification.created` with `clientBroadcast: true`
|
|
10
|
+
- Server emits `notifications.notification.batch_created` for fan-out operations (`createBatch`, `createForRole`, `createForFeature`)
|
|
11
|
+
- Payload includes scoped audience fields (`tenantId`, `organizationId`, `recipientUserId`) and the full `NotificationDto`
|
|
12
|
+
- UI consumes the stream through `useNotifications` (SSE-first strategy hook) and updates panel state immediately
|
|
13
|
+
- Notification handlers from `notifications.handlers.ts` (SPEC-043) are dispatched on arrival without polling
|
|
14
|
+
|
|
15
|
+
Legacy internal notification events (`notifications.created`, `notifications.read`, etc.) remain unchanged for subscribers and backward compatibility.
|
|
16
|
+
|
|
17
|
+
## Client Hooks
|
|
18
|
+
|
|
19
|
+
- `useNotifications` is the default notification state hook for `NotificationBell` and inbox pages
|
|
20
|
+
- `useNotifications` resolves to `useNotificationsSse` when `EventSource` is available
|
|
21
|
+
- `useNotificationsPoll` remains as an automatic fallback path when SSE is unavailable
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createModuleEvents } from '@open-mercato/shared/modules/events'
|
|
2
|
+
import { NOTIFICATION_SSE_EVENTS } from './lib/events'
|
|
3
|
+
|
|
4
|
+
const events = [
|
|
5
|
+
{
|
|
6
|
+
id: NOTIFICATION_SSE_EVENTS.CREATED,
|
|
7
|
+
label: 'Notification Created',
|
|
8
|
+
entity: 'notification',
|
|
9
|
+
category: 'system',
|
|
10
|
+
clientBroadcast: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: NOTIFICATION_SSE_EVENTS.BATCH_CREATED,
|
|
14
|
+
label: 'Notification Batch Created',
|
|
15
|
+
entity: 'notification',
|
|
16
|
+
category: 'system',
|
|
17
|
+
clientBroadcast: true,
|
|
18
|
+
},
|
|
19
|
+
] as const
|
|
20
|
+
|
|
21
|
+
export const eventsConfig = createModuleEvents({
|
|
22
|
+
moduleId: 'notifications',
|
|
23
|
+
events,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const emitNotificationEvent = eventsConfig.emit
|
|
27
|
+
|
|
28
|
+
export default eventsConfig
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { useRouter } from 'next/navigation'
|
|
5
5
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
6
|
-
import { NotificationPanel } from '@open-mercato/ui/backend/notifications'
|
|
7
|
-
import { useNotificationsPoll } from '@open-mercato/ui/backend/notifications'
|
|
6
|
+
import { NotificationPanel, useNotifications } from '@open-mercato/ui/backend/notifications'
|
|
8
7
|
|
|
9
8
|
export function NotificationInboxPageClient() {
|
|
10
9
|
const t = useT()
|
|
@@ -18,7 +17,7 @@ export function NotificationInboxPageClient() {
|
|
|
18
17
|
dismissUndo,
|
|
19
18
|
undoDismiss,
|
|
20
19
|
markAllRead,
|
|
21
|
-
} =
|
|
20
|
+
} = useNotifications()
|
|
22
21
|
|
|
23
22
|
return (
|
|
24
23
|
<NotificationPanel
|
|
@@ -7,6 +7,11 @@ export const NOTIFICATION_EVENTS = {
|
|
|
7
7
|
EXPIRED: 'notifications.expired',
|
|
8
8
|
} as const
|
|
9
9
|
|
|
10
|
+
export const NOTIFICATION_SSE_EVENTS = {
|
|
11
|
+
CREATED: 'notifications.notification.created',
|
|
12
|
+
BATCH_CREATED: 'notifications.notification.batch_created',
|
|
13
|
+
} as const
|
|
14
|
+
|
|
10
15
|
export type NotificationCreatedPayload = {
|
|
11
16
|
notificationId: string
|
|
12
17
|
recipientUserId: string
|
|
@@ -2,6 +2,17 @@ import type { NotificationDto } from '@open-mercato/shared/modules/notifications
|
|
|
2
2
|
import { Notification } from '../data/entities'
|
|
3
3
|
|
|
4
4
|
export function toNotificationDto(notification: Notification): NotificationDto {
|
|
5
|
+
const createdAt = notification.createdAt instanceof Date
|
|
6
|
+
? notification.createdAt
|
|
7
|
+
: (() => {
|
|
8
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
9
|
+
console.warn(
|
|
10
|
+
'[notifications] Invalid createdAt on notification entity, falling back to current time',
|
|
11
|
+
{ id: notification.id, createdAt: notification.createdAt },
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
return new Date()
|
|
15
|
+
})()
|
|
5
16
|
return {
|
|
6
17
|
id: notification.id,
|
|
7
18
|
type: notification.type,
|
|
@@ -26,7 +37,7 @@ export function toNotificationDto(notification: Notification): NotificationDto {
|
|
|
26
37
|
sourceEntityType: notification.sourceEntityType,
|
|
27
38
|
sourceEntityId: notification.sourceEntityId,
|
|
28
39
|
linkHref: notification.linkHref,
|
|
29
|
-
createdAt:
|
|
40
|
+
createdAt: createdAt.toISOString(),
|
|
30
41
|
readAt: notification.readAt?.toISOString() ?? null,
|
|
31
42
|
actionTaken: notification.actionTaken,
|
|
32
43
|
}
|
|
@@ -3,7 +3,7 @@ import type { Knex } from 'knex'
|
|
|
3
3
|
import { Notification, type NotificationStatus } from '../data/entities'
|
|
4
4
|
import type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'
|
|
5
5
|
import type { NotificationPollData } from '@open-mercato/shared/modules/notifications/types'
|
|
6
|
-
import { NOTIFICATION_EVENTS } from './events'
|
|
6
|
+
import { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from './events'
|
|
7
7
|
import {
|
|
8
8
|
buildNotificationEntity,
|
|
9
9
|
emitNotificationCreated,
|
|
@@ -75,6 +75,29 @@ function applyNotificationContent(
|
|
|
75
75
|
notification.createdAt = new Date()
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
async function emitNotificationSseEvents(
|
|
79
|
+
eventBus: { emit: (event: string, payload: unknown) => Promise<void> },
|
|
80
|
+
notifications: Notification[],
|
|
81
|
+
ctx: NotificationServiceContext,
|
|
82
|
+
recipientUserIds: string[],
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
await eventBus.emit(NOTIFICATION_SSE_EVENTS.BATCH_CREATED, {
|
|
85
|
+
tenantId: ctx.tenantId,
|
|
86
|
+
organizationId: normalizeOrgScope(ctx.organizationId),
|
|
87
|
+
recipientUserIds,
|
|
88
|
+
count: notifications.length,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
for (const notification of notifications) {
|
|
92
|
+
await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {
|
|
93
|
+
tenantId: notification.tenantId,
|
|
94
|
+
organizationId: notification.organizationId ?? null,
|
|
95
|
+
recipientUserId: notification.recipientUserId,
|
|
96
|
+
notification: toNotificationDto(notification),
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
78
101
|
async function createOrRefreshNotification(
|
|
79
102
|
em: EntityManager,
|
|
80
103
|
input: NotificationContentInput,
|
|
@@ -171,6 +194,12 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
171
194
|
})
|
|
172
195
|
|
|
173
196
|
await emitNotificationCreated(eventBus, notification, ctx)
|
|
197
|
+
await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {
|
|
198
|
+
tenantId: notification.tenantId,
|
|
199
|
+
organizationId: notification.organizationId ?? null,
|
|
200
|
+
recipientUserId: notification.recipientUserId,
|
|
201
|
+
notification: toNotificationDto(notification),
|
|
202
|
+
})
|
|
174
203
|
|
|
175
204
|
return notification
|
|
176
205
|
},
|
|
@@ -190,6 +219,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
190
219
|
})
|
|
191
220
|
|
|
192
221
|
await emitNotificationCreatedBatch(eventBus, notifications, ctx)
|
|
222
|
+
await emitNotificationSseEvents(eventBus, notifications, ctx, recipientUserIds)
|
|
193
223
|
|
|
194
224
|
return notifications
|
|
195
225
|
},
|
|
@@ -217,6 +247,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
217
247
|
})
|
|
218
248
|
|
|
219
249
|
await emitNotificationCreatedBatch(eventBus, notifications, ctx)
|
|
250
|
+
await emitNotificationSseEvents(eventBus, notifications, ctx, uniqueRecipientUserIds)
|
|
220
251
|
|
|
221
252
|
return notifications
|
|
222
253
|
},
|
|
@@ -247,6 +278,7 @@ export function createNotificationService(deps: NotificationServiceDeps): Notifi
|
|
|
247
278
|
})
|
|
248
279
|
|
|
249
280
|
await emitNotificationCreatedBatch(eventBus, notifications, ctx)
|
|
281
|
+
await emitNotificationSseEvents(eventBus, notifications, ctx, uniqueRecipientUserIds)
|
|
250
282
|
|
|
251
283
|
return notifications
|
|
252
284
|
},
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { createModuleEvents } from '@open-mercato/shared/modules/events'
|
|
2
2
|
|
|
3
3
|
export const events = [
|
|
4
|
-
{ id: 'progress.job.created', label: 'Job Created', entity: 'job', category: 'crud' },
|
|
5
|
-
{ id: 'progress.job.started', label: 'Job Started', entity: 'job', category: 'lifecycle' },
|
|
6
|
-
{ id: 'progress.job.updated', label: 'Job Updated', entity: 'job', category: 'lifecycle' },
|
|
7
|
-
{ id: 'progress.job.completed', label: 'Job Completed', entity: 'job', category: 'lifecycle' },
|
|
8
|
-
{ id: 'progress.job.failed', label: 'Job Failed', entity: 'job', category: 'lifecycle' },
|
|
9
|
-
{ id: 'progress.job.cancelled', label: 'Job Cancelled', entity: 'job', category: 'lifecycle' },
|
|
4
|
+
{ id: 'progress.job.created', label: 'Job Created', entity: 'job', category: 'crud', clientBroadcast: true },
|
|
5
|
+
{ id: 'progress.job.started', label: 'Job Started', entity: 'job', category: 'lifecycle', clientBroadcast: true },
|
|
6
|
+
{ id: 'progress.job.updated', label: 'Job Updated', entity: 'job', category: 'lifecycle', clientBroadcast: true },
|
|
7
|
+
{ id: 'progress.job.completed', label: 'Job Completed', entity: 'job', category: 'lifecycle', clientBroadcast: true },
|
|
8
|
+
{ id: 'progress.job.failed', label: 'Job Failed', entity: 'job', category: 'lifecycle', clientBroadcast: true },
|
|
9
|
+
{ id: 'progress.job.cancelled', label: 'Job Cancelled', entity: 'job', category: 'lifecycle', clientBroadcast: true },
|
|
10
10
|
] as const
|
|
11
11
|
|
|
12
12
|
export const eventsConfig = createModuleEvents({
|