@open-mercato/core 0.6.8-develop.6891.1.4dca3f1ad3 → 0.6.8-develop.6893.1.7af3b3a72d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/communication_channels/api/get/me/channels/route.js +16 -2
- package/dist/modules/communication_channels/api/get/me/channels/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js +14 -1
- package/dist/modules/communication_channels/api/post/channels/[id]/poll-now/route.js.map +2 -2
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js +24 -11
- package/dist/modules/communication_channels/backend/profile/communication-channels/page.js.map +2 -2
- package/dist/modules/communication_channels/lib/polling-eligibility.js +8 -0
- package/dist/modules/communication_channels/lib/polling-eligibility.js.map +7 -0
- package/dist/modules/communication_channels/workers/poll-channel.js +2 -2
- package/dist/modules/communication_channels/workers/poll-channel.js.map +2 -2
- package/dist/modules/configs/cli.js +12 -10
- package/dist/modules/configs/cli.js.map +2 -2
- package/dist/modules/configs/lib/touchGeneratedBarrels.js +2 -2
- package/dist/modules/configs/lib/touchGeneratedBarrels.js.map +2 -2
- package/dist/modules/customers/message-objects.js +1 -1
- package/dist/modules/customers/message-objects.js.map +1 -1
- package/package.json +8 -8
- package/src/modules/communication_channels/api/get/me/channels/route.ts +24 -2
- package/src/modules/communication_channels/api/post/channels/[id]/poll-now/route.ts +22 -1
- package/src/modules/communication_channels/backend/profile/communication-channels/page.tsx +58 -13
- package/src/modules/communication_channels/i18n/de.json +6 -0
- package/src/modules/communication_channels/i18n/en.json +6 -0
- package/src/modules/communication_channels/i18n/es.json +6 -0
- package/src/modules/communication_channels/i18n/ko.json +6 -0
- package/src/modules/communication_channels/i18n/pl.json +6 -0
- package/src/modules/communication_channels/lib/polling-eligibility.ts +14 -0
- package/src/modules/communication_channels/workers/poll-channel.ts +3 -3
- package/src/modules/configs/cli.ts +12 -10
- package/src/modules/configs/lib/touchGeneratedBarrels.ts +5 -2
- package/src/modules/customers/message-objects.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/communication_channels/workers/poll-channel.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\nimport {\n COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n type IngestInboundMessageInput,\n} from '../commands/ingest-inbound-message'\nimport { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'\nimport { preservePushState } from '../lib/push-state'\nimport { writeIngestDeadLetter } from '../lib/dead-letter'\nimport { classifyOutboundError, computeBackoffMs, isReauthError } from '../lib/error-classification'\nimport { refreshCredentialsIfNeeded } from '../lib/credential-refresh'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type { NormalizedInboundMessage } from '../lib/adapter'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'poll-channel' })\n\n/**\n * Job payload for the `communication-channels-poll` queue.\n *\n * One job per channel per scheduler tick. The poll-tick worker (sibling file)\n * enumerates due channels and enqueues these jobs.\n */\nexport type PollChannelJobPayload = {\n channelId: string\n scope: {\n tenantId: string\n organizationId: string | null\n }\n /** Attempt count, 1-based; used for retry-backoff decisions. */\n attempt?: number\n /** Self-re-enqueue drain counter (bounds the multi-page `hasMore` drain loop). */\n drainPage?: number\n}\n\nexport const POLL_CHANNEL_MAX_ATTEMPTS = 3\n\n/**\n * Hard cap on `hasMore` self-re-enqueue drain pages \u2014 guards against an adapter\n * that returns `hasMore: true` with a non-advancing (pinned) cursor, which would\n * otherwise spin a tight, unthrottled re-enqueue loop. Mirrors the same guard in\n * `gmail-history-sync`.\n */\nconst MAX_DRAIN_PAGES = 100\n\nexport const metadata: WorkerMeta = {\n queue: COMMUNICATION_CHANNELS_QUEUES.poll,\n id: 'communication_channels:poll-channel',\n concurrency: 10,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\n/**\n * Poll a single channel for inbound messages.\n *\n * Per SPEC-045d \u00A7 6 (with email-spec \u00A7 Hub Deltas \u2192 Delta 6 polling extensions):\n * 1. Load the channel; skip when `is_active === false` or `status !== 'connected'`.\n * 2. Skip when the adapter declares `realtimePush !== false` (it doesn't want polling).\n * 3. Refresh credentials if OAuth and within the expiry window.\n * 4. Call `adapter.fetchHistory({ channelId, credentials, since: lastPolledAt })`.\n * 5. For each normalized message, dispatch `ingest_inbound_message` (idempotent on\n * `(channel_id, external_message_id)`).\n * 6. Update `channel.lastPolledAt = NOW()` on success.\n * 7. On error: classify, set `channel.status` accordingly, set `last_error`, emit\n * `channel.requires_reauth` on 401, retry transient failures up to MAX.\n *\n * The hub doesn't drain a remote mailbox indefinitely \u2014 `fetchHistory` returns a\n * single page (provider decides the size). If the provider has more, the next tick\n * picks it up after the configured `poll_interval_seconds`.\n */\nexport default async function handle(\n job: QueuedJob<PollChannelJobPayload>,\n ctx: HandlerContext,\n): Promise<void> {\n const { channelId, scope, attempt = 1, drainPage = 0 } = job.payload\n const em = (ctx.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.resolve<ChannelAdapterRegistry>('channelAdapterRegistry')\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: channelId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!channel) {\n logger.warn('channel not found (skipping)', { channelId })\n return\n }\n if (!channel.isActive) return\n // Allow `connected` (normal poll) and `error` (Spec B \u00A7 B5 auto-recovery\n // sweep enqueues these intentionally \u2014 on a successful poll below we\n // flip them back to `connected`). `requires_reauth` and `disconnected`\n // are owned by the credential-refresh and disconnect flows.\n if (channel.status !== 'connected' && channel.status !== 'error') return\n\n const adapter = adapterRegistry?.get(channel.providerKey)\n if (!adapter) {\n logger.warn('no adapter for provider', { providerKey: channel.providerKey, channelId })\n return\n }\n // Adapter opted out of polling \u2014 webhook providers.\n const capabilities = (channel.capabilities as { realtimePush?: boolean } | null) ?? null\n if (capabilities?.realtimePush !== false) {\n // realtimePush is `true` (default for back-compat) \u2014 don't poll push providers.\n return\n }\n if (typeof adapter.fetchHistory !== 'function') {\n // Adapter doesn't implement history fetching \u2014 nothing we can do.\n return\n }\n\n // Credentials.\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = ctx.resolve<CredentialsServiceLike>('integrationCredentialsService')\n } catch {\n credentialsService = null\n }\n // Per-user credentials scope: pass `channel.userId` so the credentials\n // service returns this user's row, not whoever connected the provider last.\n // See review R2-C1 / N1 (2026-05-26).\n const credentialsScope = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? scope.tenantId,\n userId: channel.userId ?? null,\n }\n let credentials: Record<string, unknown> = {}\n if (channel.credentialsRef && credentialsService) {\n try {\n credentials =\n (await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope)) ?? {}\n } catch {\n credentials = {}\n }\n }\n const refreshed = await refreshCredentialsIfNeeded(\n {\n adapter,\n channelId: channel.id,\n credentials,\n scope: credentialsScope,\n },\n { credentialsService },\n )\n credentials = refreshed.credentials\n\n // Fetch a single page of history.\n // `channelState` is the provider-specific resumption cursor \u2014 Gmail historyId,\n // IMAP UIDVALIDITY+UIDNEXT, etc. We persist it across\n // ticks on `channel.channelState` so each poll resumes from the prior one\n // instead of running a full mailbox resync. Empty / NULL = \"first poll;\n // bootstrap the cursor from the provider\".\n let normalized: NormalizedInboundMessage[] = []\n let nextCursor: string | undefined\n let hasMore = false\n try {\n const result = await adapter.fetchHistory({\n conversationId: channel.externalIdentifier ?? channel.id,\n credentials,\n cursor: channel.lastPolledAt ? channel.lastPolledAt.toISOString() : undefined,\n channelState: (channel.channelState as Record<string, unknown> | null) ?? undefined,\n scope: {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? scope.tenantId,\n },\n // `contactFilter.sinceDays` is a hint to provider adapters about how far\n // back to look on first-poll bootstrap. For UID-incremental polls (every\n // tick after the first), adapters ignore this and just fetch new mail\n // since the persisted cursor.\n contactFilter: { addresses: [], sinceDays: 7 },\n })\n normalized = Array.isArray(result?.messages) ? result.messages : []\n nextCursor = result?.nextCursor\n hasMore = result?.hasMore === true\n } catch (err) {\n await handlePollError(err, em, channel, scope, attempt, job.payload)\n return\n }\n\n // Dispatch ingest commands for each message.\n const commandBus = ctx.resolve<CommandBus>('commandBus')\n const containerProxy = { resolve: ctx.resolve.bind(ctx) }\n const commandCtx = {\n container: containerProxy as never,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId ?? null,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n }\n // Spec B \u00A7 Per-message commit + dead-letter:\n // - Permanent failure (malformed MIME, schema, contract violation):\n // write to channel_ingest_dead_letter, log, advance cursor anyway so\n // the bad blob never stalls the channel again.\n // - Transient failure (DB drop, network blip): abort the loop without\n // advancing the cursor. The next tick re-fetches the same page;\n // idempotency via the (channel_id, external_message_id) unique\n // constraint means already-ingested messages no-op on the retry.\n let transientIngestAbort = false\n for (const message of normalized) {\n try {\n const input: IngestInboundMessageInput = {\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n scope: {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n },\n message,\n }\n await commandBus.execute(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {\n input,\n ctx: commandCtx as never,\n })\n } catch (err) {\n const classification = classifyOutboundError(err)\n if (classification.transient) {\n logger.warn('transient ingest failure; aborting page so cursor is not advanced', { channelId: channel.id, reason: classification.message })\n transientIngestAbort = true\n break\n }\n // Permanent \u2014 write to dead-letter so an operator can replay later.\n // The shared helper is best-effort (never throws) and idempotent on\n // `(channelId, externalMessageId)`, so a replayed page that fails the\n // same message again does not insert a duplicate row.\n await writeIngestDeadLetter({\n em,\n scope,\n channel,\n message,\n err,\n errorMessage: classification.message,\n })\n logger.warn('permanent ingest failure; recorded in dead-letter and advancing cursor', { channelId: channel.id, externalMessageId: message.externalMessageId, reason: classification.message })\n }\n }\n\n // Transient abort: keep prior cursor + lastPolledAt so the next tick\n // re-fetches the same page (idempotent at the DB layer).\n if (transientIngestAbort) {\n channel.lastError = 'transient_ingest_failure'\n await em.flush()\n return\n }\n\n // Update poll cursor + clear any stale error state.\n channel.lastPolledAt = new Date()\n if (channel.lastError) channel.lastError = null\n // Recover the channel from any prior non-fatal error state. The previous\n // poll(s) may have set status='error' after exhausting transient-retry\n // attempts, but a fresh successful poll means the upstream is healthy\n // again \u2014 flip it back to 'connected' so the scheduler keeps it in\n // rotation and the user doesn't have to manually reconnect.\n // We DON'T touch 'requires_reauth' here (that lifecycle state is owned\n // by the credential-refresh / OAuth flow) or 'disconnected' (owned by\n // the cascade-on-user-delete subscriber).\n if (channel.status === 'error') {\n channel.status = 'connected'\n }\n // Providers encode their cursor as base64-encoded JSON in `nextCursor`; we\n // decode it back to an object so the next tick can pass it straight into\n // `fetchHistory` as `channelState`. Decode failures fall back to the prior\n // state (next tick bootstraps from there).\n //\n // Push-delivery state (Spec C) \u2014 watch/subscription identifiers and expiry \u2014 is\n // owned by the push register/renew commands, not the sync cursor. A provider's\n // `fetchHistory` returns only sync-cursor fields, so persisting the decoded\n // cursor as a full replace would silently wipe push state and stop\n // `gmail-renew-watch` from renewing it. Carry\n // the hub-owned push keys forward whenever the new cursor omits them.\n if (typeof nextCursor === 'string' && nextCursor.length > 0) {\n const decoded = decodeChannelStateCursor(nextCursor)\n if (decoded) {\n channel.channelState = preservePushState(channel.channelState, decoded)\n }\n }\n await em.flush()\n\n // Drain contract (review H1, 2026-05-26): adapters that have additional\n // pages beyond this tick's batch (large mailboxes, mid-deltaLink walks,\n // UID overflows) signal `hasMore: true`. The persisted `channelState`\n // already encodes the mid-drain resumption token (Gmail `pendingHistoryPageToken`,\n // IMAP non-terminal `uidNext`). Re-enqueue with\n // a small delay so we keep draining without overrunning rate limits.\n if (hasMore) {\n // Bound the drain: an adapter that returns `hasMore: true` with a\n // non-advancing cursor (e.g. a persistently-failing message that pins the\n // Gmail cursor via `hardFailed`) must not spin an unthrottled loop. Stop\n // at MAX_DRAIN_PAGES; the next scheduled poll tick re-checks the channel.\n if (drainPage < MAX_DRAIN_PAGES) {\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)\n await queue.enqueue(\n { channelId: channel.id, scope, attempt: 1, drainPage: drainPage + 1 } as unknown as Record<string, unknown>,\n { delayMs: 250 },\n )\n } else {\n logger.warn('drain page cap reached; stopping re-enqueue until the next scheduled tick', { maxDrainPages: MAX_DRAIN_PAGES, channelId: channel.id })\n }\n }\n}\n\nfunction decodeChannelStateCursor(cursor: string): Record<string, unknown> | null {\n try {\n const decoded = Buffer.from(cursor, 'base64').toString('utf8')\n const parsed = JSON.parse(decoded) as unknown\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function handlePollError(\n err: unknown,\n em: EntityManager,\n channel: CommunicationChannel,\n scope: PollChannelJobPayload['scope'],\n attempt: number,\n payload: PollChannelJobPayload,\n): Promise<void> {\n const classification = classifyOutboundError(err)\n channel.lastError = classification.message\n\n if (isReauthError(classification)) {\n channel.status = 'requires_reauth'\n await em.flush()\n await emitCommunicationChannelsEvent(\n 'communication_channels.channel.requires_reauth',\n {\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n reason: classification.message,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n },\n { persistent: true },\n )\n return\n }\n\n if (classification.transient && attempt < POLL_CHANNEL_MAX_ATTEMPTS) {\n // Transient error \u2014 re-enqueue with backoff. Channel status stays\n // `connected` so the scheduler keeps it in rotation.\n await em.flush()\n const next: PollChannelJobPayload = { ...payload, attempt: attempt + 1 }\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)\n await queue.enqueue(next as unknown as Record<string, unknown>, {\n delayMs: computeBackoffMs(attempt),\n })\n return\n }\n\n // Permanent or attempts exhausted \u2014 mark channel as error and stop the loop.\n channel.status = 'error'\n await em.flush()\n}\n"],
|
|
5
|
-
"mappings": "AAGA,SAAS,6BAA6B;AACtC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,OAEK;AACP,SAAS,+BAA+B,qCAAqC;AAC7E,SAAS,yBAAyB;AAClC,SAAS,6BAA6B;AACtC,SAAS,uBAAuB,kBAAkB,qBAAqB;AACvE,SAAS,kCAAkC;AAC3C,SAAS,sCAAsC;AAG/C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,eAAe,CAAC;AAoBlF,MAAM,4BAA4B;AAQzC,MAAM,kBAAkB;AAEjB,MAAM,WAAuB;AAAA,EAClC,OAAO,8BAA8B;AAAA,EACrC,IAAI;AAAA,EACJ,aAAa;AACf;AAoCA,eAAO,OACL,KACA,KACe;AACf,QAAM,EAAE,WAAW,OAAO,UAAU,GAAG,YAAY,EAAE,IAAI,IAAI;AAC7D,QAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,QAAM,kBAAkB,IAAI,QAAgC,wBAAwB;AAEpF,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,MACxC,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,CAAC,SAAS;AACZ,WAAO,KAAK,gCAAgC,EAAE,UAAU,CAAC;AACzD;AAAA,EACF;AACA,MAAI,CAAC,QAAQ,SAAU;AAKvB,MAAI,QAAQ,WAAW,eAAe,QAAQ,WAAW,QAAS;AAElE,QAAM,UAAU,iBAAiB,IAAI,QAAQ,WAAW;AACxD,MAAI,CAAC,SAAS;AACZ,WAAO,KAAK,2BAA2B,EAAE,aAAa,QAAQ,aAAa,UAAU,CAAC;AACtF;AAAA,EACF;AAEA,
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport type { JobContext, QueuedJob, WorkerMeta } from '@open-mercato/queue'\nimport type { CommandBus } from '@open-mercato/shared/lib/commands'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { CommunicationChannel } from '../data/entities'\nimport {\n COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,\n type IngestInboundMessageInput,\n} from '../commands/ingest-inbound-message'\nimport { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../lib/queue'\nimport { preservePushState } from '../lib/push-state'\nimport { isHubPolledChannel } from '../lib/polling-eligibility'\nimport { writeIngestDeadLetter } from '../lib/dead-letter'\nimport { classifyOutboundError, computeBackoffMs, isReauthError } from '../lib/error-classification'\nimport { refreshCredentialsIfNeeded } from '../lib/credential-refresh'\nimport { emitCommunicationChannelsEvent } from '../events'\nimport type { ChannelAdapterRegistry } from '../lib/registry'\nimport type { NormalizedInboundMessage } from '../lib/adapter'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('communication_channels').child({ component: 'poll-channel' })\n\n/**\n * Job payload for the `communication-channels-poll` queue.\n *\n * One job per channel per scheduler tick. The poll-tick worker (sibling file)\n * enumerates due channels and enqueues these jobs.\n */\nexport type PollChannelJobPayload = {\n channelId: string\n scope: {\n tenantId: string\n organizationId: string | null\n }\n /** Attempt count, 1-based; used for retry-backoff decisions. */\n attempt?: number\n /** Self-re-enqueue drain counter (bounds the multi-page `hasMore` drain loop). */\n drainPage?: number\n}\n\nexport const POLL_CHANNEL_MAX_ATTEMPTS = 3\n\n/**\n * Hard cap on `hasMore` self-re-enqueue drain pages \u2014 guards against an adapter\n * that returns `hasMore: true` with a non-advancing (pinned) cursor, which would\n * otherwise spin a tight, unthrottled re-enqueue loop. Mirrors the same guard in\n * `gmail-history-sync`.\n */\nconst MAX_DRAIN_PAGES = 100\n\nexport const metadata: WorkerMeta = {\n queue: COMMUNICATION_CHANNELS_QUEUES.poll,\n id: 'communication_channels:poll-channel',\n concurrency: 10,\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\ntype CredentialsServiceLike = {\n resolve: (\n integrationId: string,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<Record<string, unknown> | null>\n save?: (\n integrationId: string,\n credentials: Record<string, unknown>,\n scope: { organizationId: string; tenantId: string; userId?: string | null },\n ) => Promise<void>\n}\n\n/**\n * Poll a single channel for inbound messages.\n *\n * Per SPEC-045d \u00A7 6 (with email-spec \u00A7 Hub Deltas \u2192 Delta 6 polling extensions):\n * 1. Load the channel; skip when `is_active === false` or `status !== 'connected'`.\n * 2. Skip when the adapter declares `realtimePush !== false` (it doesn't want polling).\n * 3. Refresh credentials if OAuth and within the expiry window.\n * 4. Call `adapter.fetchHistory({ channelId, credentials, since: lastPolledAt })`.\n * 5. For each normalized message, dispatch `ingest_inbound_message` (idempotent on\n * `(channel_id, external_message_id)`).\n * 6. Update `channel.lastPolledAt = NOW()` on success.\n * 7. On error: classify, set `channel.status` accordingly, set `last_error`, emit\n * `channel.requires_reauth` on 401, retry transient failures up to MAX.\n *\n * The hub doesn't drain a remote mailbox indefinitely \u2014 `fetchHistory` returns a\n * single page (provider decides the size). If the provider has more, the next tick\n * picks it up after the configured `poll_interval_seconds`.\n */\nexport default async function handle(\n job: QueuedJob<PollChannelJobPayload>,\n ctx: HandlerContext,\n): Promise<void> {\n const { channelId, scope, attempt = 1, drainPage = 0 } = job.payload\n const em = (ctx.resolve('em') as EntityManager).fork()\n const adapterRegistry = ctx.resolve<ChannelAdapterRegistry>('channelAdapterRegistry')\n\n const channel = await findOneWithDecryption(\n em,\n CommunicationChannel,\n {\n id: channelId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!channel) {\n logger.warn('channel not found (skipping)', { channelId })\n return\n }\n if (!channel.isActive) return\n // Allow `connected` (normal poll) and `error` (Spec B \u00A7 B5 auto-recovery\n // sweep enqueues these intentionally \u2014 on a successful poll below we\n // flip them back to `connected`). `requires_reauth` and `disconnected`\n // are owned by the credential-refresh and disconnect flows.\n if (channel.status !== 'connected' && channel.status !== 'error') return\n\n const adapter = adapterRegistry?.get(channel.providerKey)\n if (!adapter) {\n logger.warn('no adapter for provider', { providerKey: channel.providerKey, channelId })\n return\n }\n // Adapter opted out of polling \u2014 webhook and gateway providers.\n if (!isHubPolledChannel(channel.capabilities)) {\n // realtimePush is `true` (default for back-compat) \u2014 don't poll push providers.\n return\n }\n if (typeof adapter.fetchHistory !== 'function') {\n // Adapter doesn't implement history fetching \u2014 nothing we can do.\n return\n }\n\n // Credentials.\n let credentialsService: CredentialsServiceLike | null = null\n try {\n credentialsService = ctx.resolve<CredentialsServiceLike>('integrationCredentialsService')\n } catch {\n credentialsService = null\n }\n // Per-user credentials scope: pass `channel.userId` so the credentials\n // service returns this user's row, not whoever connected the provider last.\n // See review R2-C1 / N1 (2026-05-26).\n const credentialsScope = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? scope.tenantId,\n userId: channel.userId ?? null,\n }\n let credentials: Record<string, unknown> = {}\n if (channel.credentialsRef && credentialsService) {\n try {\n credentials =\n (await credentialsService.resolve(`channel_${channel.providerKey}`, credentialsScope)) ?? {}\n } catch {\n credentials = {}\n }\n }\n const refreshed = await refreshCredentialsIfNeeded(\n {\n adapter,\n channelId: channel.id,\n credentials,\n scope: credentialsScope,\n },\n { credentialsService },\n )\n credentials = refreshed.credentials\n\n // Fetch a single page of history.\n // `channelState` is the provider-specific resumption cursor \u2014 Gmail historyId,\n // IMAP UIDVALIDITY+UIDNEXT, etc. We persist it across\n // ticks on `channel.channelState` so each poll resumes from the prior one\n // instead of running a full mailbox resync. Empty / NULL = \"first poll;\n // bootstrap the cursor from the provider\".\n let normalized: NormalizedInboundMessage[] = []\n let nextCursor: string | undefined\n let hasMore = false\n try {\n const result = await adapter.fetchHistory({\n conversationId: channel.externalIdentifier ?? channel.id,\n credentials,\n cursor: channel.lastPolledAt ? channel.lastPolledAt.toISOString() : undefined,\n channelState: (channel.channelState as Record<string, unknown> | null) ?? undefined,\n scope: {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? scope.tenantId,\n },\n // `contactFilter.sinceDays` is a hint to provider adapters about how far\n // back to look on first-poll bootstrap. For UID-incremental polls (every\n // tick after the first), adapters ignore this and just fetch new mail\n // since the persisted cursor.\n contactFilter: { addresses: [], sinceDays: 7 },\n })\n normalized = Array.isArray(result?.messages) ? result.messages : []\n nextCursor = result?.nextCursor\n hasMore = result?.hasMore === true\n } catch (err) {\n await handlePollError(err, em, channel, scope, attempt, job.payload)\n return\n }\n\n // Dispatch ingest commands for each message.\n const commandBus = ctx.resolve<CommandBus>('commandBus')\n const containerProxy = { resolve: ctx.resolve.bind(ctx) }\n const commandCtx = {\n container: containerProxy as never,\n auth: null,\n organizationScope: null,\n selectedOrganizationId: scope.organizationId ?? null,\n organizationIds: scope.organizationId ? [scope.organizationId] : null,\n }\n // Spec B \u00A7 Per-message commit + dead-letter:\n // - Permanent failure (malformed MIME, schema, contract violation):\n // write to channel_ingest_dead_letter, log, advance cursor anyway so\n // the bad blob never stalls the channel again.\n // - Transient failure (DB drop, network blip): abort the loop without\n // advancing the cursor. The next tick re-fetches the same page;\n // idempotency via the (channel_id, external_message_id) unique\n // constraint means already-ingested messages no-op on the retry.\n let transientIngestAbort = false\n for (const message of normalized) {\n try {\n const input: IngestInboundMessageInput = {\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n scope: {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n },\n message,\n }\n await commandBus.execute(COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID, {\n input,\n ctx: commandCtx as never,\n })\n } catch (err) {\n const classification = classifyOutboundError(err)\n if (classification.transient) {\n logger.warn('transient ingest failure; aborting page so cursor is not advanced', { channelId: channel.id, reason: classification.message })\n transientIngestAbort = true\n break\n }\n // Permanent \u2014 write to dead-letter so an operator can replay later.\n // The shared helper is best-effort (never throws) and idempotent on\n // `(channelId, externalMessageId)`, so a replayed page that fails the\n // same message again does not insert a duplicate row.\n await writeIngestDeadLetter({\n em,\n scope,\n channel,\n message,\n err,\n errorMessage: classification.message,\n })\n logger.warn('permanent ingest failure; recorded in dead-letter and advancing cursor', { channelId: channel.id, externalMessageId: message.externalMessageId, reason: classification.message })\n }\n }\n\n // Transient abort: keep prior cursor + lastPolledAt so the next tick\n // re-fetches the same page (idempotent at the DB layer).\n if (transientIngestAbort) {\n channel.lastError = 'transient_ingest_failure'\n await em.flush()\n return\n }\n\n // Update poll cursor + clear any stale error state.\n channel.lastPolledAt = new Date()\n if (channel.lastError) channel.lastError = null\n // Recover the channel from any prior non-fatal error state. The previous\n // poll(s) may have set status='error' after exhausting transient-retry\n // attempts, but a fresh successful poll means the upstream is healthy\n // again \u2014 flip it back to 'connected' so the scheduler keeps it in\n // rotation and the user doesn't have to manually reconnect.\n // We DON'T touch 'requires_reauth' here (that lifecycle state is owned\n // by the credential-refresh / OAuth flow) or 'disconnected' (owned by\n // the cascade-on-user-delete subscriber).\n if (channel.status === 'error') {\n channel.status = 'connected'\n }\n // Providers encode their cursor as base64-encoded JSON in `nextCursor`; we\n // decode it back to an object so the next tick can pass it straight into\n // `fetchHistory` as `channelState`. Decode failures fall back to the prior\n // state (next tick bootstraps from there).\n //\n // Push-delivery state (Spec C) \u2014 watch/subscription identifiers and expiry \u2014 is\n // owned by the push register/renew commands, not the sync cursor. A provider's\n // `fetchHistory` returns only sync-cursor fields, so persisting the decoded\n // cursor as a full replace would silently wipe push state and stop\n // `gmail-renew-watch` from renewing it. Carry\n // the hub-owned push keys forward whenever the new cursor omits them.\n if (typeof nextCursor === 'string' && nextCursor.length > 0) {\n const decoded = decodeChannelStateCursor(nextCursor)\n if (decoded) {\n channel.channelState = preservePushState(channel.channelState, decoded)\n }\n }\n await em.flush()\n\n // Drain contract (review H1, 2026-05-26): adapters that have additional\n // pages beyond this tick's batch (large mailboxes, mid-deltaLink walks,\n // UID overflows) signal `hasMore: true`. The persisted `channelState`\n // already encodes the mid-drain resumption token (Gmail `pendingHistoryPageToken`,\n // IMAP non-terminal `uidNext`). Re-enqueue with\n // a small delay so we keep draining without overrunning rate limits.\n if (hasMore) {\n // Bound the drain: an adapter that returns `hasMore: true` with a\n // non-advancing cursor (e.g. a persistently-failing message that pins the\n // Gmail cursor via `hardFailed`) must not spin an unthrottled loop. Stop\n // at MAX_DRAIN_PAGES; the next scheduled poll tick re-checks the channel.\n if (drainPage < MAX_DRAIN_PAGES) {\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)\n await queue.enqueue(\n { channelId: channel.id, scope, attempt: 1, drainPage: drainPage + 1 } as unknown as Record<string, unknown>,\n { delayMs: 250 },\n )\n } else {\n logger.warn('drain page cap reached; stopping re-enqueue until the next scheduled tick', { maxDrainPages: MAX_DRAIN_PAGES, channelId: channel.id })\n }\n }\n}\n\nfunction decodeChannelStateCursor(cursor: string): Record<string, unknown> | null {\n try {\n const decoded = Buffer.from(cursor, 'base64').toString('utf8')\n const parsed = JSON.parse(decoded) as unknown\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return parsed as Record<string, unknown>\n }\n return null\n } catch {\n return null\n }\n}\n\nasync function handlePollError(\n err: unknown,\n em: EntityManager,\n channel: CommunicationChannel,\n scope: PollChannelJobPayload['scope'],\n attempt: number,\n payload: PollChannelJobPayload,\n): Promise<void> {\n const classification = classifyOutboundError(err)\n channel.lastError = classification.message\n\n if (isReauthError(classification)) {\n channel.status = 'requires_reauth'\n await em.flush()\n await emitCommunicationChannelsEvent(\n 'communication_channels.channel.requires_reauth',\n {\n channelId: channel.id,\n providerKey: channel.providerKey,\n channelType: channel.channelType,\n reason: classification.message,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId ?? null,\n },\n { persistent: true },\n )\n return\n }\n\n if (classification.transient && attempt < POLL_CHANNEL_MAX_ATTEMPTS) {\n // Transient error \u2014 re-enqueue with backoff. Channel status stays\n // `connected` so the scheduler keeps it in rotation.\n await em.flush()\n const next: PollChannelJobPayload = { ...payload, attempt: attempt + 1 }\n const queue = getCommunicationChannelsQueue(COMMUNICATION_CHANNELS_QUEUES.poll)\n await queue.enqueue(next as unknown as Record<string, unknown>, {\n delayMs: computeBackoffMs(attempt),\n })\n return\n }\n\n // Permanent or attempts exhausted \u2014 mark channel as error and stop the loop.\n channel.status = 'error'\n await em.flush()\n}\n"],
|
|
5
|
+
"mappings": "AAGA,SAAS,6BAA6B;AACtC,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,OAEK;AACP,SAAS,+BAA+B,qCAAqC;AAC7E,SAAS,yBAAyB;AAClC,SAAS,0BAA0B;AACnC,SAAS,6BAA6B;AACtC,SAAS,uBAAuB,kBAAkB,qBAAqB;AACvE,SAAS,kCAAkC;AAC3C,SAAS,sCAAsC;AAG/C,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,wBAAwB,EAAE,MAAM,EAAE,WAAW,eAAe,CAAC;AAoBlF,MAAM,4BAA4B;AAQzC,MAAM,kBAAkB;AAEjB,MAAM,WAAuB;AAAA,EAClC,OAAO,8BAA8B;AAAA,EACrC,IAAI;AAAA,EACJ,aAAa;AACf;AAoCA,eAAO,OACL,KACA,KACe;AACf,QAAM,EAAE,WAAW,OAAO,UAAU,GAAG,YAAY,EAAE,IAAI,IAAI;AAC7D,QAAM,KAAM,IAAI,QAAQ,IAAI,EAAoB,KAAK;AACrD,QAAM,kBAAkB,IAAI,QAAgC,wBAAwB;AAEpF,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI;AAAA,MACJ,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM,kBAAkB;AAAA,MACxC,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,MAAI,CAAC,SAAS;AACZ,WAAO,KAAK,gCAAgC,EAAE,UAAU,CAAC;AACzD;AAAA,EACF;AACA,MAAI,CAAC,QAAQ,SAAU;AAKvB,MAAI,QAAQ,WAAW,eAAe,QAAQ,WAAW,QAAS;AAElE,QAAM,UAAU,iBAAiB,IAAI,QAAQ,WAAW;AACxD,MAAI,CAAC,SAAS;AACZ,WAAO,KAAK,2BAA2B,EAAE,aAAa,QAAQ,aAAa,UAAU,CAAC;AACtF;AAAA,EACF;AAEA,MAAI,CAAC,mBAAmB,QAAQ,YAAY,GAAG;AAE7C;AAAA,EACF;AACA,MAAI,OAAO,QAAQ,iBAAiB,YAAY;AAE9C;AAAA,EACF;AAGA,MAAI,qBAAoD;AACxD,MAAI;AACF,yBAAqB,IAAI,QAAgC,+BAA+B;AAAA,EAC1F,QAAQ;AACN,yBAAqB;AAAA,EACvB;AAIA,QAAM,mBAAmB;AAAA,IACvB,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM,kBAAkB,MAAM;AAAA,IAC9C,QAAQ,QAAQ,UAAU;AAAA,EAC5B;AACA,MAAI,cAAuC,CAAC;AAC5C,MAAI,QAAQ,kBAAkB,oBAAoB;AAChD,QAAI;AACF,oBACG,MAAM,mBAAmB,QAAQ,WAAW,QAAQ,WAAW,IAAI,gBAAgB,KAAM,CAAC;AAAA,IAC/F,QAAQ;AACN,oBAAc,CAAC;AAAA,IACjB;AAAA,EACF;AACA,QAAM,YAAY,MAAM;AAAA,IACtB;AAAA,MACE;AAAA,MACA,WAAW,QAAQ;AAAA,MACnB;AAAA,MACA,OAAO;AAAA,IACT;AAAA,IACA,EAAE,mBAAmB;AAAA,EACvB;AACA,gBAAc,UAAU;AAQxB,MAAI,aAAyC,CAAC;AAC9C,MAAI;AACJ,MAAI,UAAU;AACd,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ,aAAa;AAAA,MACxC,gBAAgB,QAAQ,sBAAsB,QAAQ;AAAA,MACtD;AAAA,MACA,QAAQ,QAAQ,eAAe,QAAQ,aAAa,YAAY,IAAI;AAAA,MACpE,cAAe,QAAQ,gBAAmD;AAAA,MAC1E,OAAO;AAAA,QACL,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM,kBAAkB,MAAM;AAAA,MAChD;AAAA;AAAA;AAAA;AAAA;AAAA,MAKA,eAAe,EAAE,WAAW,CAAC,GAAG,WAAW,EAAE;AAAA,IAC/C,CAAC;AACD,iBAAa,MAAM,QAAQ,QAAQ,QAAQ,IAAI,OAAO,WAAW,CAAC;AAClE,iBAAa,QAAQ;AACrB,cAAU,QAAQ,YAAY;AAAA,EAChC,SAAS,KAAK;AACZ,UAAM,gBAAgB,KAAK,IAAI,SAAS,OAAO,SAAS,IAAI,OAAO;AACnE;AAAA,EACF;AAGA,QAAM,aAAa,IAAI,QAAoB,YAAY;AACvD,QAAM,iBAAiB,EAAE,SAAS,IAAI,QAAQ,KAAK,GAAG,EAAE;AACxD,QAAM,aAAa;AAAA,IACjB,WAAW;AAAA,IACX,MAAM;AAAA,IACN,mBAAmB;AAAA,IACnB,wBAAwB,MAAM,kBAAkB;AAAA,IAChD,iBAAiB,MAAM,iBAAiB,CAAC,MAAM,cAAc,IAAI;AAAA,EACnE;AASA,MAAI,uBAAuB;AAC3B,aAAW,WAAW,YAAY;AAChC,QAAI;AACF,YAAM,QAAmC;AAAA,QACvC,WAAW,QAAQ;AAAA,QACnB,aAAa,QAAQ;AAAA,QACrB,aAAa,QAAQ;AAAA,QACrB,OAAO;AAAA,UACL,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM,kBAAkB;AAAA,QAC1C;AAAA,QACA;AAAA,MACF;AACA,YAAM,WAAW,QAAQ,kDAAkD;AAAA,QACzE;AAAA,QACA,KAAK;AAAA,MACP,CAAC;AAAA,IACH,SAAS,KAAK;AACZ,YAAM,iBAAiB,sBAAsB,GAAG;AAChD,UAAI,eAAe,WAAW;AAC5B,eAAO,KAAK,qEAAqE,EAAE,WAAW,QAAQ,IAAI,QAAQ,eAAe,QAAQ,CAAC;AAC1I,+BAAuB;AACvB;AAAA,MACF;AAKA,YAAM,sBAAsB;AAAA,QAC1B;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,cAAc,eAAe;AAAA,MAC/B,CAAC;AACD,aAAO,KAAK,0EAA0E,EAAE,WAAW,QAAQ,IAAI,mBAAmB,QAAQ,mBAAmB,QAAQ,eAAe,QAAQ,CAAC;AAAA,IAC/L;AAAA,EACF;AAIA,MAAI,sBAAsB;AACxB,YAAQ,YAAY;AACpB,UAAM,GAAG,MAAM;AACf;AAAA,EACF;AAGA,UAAQ,eAAe,oBAAI,KAAK;AAChC,MAAI,QAAQ,UAAW,SAAQ,YAAY;AAS3C,MAAI,QAAQ,WAAW,SAAS;AAC9B,YAAQ,SAAS;AAAA,EACnB;AAYA,MAAI,OAAO,eAAe,YAAY,WAAW,SAAS,GAAG;AAC3D,UAAM,UAAU,yBAAyB,UAAU;AACnD,QAAI,SAAS;AACX,cAAQ,eAAe,kBAAkB,QAAQ,cAAc,OAAO;AAAA,IACxE;AAAA,EACF;AACA,QAAM,GAAG,MAAM;AAQf,MAAI,SAAS;AAKX,QAAI,YAAY,iBAAiB;AAC/B,YAAM,QAAQ,8BAA8B,8BAA8B,IAAI;AAC9E,YAAM,MAAM;AAAA,QACV,EAAE,WAAW,QAAQ,IAAI,OAAO,SAAS,GAAG,WAAW,YAAY,EAAE;AAAA,QACrE,EAAE,SAAS,IAAI;AAAA,MACjB;AAAA,IACF,OAAO;AACL,aAAO,KAAK,6EAA6E,EAAE,eAAe,iBAAiB,WAAW,QAAQ,GAAG,CAAC;AAAA,IACpJ;AAAA,EACF;AACF;AAEA,SAAS,yBAAyB,QAAgD;AAChF,MAAI;AACF,UAAM,UAAU,OAAO,KAAK,QAAQ,QAAQ,EAAE,SAAS,MAAM;AAC7D,UAAM,SAAS,KAAK,MAAM,OAAO;AACjC,QAAI,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GAAG;AAClE,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,gBACb,KACA,IACA,SACA,OACA,SACA,SACe;AACf,QAAM,iBAAiB,sBAAsB,GAAG;AAChD,UAAQ,YAAY,eAAe;AAEnC,MAAI,cAAc,cAAc,GAAG;AACjC,YAAQ,SAAS;AACjB,UAAM,GAAG,MAAM;AACf,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,QACE,WAAW,QAAQ;AAAA,QACnB,aAAa,QAAQ;AAAA,QACrB,aAAa,QAAQ;AAAA,QACrB,QAAQ,eAAe;AAAA,QACvB,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM,kBAAkB;AAAA,MAC1C;AAAA,MACA,EAAE,YAAY,KAAK;AAAA,IACrB;AACA;AAAA,EACF;AAEA,MAAI,eAAe,aAAa,UAAU,2BAA2B;AAGnE,UAAM,GAAG,MAAM;AACf,UAAM,OAA8B,EAAE,GAAG,SAAS,SAAS,UAAU,EAAE;AACvE,UAAM,QAAQ,8BAA8B,8BAA8B,IAAI;AAC9E,UAAM,MAAM,QAAQ,MAA4C;AAAA,MAC9D,SAAS,iBAAiB,OAAO;AAAA,IACnC,CAAC;AACD;AAAA,EACF;AAGA,UAAQ,SAAS;AACjB,QAAM,GAAG,MAAM;AACjB;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -125,12 +125,12 @@ function printCacheHelp() {
|
|
|
125
125
|
console.log(" yarn mercato configs cache purge --key <key1,key2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
|
|
126
126
|
console.log(" yarn mercato configs cache purge --id <token1,token2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
|
|
127
127
|
console.log(" yarn mercato configs cache purge --pattern <glob> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
|
|
128
|
-
console.log(" yarn mercato configs cache structural [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]");
|
|
128
|
+
console.log(" yarn mercato configs cache structural [--tenant <id> | --global | --all-tenants] [--touch-generated] [--dry-run] [--json]");
|
|
129
129
|
console.log("");
|
|
130
130
|
console.log("\u2139\uFE0F Notes:");
|
|
131
131
|
console.log(" `stats` mirrors the cache admin page segment overview for CRUD/widget caches.");
|
|
132
132
|
console.log(" `purge --id` removes every key whose name contains the provided token (for example a user id or entity id).");
|
|
133
|
-
console.log(" `structural` targets navigation/sidebar caches
|
|
133
|
+
console.log(" `structural` targets navigation/sidebar caches. Add `--touch-generated` only for explicit stale-compiler recovery.");
|
|
134
134
|
console.log(" When no scope flag is supplied, this command uses the global cache scope only.");
|
|
135
135
|
}
|
|
136
136
|
async function disposeContainer(container) {
|
|
@@ -228,14 +228,16 @@ async function runStructuralCachePurge(args) {
|
|
|
228
228
|
if (json) {
|
|
229
229
|
console.log(JSON.stringify(structuralResults, null, 2));
|
|
230
230
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
if (flagEnabled(args, "touch-generated", "touchGenerated")) {
|
|
232
|
+
const quiet = flagEnabled(args, "quiet");
|
|
233
|
+
try {
|
|
234
|
+
touchGeneratedBarrels({ quiet: quiet || json });
|
|
235
|
+
} catch (err) {
|
|
236
|
+
if (!quiet && !json) {
|
|
237
|
+
console.warn(
|
|
238
|
+
`[structural] failed to touch generated barrels: ${err.message ?? err}`
|
|
239
|
+
);
|
|
240
|
+
}
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/configs/cli.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { ModuleConfigService } from './lib/module-config-service'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, NOTIFICATIONS_DELIVERY_CONFIG_KEY } from '../notifications/lib/deliveryConfig'\nimport { Tenant } from '../directory/data/entities'\nimport {\n collectCacheStats,\n executeCachePurge,\n previewCachePurge,\n type CachePurgeRequest,\n} from './lib/cache-cli'\nimport { touchGeneratedBarrels } from './lib/touchGeneratedBarrels'\n\ntype ParsedArgs = Record<string, string | boolean>\n\nexport const STRUCTURAL_CACHE_REQUESTS: CachePurgeRequest[] = [\n { kind: 'pattern', pattern: 'nav:*' },\n { kind: 'segment', segment: 'admin-nav' },\n { kind: 'segment', segment: 'portal-nav' },\n]\n\ntype CacheScope = {\n label: string\n tenantId: string | null\n}\n\nfunction parseArgs(rest: string[]): ParsedArgs {\n const args: ParsedArgs = {}\n for (let i = 0; i < rest.length; i += 1) {\n const part = rest[i]\n if (!part?.startsWith('--')) continue\n const [rawKey, rawValue] = part.slice(2).split('=')\n if (!rawKey) continue\n if (rawValue !== undefined) {\n args[rawKey] = rawValue\n } else if (i + 1 < rest.length && !rest[i + 1]!.startsWith('--')) {\n args[rawKey] = rest[i + 1]!\n i += 1\n } else {\n args[rawKey] = true\n }\n }\n return args\n}\n\nfunction stringOption(args: ParsedArgs, ...keys: string[]): string | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (typeof raw !== 'string') continue\n const trimmed = raw.trim()\n if (trimmed.length > 0) return trimmed\n }\n return undefined\n}\n\nfunction flagEnabled(args: ParsedArgs, ...keys: string[]): boolean {\n for (const key of keys) {\n const raw = args[key]\n if (raw === undefined) continue\n if (raw === true) return true\n if (typeof raw === 'string') {\n const parsed = parseBooleanToken(raw)\n return parsed === null ? true : parsed\n }\n }\n return false\n}\n\nfunction splitListOption(raw: string | undefined): string[] {\n if (!raw) return []\n const seen = new Set<string>()\n const values: string[] = []\n for (const item of raw.split(',')) {\n const trimmed = item.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n values.push(trimmed)\n }\n return values\n}\n\nasync function resolveCacheScopes(\n em: EntityManager,\n args: ParsedArgs,\n): Promise<CacheScope[]> {\n const explicitTenantId = stringOption(args, 'tenant', 'tenantId')\n const globalOnly = flagEnabled(args, 'global')\n const allTenants = flagEnabled(args, 'all-tenants', 'allTenants')\n\n if (explicitTenantId && globalOnly) {\n throw new Error('Cannot combine `--tenant` with `--global`.')\n }\n if (explicitTenantId && allTenants) {\n throw new Error('Cannot combine `--tenant` with `--all-tenants`.')\n }\n if (globalOnly && allTenants) {\n throw new Error('Cannot combine `--global` with `--all-tenants`.')\n }\n\n if (explicitTenantId) {\n return [{ label: `tenant:${explicitTenantId}`, tenantId: explicitTenantId }]\n }\n\n if (globalOnly) {\n return [{ label: 'global', tenantId: null }]\n }\n\n if (!allTenants) {\n return [{ label: 'global', tenantId: null }]\n }\n\n const tenants = await em.find(Tenant, { deletedAt: null }, { orderBy: { name: 'asc' } })\n const scopes: CacheScope[] = [{ label: 'global', tenantId: null }]\n const seen = new Set<string>()\n for (const tenant of tenants) {\n const tenantId = typeof tenant.id === 'string' ? tenant.id : ''\n if (!tenantId || seen.has(tenantId)) continue\n seen.add(tenantId)\n scopes.push({ label: `tenant:${tenantId}`, tenantId })\n }\n return scopes\n}\n\nfunction resolveCachePurgeRequest(args: ParsedArgs): CachePurgeRequest {\n if (flagEnabled(args, 'all')) return { kind: 'all' }\n\n const segment = stringOption(args, 'segment')\n if (segment) return { kind: 'segment', segment }\n\n const tags = splitListOption(stringOption(args, 'tag', 'tags'))\n if (tags.length > 0) return { kind: 'tags', tags }\n\n const keys = splitListOption(stringOption(args, 'key', 'keys'))\n if (keys.length > 0) return { kind: 'keys', keys }\n\n const ids = splitListOption(stringOption(args, 'id', 'ids'))\n if (ids.length > 0) return { kind: 'ids', ids }\n\n const pattern = stringOption(args, 'pattern')\n if (pattern) return { kind: 'pattern', pattern }\n\n throw new Error(\n 'Choose a purge target: `--all`, `--segment <id>`, `--tag <tag1,tag2>`, `--key <key1,key2>`, `--id <token1,token2>`, or `--pattern <glob>`.',\n )\n}\n\nfunction printCacheHelp() {\n console.log('\uD83E\uDDF9 Cache CLI')\n console.log('')\n console.log('\uD83D\uDE80 Usage:')\n console.log(' yarn mercato configs cache stats [--tenant <id> | --global | --all-tenants] [--json]')\n console.log(' yarn mercato configs cache purge --all [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --segment <segment> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --tag <tag1,tag2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --key <key1,key2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --id <token1,token2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --pattern <glob> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache structural [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log('')\n console.log('\u2139\uFE0F Notes:')\n console.log(' `stats` mirrors the cache admin page segment overview for CRUD/widget caches.')\n console.log(' `purge --id` removes every key whose name contains the provided token (for example a user id or entity id).')\n console.log(' `structural` targets navigation/sidebar caches and is the recommended post-step after module/sidebar structure changes.')\n console.log(' When no scope flag is supplied, this command uses the global cache scope only.')\n}\n\nasync function disposeContainer(container: unknown) {\n const disposable = container as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n}\n\nasync function runCacheStats(args: ParsedArgs) {\n const json = flagEnabled(args, 'json')\n const container = await createRequestContainer()\n try {\n const em = container.resolve('em') as EntityManager\n const cache = container.resolve('cache') as CacheStrategy\n const scopes = await resolveCacheScopes(em, args)\n const results = []\n for (const scope of scopes) {\n const stats = await runWithCacheTenant(scope.tenantId, async () => collectCacheStats(cache))\n results.push({ scope: scope.label, ...stats })\n }\n\n if (json) {\n console.log(JSON.stringify(results, null, 2))\n return\n }\n\n for (const result of results) {\n console.log(`\uD83D\uDD0E [cache] scope=${result.scope} totalKeys=${result.totalKeys} generatedAt=${result.generatedAt}`)\n if (result.segments.length === 0) {\n console.log(' \u2205 segments: none')\n continue\n }\n for (const segment of result.segments) {\n console.log(` \u2022 ${segment.segment} (${segment.keyCount})${segment.path ? ` ${segment.path}` : ''}`)\n }\n }\n } finally {\n await disposeContainer(container)\n }\n}\n\nasync function runCachePurgeRequest(\n args: ParsedArgs,\n request: CachePurgeRequest,\n emitOutput = true,\n) {\n const json = flagEnabled(args, 'json')\n const quiet = flagEnabled(args, 'quiet')\n const dryRun = flagEnabled(args, 'dry-run', 'dryRun')\n const container = await createRequestContainer()\n try {\n const em = container.resolve('em') as EntityManager\n const cache = container.resolve('cache') as CacheStrategy\n const scopes = await resolveCacheScopes(em, args)\n const results = []\n\n for (const scope of scopes) {\n const result = await runWithCacheTenant(scope.tenantId, async () =>\n dryRun ? previewCachePurge(cache, request) : executeCachePurge(cache, request)\n )\n results.push({\n scope: scope.label,\n dryRun,\n request,\n deleted: result.deleted,\n keyCount: result.keys.length,\n keys: result.keys,\n note: result.note,\n })\n }\n\n if (json && emitOutput) {\n console.log(JSON.stringify(results, null, 2))\n return results\n }\n\n if (quiet || !emitOutput) {\n return results\n }\n\n for (const result of results) {\n console.log(`${result.dryRun ? '\uD83E\uDDEA' : '\uD83E\uDDF9'} [cache] scope=${result.scope} deleted=${result.deleted}${result.dryRun ? ' (dry-run)' : ''}`)\n if (result.note) console.log(` \u2139\uFE0F note: ${result.note}`)\n if (result.keys.length > 0) {\n for (const key of result.keys) {\n console.log(` \u2022 ${key}`)\n }\n }\n }\n return results\n } finally {\n await disposeContainer(container)\n }\n}\n\nasync function runCachePurge(args: ParsedArgs) {\n await runCachePurgeRequest(args, resolveCachePurgeRequest(args))\n}\n\nasync function runStructuralCachePurge(args: ParsedArgs) {\n const json = flagEnabled(args, 'json')\n const structuralResults: Array<{\n request: CachePurgeRequest\n results: Awaited<ReturnType<typeof runCachePurgeRequest>>\n }> = []\n for (const request of STRUCTURAL_CACHE_REQUESTS) {\n const results = await runCachePurgeRequest(args, request, !json)\n structuralResults.push({ request, results })\n }\n if (json) {\n console.log(JSON.stringify(structuralResults, null, 2))\n }\n const quiet = flagEnabled(args, 'quiet')\n try {\n touchGeneratedBarrels({ quiet: quiet || json })\n } catch (err) {\n if (!quiet && !json) {\n console.warn(\n `[structural] failed to touch generated barrels: ${(err as Error).message ?? err}`,\n )\n }\n }\n}\n\nfunction envDisablesAutoIndexing(): boolean {\n const raw =\n process.env.OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING ??\n process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING\n if (!raw) return false\n return parseBooleanToken(raw) === true\n}\n\nconst restoreDefaults: ModuleCli = {\n command: 'restore-defaults',\n async run() {\n const container = await createRequestContainer()\n try {\n let service: ModuleConfigService\n try {\n service = (container.resolve('moduleConfigService') as ModuleConfigService)\n } catch {\n console.error('[configs] moduleConfigService is not registered in the container.')\n return\n }\n\n const disabledByEnv = envDisablesAutoIndexing()\n const defaultEnabled = !disabledByEnv\n await service.restoreDefaults(\n [\n {\n moduleId: 'vector',\n name: 'auto_index_enabled',\n value: defaultEnabled,\n },\n {\n moduleId: 'notifications',\n name: NOTIFICATIONS_DELIVERY_CONFIG_KEY,\n value: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n },\n ],\n { force: true },\n )\n console.log(\n `[configs] Vector auto-indexing default set to ${defaultEnabled ? 'enabled' : 'disabled'}${\n disabledByEnv\n ? ' (forced by OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING or legacy DISABLE_VECTOR_SEARCH_AUTOINDEXING)'\n : ''\n }.`,\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst help: ModuleCli = {\n command: 'help',\n async run() {\n console.log('\u2699\uFE0F Configs CLI')\n console.log('')\n console.log('\uD83D\uDE80 Usage: yarn mercato configs restore-defaults')\n console.log(' Ensures global module configuration defaults exist.')\n console.log('')\n printCacheHelp()\n },\n}\n\nconst cacheCommand: ModuleCli = {\n command: 'cache',\n async run(rest) {\n const [subcommand, ...subRest] = rest\n const args = parseArgs(subRest)\n\n if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {\n printCacheHelp()\n return\n }\n\n if (subcommand === 'stats') {\n await runCacheStats(args)\n return\n }\n\n if (subcommand === 'purge') {\n await runCachePurge(args)\n return\n }\n\n if (subcommand === 'structural') {\n await runStructuralCachePurge(args)\n return\n }\n\n throw new Error(`Unknown cache subcommand \"${subcommand}\".`)\n },\n}\n\nexport default [restoreDefaults, cacheCommand, help]\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { ModuleCli } from '@open-mercato/shared/modules/registry'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { runWithCacheTenant, type CacheStrategy } from '@open-mercato/cache'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { ModuleConfigService } from './lib/module-config-service'\nimport { parseBooleanToken } from '@open-mercato/shared/lib/boolean'\nimport { DEFAULT_NOTIFICATION_DELIVERY_CONFIG, NOTIFICATIONS_DELIVERY_CONFIG_KEY } from '../notifications/lib/deliveryConfig'\nimport { Tenant } from '../directory/data/entities'\nimport {\n collectCacheStats,\n executeCachePurge,\n previewCachePurge,\n type CachePurgeRequest,\n} from './lib/cache-cli'\nimport { touchGeneratedBarrels } from './lib/touchGeneratedBarrels'\n\ntype ParsedArgs = Record<string, string | boolean>\n\nexport const STRUCTURAL_CACHE_REQUESTS: CachePurgeRequest[] = [\n { kind: 'pattern', pattern: 'nav:*' },\n { kind: 'segment', segment: 'admin-nav' },\n { kind: 'segment', segment: 'portal-nav' },\n]\n\ntype CacheScope = {\n label: string\n tenantId: string | null\n}\n\nfunction parseArgs(rest: string[]): ParsedArgs {\n const args: ParsedArgs = {}\n for (let i = 0; i < rest.length; i += 1) {\n const part = rest[i]\n if (!part?.startsWith('--')) continue\n const [rawKey, rawValue] = part.slice(2).split('=')\n if (!rawKey) continue\n if (rawValue !== undefined) {\n args[rawKey] = rawValue\n } else if (i + 1 < rest.length && !rest[i + 1]!.startsWith('--')) {\n args[rawKey] = rest[i + 1]!\n i += 1\n } else {\n args[rawKey] = true\n }\n }\n return args\n}\n\nfunction stringOption(args: ParsedArgs, ...keys: string[]): string | undefined {\n for (const key of keys) {\n const raw = args[key]\n if (typeof raw !== 'string') continue\n const trimmed = raw.trim()\n if (trimmed.length > 0) return trimmed\n }\n return undefined\n}\n\nfunction flagEnabled(args: ParsedArgs, ...keys: string[]): boolean {\n for (const key of keys) {\n const raw = args[key]\n if (raw === undefined) continue\n if (raw === true) return true\n if (typeof raw === 'string') {\n const parsed = parseBooleanToken(raw)\n return parsed === null ? true : parsed\n }\n }\n return false\n}\n\nfunction splitListOption(raw: string | undefined): string[] {\n if (!raw) return []\n const seen = new Set<string>()\n const values: string[] = []\n for (const item of raw.split(',')) {\n const trimmed = item.trim()\n if (!trimmed || seen.has(trimmed)) continue\n seen.add(trimmed)\n values.push(trimmed)\n }\n return values\n}\n\nasync function resolveCacheScopes(\n em: EntityManager,\n args: ParsedArgs,\n): Promise<CacheScope[]> {\n const explicitTenantId = stringOption(args, 'tenant', 'tenantId')\n const globalOnly = flagEnabled(args, 'global')\n const allTenants = flagEnabled(args, 'all-tenants', 'allTenants')\n\n if (explicitTenantId && globalOnly) {\n throw new Error('Cannot combine `--tenant` with `--global`.')\n }\n if (explicitTenantId && allTenants) {\n throw new Error('Cannot combine `--tenant` with `--all-tenants`.')\n }\n if (globalOnly && allTenants) {\n throw new Error('Cannot combine `--global` with `--all-tenants`.')\n }\n\n if (explicitTenantId) {\n return [{ label: `tenant:${explicitTenantId}`, tenantId: explicitTenantId }]\n }\n\n if (globalOnly) {\n return [{ label: 'global', tenantId: null }]\n }\n\n if (!allTenants) {\n return [{ label: 'global', tenantId: null }]\n }\n\n const tenants = await em.find(Tenant, { deletedAt: null }, { orderBy: { name: 'asc' } })\n const scopes: CacheScope[] = [{ label: 'global', tenantId: null }]\n const seen = new Set<string>()\n for (const tenant of tenants) {\n const tenantId = typeof tenant.id === 'string' ? tenant.id : ''\n if (!tenantId || seen.has(tenantId)) continue\n seen.add(tenantId)\n scopes.push({ label: `tenant:${tenantId}`, tenantId })\n }\n return scopes\n}\n\nfunction resolveCachePurgeRequest(args: ParsedArgs): CachePurgeRequest {\n if (flagEnabled(args, 'all')) return { kind: 'all' }\n\n const segment = stringOption(args, 'segment')\n if (segment) return { kind: 'segment', segment }\n\n const tags = splitListOption(stringOption(args, 'tag', 'tags'))\n if (tags.length > 0) return { kind: 'tags', tags }\n\n const keys = splitListOption(stringOption(args, 'key', 'keys'))\n if (keys.length > 0) return { kind: 'keys', keys }\n\n const ids = splitListOption(stringOption(args, 'id', 'ids'))\n if (ids.length > 0) return { kind: 'ids', ids }\n\n const pattern = stringOption(args, 'pattern')\n if (pattern) return { kind: 'pattern', pattern }\n\n throw new Error(\n 'Choose a purge target: `--all`, `--segment <id>`, `--tag <tag1,tag2>`, `--key <key1,key2>`, `--id <token1,token2>`, or `--pattern <glob>`.',\n )\n}\n\nfunction printCacheHelp() {\n console.log('\uD83E\uDDF9 Cache CLI')\n console.log('')\n console.log('\uD83D\uDE80 Usage:')\n console.log(' yarn mercato configs cache stats [--tenant <id> | --global | --all-tenants] [--json]')\n console.log(' yarn mercato configs cache purge --all [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --segment <segment> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --tag <tag1,tag2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --key <key1,key2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --id <token1,token2> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache purge --pattern <glob> [--tenant <id> | --global | --all-tenants] [--dry-run] [--json]')\n console.log(' yarn mercato configs cache structural [--tenant <id> | --global | --all-tenants] [--touch-generated] [--dry-run] [--json]')\n console.log('')\n console.log('\u2139\uFE0F Notes:')\n console.log(' `stats` mirrors the cache admin page segment overview for CRUD/widget caches.')\n console.log(' `purge --id` removes every key whose name contains the provided token (for example a user id or entity id).')\n console.log(' `structural` targets navigation/sidebar caches. Add `--touch-generated` only for explicit stale-compiler recovery.')\n console.log(' When no scope flag is supplied, this command uses the global cache scope only.')\n}\n\nasync function disposeContainer(container: unknown) {\n const disposable = container as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n}\n\nasync function runCacheStats(args: ParsedArgs) {\n const json = flagEnabled(args, 'json')\n const container = await createRequestContainer()\n try {\n const em = container.resolve('em') as EntityManager\n const cache = container.resolve('cache') as CacheStrategy\n const scopes = await resolveCacheScopes(em, args)\n const results = []\n for (const scope of scopes) {\n const stats = await runWithCacheTenant(scope.tenantId, async () => collectCacheStats(cache))\n results.push({ scope: scope.label, ...stats })\n }\n\n if (json) {\n console.log(JSON.stringify(results, null, 2))\n return\n }\n\n for (const result of results) {\n console.log(`\uD83D\uDD0E [cache] scope=${result.scope} totalKeys=${result.totalKeys} generatedAt=${result.generatedAt}`)\n if (result.segments.length === 0) {\n console.log(' \u2205 segments: none')\n continue\n }\n for (const segment of result.segments) {\n console.log(` \u2022 ${segment.segment} (${segment.keyCount})${segment.path ? ` ${segment.path}` : ''}`)\n }\n }\n } finally {\n await disposeContainer(container)\n }\n}\n\nasync function runCachePurgeRequest(\n args: ParsedArgs,\n request: CachePurgeRequest,\n emitOutput = true,\n) {\n const json = flagEnabled(args, 'json')\n const quiet = flagEnabled(args, 'quiet')\n const dryRun = flagEnabled(args, 'dry-run', 'dryRun')\n const container = await createRequestContainer()\n try {\n const em = container.resolve('em') as EntityManager\n const cache = container.resolve('cache') as CacheStrategy\n const scopes = await resolveCacheScopes(em, args)\n const results = []\n\n for (const scope of scopes) {\n const result = await runWithCacheTenant(scope.tenantId, async () =>\n dryRun ? previewCachePurge(cache, request) : executeCachePurge(cache, request)\n )\n results.push({\n scope: scope.label,\n dryRun,\n request,\n deleted: result.deleted,\n keyCount: result.keys.length,\n keys: result.keys,\n note: result.note,\n })\n }\n\n if (json && emitOutput) {\n console.log(JSON.stringify(results, null, 2))\n return results\n }\n\n if (quiet || !emitOutput) {\n return results\n }\n\n for (const result of results) {\n console.log(`${result.dryRun ? '\uD83E\uDDEA' : '\uD83E\uDDF9'} [cache] scope=${result.scope} deleted=${result.deleted}${result.dryRun ? ' (dry-run)' : ''}`)\n if (result.note) console.log(` \u2139\uFE0F note: ${result.note}`)\n if (result.keys.length > 0) {\n for (const key of result.keys) {\n console.log(` \u2022 ${key}`)\n }\n }\n }\n return results\n } finally {\n await disposeContainer(container)\n }\n}\n\nasync function runCachePurge(args: ParsedArgs) {\n await runCachePurgeRequest(args, resolveCachePurgeRequest(args))\n}\n\nasync function runStructuralCachePurge(args: ParsedArgs) {\n const json = flagEnabled(args, 'json')\n const structuralResults: Array<{\n request: CachePurgeRequest\n results: Awaited<ReturnType<typeof runCachePurgeRequest>>\n }> = []\n for (const request of STRUCTURAL_CACHE_REQUESTS) {\n const results = await runCachePurgeRequest(args, request, !json)\n structuralResults.push({ request, results })\n }\n if (json) {\n console.log(JSON.stringify(structuralResults, null, 2))\n }\n if (flagEnabled(args, 'touch-generated', 'touchGenerated')) {\n const quiet = flagEnabled(args, 'quiet')\n try {\n touchGeneratedBarrels({ quiet: quiet || json })\n } catch (err) {\n if (!quiet && !json) {\n console.warn(\n `[structural] failed to touch generated barrels: ${(err as Error).message ?? err}`,\n )\n }\n }\n }\n}\n\nfunction envDisablesAutoIndexing(): boolean {\n const raw =\n process.env.OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING ??\n process.env.DISABLE_VECTOR_SEARCH_AUTOINDEXING\n if (!raw) return false\n return parseBooleanToken(raw) === true\n}\n\nconst restoreDefaults: ModuleCli = {\n command: 'restore-defaults',\n async run() {\n const container = await createRequestContainer()\n try {\n let service: ModuleConfigService\n try {\n service = (container.resolve('moduleConfigService') as ModuleConfigService)\n } catch {\n console.error('[configs] moduleConfigService is not registered in the container.')\n return\n }\n\n const disabledByEnv = envDisablesAutoIndexing()\n const defaultEnabled = !disabledByEnv\n await service.restoreDefaults(\n [\n {\n moduleId: 'vector',\n name: 'auto_index_enabled',\n value: defaultEnabled,\n },\n {\n moduleId: 'notifications',\n name: NOTIFICATIONS_DELIVERY_CONFIG_KEY,\n value: DEFAULT_NOTIFICATION_DELIVERY_CONFIG,\n },\n ],\n { force: true },\n )\n console.log(\n `[configs] Vector auto-indexing default set to ${defaultEnabled ? 'enabled' : 'disabled'}${\n disabledByEnv\n ? ' (forced by OM_DISABLE_VECTOR_SEARCH_AUTOINDEXING or legacy DISABLE_VECTOR_SEARCH_AUTOINDEXING)'\n : ''\n }.`,\n )\n } finally {\n const disposable = container as unknown as { dispose?: () => Promise<void> }\n if (typeof disposable.dispose === 'function') {\n await disposable.dispose()\n }\n }\n },\n}\n\nconst help: ModuleCli = {\n command: 'help',\n async run() {\n console.log('\u2699\uFE0F Configs CLI')\n console.log('')\n console.log('\uD83D\uDE80 Usage: yarn mercato configs restore-defaults')\n console.log(' Ensures global module configuration defaults exist.')\n console.log('')\n printCacheHelp()\n },\n}\n\nconst cacheCommand: ModuleCli = {\n command: 'cache',\n async run(rest) {\n const [subcommand, ...subRest] = rest\n const args = parseArgs(subRest)\n\n if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {\n printCacheHelp()\n return\n }\n\n if (subcommand === 'stats') {\n await runCacheStats(args)\n return\n }\n\n if (subcommand === 'purge') {\n await runCachePurge(args)\n return\n }\n\n if (subcommand === 'structural') {\n await runStructuralCachePurge(args)\n return\n }\n\n throw new Error(`Unknown cache subcommand \"${subcommand}\".`)\n },\n}\n\nexport default [restoreDefaults, cacheCommand, help]\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -26,13 +26,13 @@ function touchGeneratedBarrels(options = {}) {
|
|
|
26
26
|
return { generatedDir: null, files: [] };
|
|
27
27
|
}
|
|
28
28
|
const touched = [];
|
|
29
|
+
const touchedAt = /* @__PURE__ */ new Date();
|
|
29
30
|
const entries = fs.readdirSync(generatedDir, { withFileTypes: true });
|
|
30
31
|
for (const entry of entries) {
|
|
31
32
|
if (!entry.isFile()) continue;
|
|
32
33
|
if (!TOUCHABLE_PATTERN.test(entry.name)) continue;
|
|
33
34
|
const filePath = path.join(generatedDir, entry.name);
|
|
34
|
-
|
|
35
|
-
fs.writeFileSync(filePath, contents);
|
|
35
|
+
fs.utimesSync(filePath, touchedAt, touchedAt);
|
|
36
36
|
touched.push(filePath);
|
|
37
37
|
}
|
|
38
38
|
if (!quiet && touched.length > 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/configs/lib/touchGeneratedBarrels.ts"],
|
|
4
|
-
"sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\n\nconst GENERATED_DIR_RELATIVE = path.join('.mercato', 'generated')\nconst TOUCHABLE_PATTERN = /\\.generated(?:\\.[a-z0-9]+)?(?:\\.ts|\\.checksum)$/i\nconst MAX_PARENT_WALK = 5\n\nexport type TouchGeneratedBarrelsOptions = {\n cwd?: string\n quiet?: boolean\n log?: (message: string) => void\n}\n\nexport type TouchGeneratedBarrelsResult = {\n generatedDir: string | null\n files: string[]\n}\n\nexport function findGeneratedDir(startDir: string): string | null {\n let current = path.resolve(startDir)\n for (let depth = 0; depth <= MAX_PARENT_WALK; depth += 1) {\n const candidate = path.join(current, GENERATED_DIR_RELATIVE)\n if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {\n return candidate\n }\n const parent = path.dirname(current)\n if (parent === current) break\n current = parent\n }\n return null\n}\n\nexport function touchGeneratedBarrels(\n options: TouchGeneratedBarrelsOptions = {},\n): TouchGeneratedBarrelsResult {\n const cwd = options.cwd ?? process.cwd()\n const log = options.log ?? ((message: string) => process.stdout.write(`${message}\\n`))\n const quiet = options.quiet === true\n\n const generatedDir = findGeneratedDir(cwd)\n if (!generatedDir) {\n return { generatedDir: null, files: [] }\n }\n\n const touched: string[] = []\n const entries = fs.readdirSync(generatedDir, { withFileTypes: true })\n for (const entry of entries) {\n if (!entry.isFile()) continue\n if (!TOUCHABLE_PATTERN.test(entry.name)) continue\n const filePath = path.join(generatedDir, entry.name)\n
|
|
5
|
-
"mappings": "AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AAEjB,MAAM,yBAAyB,KAAK,KAAK,YAAY,WAAW;AAChE,MAAM,oBAAoB;AAC1B,MAAM,kBAAkB;AAajB,SAAS,iBAAiB,UAAiC;AAChE,MAAI,UAAU,KAAK,QAAQ,QAAQ;AACnC,WAAS,QAAQ,GAAG,SAAS,iBAAiB,SAAS,GAAG;AACxD,UAAM,YAAY,KAAK,KAAK,SAAS,sBAAsB;AAC3D,QAAI,GAAG,WAAW,SAAS,KAAK,GAAG,SAAS,SAAS,EAAE,YAAY,GAAG;AACpE,aAAO;AAAA,IACT;AACA,UAAM,SAAS,KAAK,QAAQ,OAAO;AACnC,QAAI,WAAW,QAAS;AACxB,cAAU;AAAA,EACZ;AACA,SAAO;AACT;AAEO,SAAS,sBACd,UAAwC,CAAC,GACZ;AAC7B,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACvC,QAAM,MAAM,QAAQ,QAAQ,CAAC,YAAoB,QAAQ,OAAO,MAAM,GAAG,OAAO;AAAA,CAAI;AACpF,QAAM,QAAQ,QAAQ,UAAU;AAEhC,QAAM,eAAe,iBAAiB,GAAG;AACzC,MAAI,CAAC,cAAc;AACjB,WAAO,EAAE,cAAc,MAAM,OAAO,CAAC,EAAE;AAAA,EACzC;AAEA,QAAM,UAAoB,CAAC;AAC3B,QAAM,UAAU,GAAG,YAAY,cAAc,EAAE,eAAe,KAAK,CAAC;AACpE,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,OAAO,EAAG;AACrB,QAAI,CAAC,kBAAkB,KAAK,MAAM,IAAI,EAAG;AACzC,UAAM,WAAW,KAAK,KAAK,cAAc,MAAM,IAAI;
|
|
4
|
+
"sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\n\nconst GENERATED_DIR_RELATIVE = path.join('.mercato', 'generated')\nconst TOUCHABLE_PATTERN = /\\.generated(?:\\.[a-z0-9]+)?(?:\\.ts|\\.checksum)$/i\nconst MAX_PARENT_WALK = 5\n\nexport type TouchGeneratedBarrelsOptions = {\n cwd?: string\n quiet?: boolean\n log?: (message: string) => void\n}\n\nexport type TouchGeneratedBarrelsResult = {\n generatedDir: string | null\n files: string[]\n}\n\nexport function findGeneratedDir(startDir: string): string | null {\n let current = path.resolve(startDir)\n for (let depth = 0; depth <= MAX_PARENT_WALK; depth += 1) {\n const candidate = path.join(current, GENERATED_DIR_RELATIVE)\n if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {\n return candidate\n }\n const parent = path.dirname(current)\n if (parent === current) break\n current = parent\n }\n return null\n}\n\nexport function touchGeneratedBarrels(\n options: TouchGeneratedBarrelsOptions = {},\n): TouchGeneratedBarrelsResult {\n const cwd = options.cwd ?? process.cwd()\n const log = options.log ?? ((message: string) => process.stdout.write(`${message}\\n`))\n const quiet = options.quiet === true\n\n const generatedDir = findGeneratedDir(cwd)\n if (!generatedDir) {\n return { generatedDir: null, files: [] }\n }\n\n const touched: string[] = []\n const touchedAt = new Date()\n const entries = fs.readdirSync(generatedDir, { withFileTypes: true })\n for (const entry of entries) {\n if (!entry.isFile()) continue\n if (!TOUCHABLE_PATTERN.test(entry.name)) continue\n const filePath = path.join(generatedDir, entry.name)\n // Advance mtime without rewriting the bytes: rewriting truncates then refills\n // multi-megabyte registries, and a concurrent Turbopack compile reading one\n // mid-write sees a partial file. Matches `packages/cli/src/lib/post-generate-invalidation.ts`.\n fs.utimesSync(filePath, touchedAt, touchedAt)\n touched.push(filePath)\n }\n\n if (!quiet && touched.length > 0) {\n log(`\uD83D\uDD01 [structural] touched ${touched.length} generated barrel(s) \u2192 ${generatedDir}`)\n }\n\n return { generatedDir, files: touched }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AAEjB,MAAM,yBAAyB,KAAK,KAAK,YAAY,WAAW;AAChE,MAAM,oBAAoB;AAC1B,MAAM,kBAAkB;AAajB,SAAS,iBAAiB,UAAiC;AAChE,MAAI,UAAU,KAAK,QAAQ,QAAQ;AACnC,WAAS,QAAQ,GAAG,SAAS,iBAAiB,SAAS,GAAG;AACxD,UAAM,YAAY,KAAK,KAAK,SAAS,sBAAsB;AAC3D,QAAI,GAAG,WAAW,SAAS,KAAK,GAAG,SAAS,SAAS,EAAE,YAAY,GAAG;AACpE,aAAO;AAAA,IACT;AACA,UAAM,SAAS,KAAK,QAAQ,OAAO;AACnC,QAAI,WAAW,QAAS;AACxB,cAAU;AAAA,EACZ;AACA,SAAO;AACT;AAEO,SAAS,sBACd,UAAwC,CAAC,GACZ;AAC7B,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACvC,QAAM,MAAM,QAAQ,QAAQ,CAAC,YAAoB,QAAQ,OAAO,MAAM,GAAG,OAAO;AAAA,CAAI;AACpF,QAAM,QAAQ,QAAQ,UAAU;AAEhC,QAAM,eAAe,iBAAiB,GAAG;AACzC,MAAI,CAAC,cAAc;AACjB,WAAO,EAAE,cAAc,MAAM,OAAO,CAAC,EAAE;AAAA,EACzC;AAEA,QAAM,UAAoB,CAAC;AAC3B,QAAM,YAAY,oBAAI,KAAK;AAC3B,QAAM,UAAU,GAAG,YAAY,cAAc,EAAE,eAAe,KAAK,CAAC;AACpE,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,MAAM,OAAO,EAAG;AACrB,QAAI,CAAC,kBAAkB,KAAK,MAAM,IAAI,EAAG;AACzC,UAAM,WAAW,KAAK,KAAK,cAAc,MAAM,IAAI;AAInD,OAAG,WAAW,UAAU,WAAW,SAAS;AAC5C,YAAQ,KAAK,QAAQ;AAAA,EACvB;AAEA,MAAI,CAAC,SAAS,QAAQ,SAAS,GAAG;AAChC,QAAI,kCAA2B,QAAQ,MAAM,+BAA0B,YAAY,EAAE;AAAA,EACvF;AAEA,SAAO,EAAE,cAAc,OAAO,QAAQ;AACxC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MessageObjectDetail, MessageObjectPreview } from "@open-mercato/ui";
|
|
1
|
+
import { MessageObjectDetail, MessageObjectPreview } from "@open-mercato/ui/backend/messages";
|
|
2
2
|
const objectMessageTypes = ["default", "messages.defaultWithObjects"];
|
|
3
3
|
const messageObjectTypes = [
|
|
4
4
|
{
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/customers/message-objects.ts"],
|
|
4
|
-
"sourcesContent": ["import type { MessageObjectTypeDefinition } from '@open-mercato/shared/modules/messages/types'\nimport { MessageObjectDetail, MessageObjectPreview } from '@open-mercato/ui'\n\nconst objectMessageTypes = ['default', 'messages.defaultWithObjects']\n\nexport const messageObjectTypes: MessageObjectTypeDefinition[] = [\n {\n module: 'customers',\n entityType: 'person',\n messageTypes: objectMessageTypes,\n entityId: 'customers:customer_person_profile',\n optionLabelField: 'name',\n optionSubtitleField: 'email',\n labelKey: 'customers.people.list.title',\n icon: 'user-round',\n PreviewComponent: MessageObjectPreview,\n DetailComponent: MessageObjectDetail,\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/customers/people/{entityId}',\n },\n ],\n loadPreview: async (entityId, ctx) => {\n if (typeof window !== 'undefined') {\n return { title: 'Person', subtitle: entityId }\n }\n const { loadCustomerPersonPreview } = await import('./lib/messageObjectPreviews')\n return loadCustomerPersonPreview(entityId, ctx)\n },\n },\n {\n module: 'customers',\n entityType: 'company',\n messageTypes: objectMessageTypes,\n entityId: 'customers:customer_company_profile',\n optionLabelField: 'name',\n optionSubtitleField: 'taxId',\n labelKey: 'customers.companies.list.title',\n icon: 'building2',\n PreviewComponent: MessageObjectPreview,\n DetailComponent: MessageObjectDetail,\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/customers/companies/{entityId}',\n },\n ],\n loadPreview: async (entityId, ctx) => {\n if (typeof window !== 'undefined') {\n return { title: 'Company', subtitle: entityId }\n }\n const { loadCustomerCompanyPreview } = await import('./lib/messageObjectPreviews')\n return loadCustomerCompanyPreview(entityId, ctx)\n },\n },\n {\n module: 'customers',\n entityType: 'deal',\n messageTypes: objectMessageTypes,\n entityId: 'customers:customer_deal',\n optionLabelField: 'title',\n optionSubtitleField: 'status',\n labelKey: 'customers.deals.list.title',\n icon: 'briefcase-business',\n PreviewComponent: MessageObjectPreview,\n DetailComponent: MessageObjectDetail,\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/customers/deals/{entityId}',\n },\n ],\n loadPreview: async (entityId, ctx) => {\n if (typeof window !== 'undefined') {\n return { title: 'Deal', subtitle: entityId }\n }\n const { loadCustomerDealPreview } = await import('./lib/messageObjectPreviews')\n return loadCustomerDealPreview(entityId, ctx)\n },\n },\n]\n\nexport default messageObjectTypes\n"],
|
|
4
|
+
"sourcesContent": ["import type { MessageObjectTypeDefinition } from '@open-mercato/shared/modules/messages/types'\nimport { MessageObjectDetail, MessageObjectPreview } from '@open-mercato/ui/backend/messages'\n\nconst objectMessageTypes = ['default', 'messages.defaultWithObjects']\n\nexport const messageObjectTypes: MessageObjectTypeDefinition[] = [\n {\n module: 'customers',\n entityType: 'person',\n messageTypes: objectMessageTypes,\n entityId: 'customers:customer_person_profile',\n optionLabelField: 'name',\n optionSubtitleField: 'email',\n labelKey: 'customers.people.list.title',\n icon: 'user-round',\n PreviewComponent: MessageObjectPreview,\n DetailComponent: MessageObjectDetail,\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/customers/people/{entityId}',\n },\n ],\n loadPreview: async (entityId, ctx) => {\n if (typeof window !== 'undefined') {\n return { title: 'Person', subtitle: entityId }\n }\n const { loadCustomerPersonPreview } = await import('./lib/messageObjectPreviews')\n return loadCustomerPersonPreview(entityId, ctx)\n },\n },\n {\n module: 'customers',\n entityType: 'company',\n messageTypes: objectMessageTypes,\n entityId: 'customers:customer_company_profile',\n optionLabelField: 'name',\n optionSubtitleField: 'taxId',\n labelKey: 'customers.companies.list.title',\n icon: 'building2',\n PreviewComponent: MessageObjectPreview,\n DetailComponent: MessageObjectDetail,\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/customers/companies/{entityId}',\n },\n ],\n loadPreview: async (entityId, ctx) => {\n if (typeof window !== 'undefined') {\n return { title: 'Company', subtitle: entityId }\n }\n const { loadCustomerCompanyPreview } = await import('./lib/messageObjectPreviews')\n return loadCustomerCompanyPreview(entityId, ctx)\n },\n },\n {\n module: 'customers',\n entityType: 'deal',\n messageTypes: objectMessageTypes,\n entityId: 'customers:customer_deal',\n optionLabelField: 'title',\n optionSubtitleField: 'status',\n labelKey: 'customers.deals.list.title',\n icon: 'briefcase-business',\n PreviewComponent: MessageObjectPreview,\n DetailComponent: MessageObjectDetail,\n actions: [\n {\n id: 'view',\n labelKey: 'common.view',\n variant: 'outline',\n href: '/backend/customers/deals/{entityId}',\n },\n ],\n loadPreview: async (entityId, ctx) => {\n if (typeof window !== 'undefined') {\n return { title: 'Deal', subtitle: entityId }\n }\n const { loadCustomerDealPreview } = await import('./lib/messageObjectPreviews')\n return loadCustomerDealPreview(entityId, ctx)\n },\n },\n]\n\nexport default messageObjectTypes\n"],
|
|
5
5
|
"mappings": "AACA,SAAS,qBAAqB,4BAA4B;AAE1D,MAAM,qBAAqB,CAAC,WAAW,6BAA6B;AAE7D,MAAM,qBAAoD;AAAA,EAC/D;AAAA,IACE,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,UAAU;AAAA,IACV,kBAAkB;AAAA,IAClB,qBAAqB;AAAA,IACrB,UAAU;AAAA,IACV,MAAM;AAAA,IACN,kBAAkB;AAAA,IAClB,iBAAiB;AAAA,IACjB,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,aAAa,OAAO,UAAU,QAAQ;AACpC,UAAI,OAAO,WAAW,aAAa;AACjC,eAAO,EAAE,OAAO,UAAU,UAAU,SAAS;AAAA,MAC/C;AACA,YAAM,EAAE,0BAA0B,IAAI,MAAM,OAAO,6BAA6B;AAChF,aAAO,0BAA0B,UAAU,GAAG;AAAA,IAChD;AAAA,EACF;AAAA,EACA;AAAA,IACE,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,UAAU;AAAA,IACV,kBAAkB;AAAA,IAClB,qBAAqB;AAAA,IACrB,UAAU;AAAA,IACV,MAAM;AAAA,IACN,kBAAkB;AAAA,IAClB,iBAAiB;AAAA,IACjB,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,aAAa,OAAO,UAAU,QAAQ;AACpC,UAAI,OAAO,WAAW,aAAa;AACjC,eAAO,EAAE,OAAO,WAAW,UAAU,SAAS;AAAA,MAChD;AACA,YAAM,EAAE,2BAA2B,IAAI,MAAM,OAAO,6BAA6B;AACjF,aAAO,2BAA2B,UAAU,GAAG;AAAA,IACjD;AAAA,EACF;AAAA,EACA;AAAA,IACE,QAAQ;AAAA,IACR,YAAY;AAAA,IACZ,cAAc;AAAA,IACd,UAAU;AAAA,IACV,kBAAkB;AAAA,IAClB,qBAAqB;AAAA,IACrB,UAAU;AAAA,IACV,MAAM;AAAA,IACN,kBAAkB;AAAA,IAClB,iBAAiB;AAAA,IACjB,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,UAAU;AAAA,QACV,SAAS;AAAA,QACT,MAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,aAAa,OAAO,UAAU,QAAQ;AACpC,UAAI,OAAO,WAAW,aAAa;AACjC,eAAO,EAAE,OAAO,QAAQ,UAAU,SAAS;AAAA,MAC7C;AACA,YAAM,EAAE,wBAAwB,IAAI,MAAM,OAAO,6BAA6B;AAC9E,aAAO,wBAAwB,UAAU,GAAG;AAAA,IAC9C;AAAA,EACF;AACF;AAEA,IAAO,0BAAQ;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.8-develop.
|
|
3
|
+
"version": "0.6.8-develop.6893.1.7af3b3a72d",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
"leaflet": "^1.9.4",
|
|
245
245
|
"leaflet.markercluster": "^1.5.3",
|
|
246
246
|
"mammoth": "^1.9.0",
|
|
247
|
-
"pdfjs-dist": "^6.
|
|
247
|
+
"pdfjs-dist": "^6.2.108",
|
|
248
248
|
"resend": "^6.18.1",
|
|
249
249
|
"sanitize-html": "2.17.5",
|
|
250
250
|
"semver": "^7.8.5",
|
|
@@ -254,16 +254,16 @@
|
|
|
254
254
|
"zod": "^4.4.3"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
258
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.6893.1.7af3b3a72d",
|
|
258
|
+
"@open-mercato/shared": "0.6.8-develop.6893.1.7af3b3a72d",
|
|
259
|
+
"@open-mercato/ui": "0.6.8-develop.6893.1.7af3b3a72d",
|
|
260
260
|
"react": "^19.0.0",
|
|
261
261
|
"react-dom": "^19.0.0"
|
|
262
262
|
},
|
|
263
263
|
"devDependencies": {
|
|
264
|
-
"@open-mercato/ai-assistant": "0.6.8-develop.
|
|
265
|
-
"@open-mercato/shared": "0.6.8-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.8-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.8-develop.6893.1.7af3b3a72d",
|
|
265
|
+
"@open-mercato/shared": "0.6.8-develop.6893.1.7af3b3a72d",
|
|
266
|
+
"@open-mercato/ui": "0.6.8-develop.6893.1.7af3b3a72d",
|
|
267
267
|
"@testing-library/dom": "^10.4.1",
|
|
268
268
|
"@testing-library/jest-dom": "^7.0.0",
|
|
269
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -4,6 +4,8 @@ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
|
4
4
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
5
5
|
import { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
6
6
|
import { CommunicationChannel } from '../../../../data/entities'
|
|
7
|
+
import { isHubPolledChannel } from '../../../../lib/polling-eligibility'
|
|
8
|
+
import type { ChannelAdapterRegistry } from '../../../../lib/registry'
|
|
7
9
|
|
|
8
10
|
export const metadata = {
|
|
9
11
|
path: '/communication_channels/me/channels',
|
|
@@ -41,13 +43,25 @@ export async function GET(req: Request): Promise<Response> {
|
|
|
41
43
|
{ tenantId: auth.tenantId as string, organizationId: (auth as { orgId?: string | null }).orgId ?? null },
|
|
42
44
|
)
|
|
43
45
|
|
|
46
|
+
// The profile grid renders "is this channel polled or push-driven?" and
|
|
47
|
+
// "can push be registered here?" — both are adapter facts, not provider names,
|
|
48
|
+
// so they are resolved server-side (#4980).
|
|
49
|
+
let adapterRegistry: ChannelAdapterRegistry | null = null
|
|
50
|
+
try {
|
|
51
|
+
adapterRegistry = container.resolve('channelAdapterRegistry') as ChannelAdapterRegistry
|
|
52
|
+
} catch {
|
|
53
|
+
adapterRegistry = null
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
return NextResponse.json({
|
|
45
|
-
items: (channels as CommunicationChannel[]).map(
|
|
57
|
+
items: (channels as CommunicationChannel[]).map((channel) =>
|
|
58
|
+
serialize(channel, adapterRegistry),
|
|
59
|
+
),
|
|
46
60
|
total: channels.length,
|
|
47
61
|
})
|
|
48
62
|
}
|
|
49
63
|
|
|
50
|
-
function serialize(channel: CommunicationChannel) {
|
|
64
|
+
function serialize(channel: CommunicationChannel, adapterRegistry: ChannelAdapterRegistry | null) {
|
|
51
65
|
// Spec C — expose push status + last push error to the operator UI so
|
|
52
66
|
// the `PushStatusSection` can render the "Re-register push" affordance.
|
|
53
67
|
const channelState =
|
|
@@ -66,6 +80,12 @@ function serialize(channel: CommunicationChannel) {
|
|
|
66
80
|
at: channelState.lastPushError.at ?? null,
|
|
67
81
|
}
|
|
68
82
|
: null
|
|
83
|
+
// `true` when the hub's poll worker skips this channel because the adapter
|
|
84
|
+
// declares real-time push — the inverse of `isHubPolledChannel`, which the
|
|
85
|
+
// worker itself uses, so the label cannot contradict the behaviour.
|
|
86
|
+
const supportsRealtimePush = !isHubPolledChannel(channel.capabilities)
|
|
87
|
+
const adapter = adapterRegistry?.get(channel.providerKey)
|
|
88
|
+
const supportsPushRegistration = typeof adapter?.registerPush === 'function'
|
|
69
89
|
return {
|
|
70
90
|
id: channel.id,
|
|
71
91
|
providerKey: channel.providerKey,
|
|
@@ -80,6 +100,8 @@ function serialize(channel: CommunicationChannel) {
|
|
|
80
100
|
lastPolledAt: channel.lastPolledAt?.toISOString?.() ?? null,
|
|
81
101
|
pushStatus,
|
|
82
102
|
lastPushError,
|
|
103
|
+
supportsRealtimePush,
|
|
104
|
+
supportsPushRegistration,
|
|
83
105
|
createdAt: channel.createdAt?.toISOString?.() ?? null,
|
|
84
106
|
}
|
|
85
107
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { z } from 'zod'
|
|
3
3
|
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
4
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
4
5
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
6
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
6
7
|
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
7
8
|
import { CommunicationChannel } from '../../../../../data/entities'
|
|
8
9
|
import { ChannelAccessDeniedError, assertCanManageChannel } from '../../../../../lib/access-control'
|
|
9
10
|
import { COMMUNICATION_CHANNELS_QUEUES, getCommunicationChannelsQueue } from '../../../../../lib/queue'
|
|
11
|
+
import { isHubPolledChannel } from '../../../../../lib/polling-eligibility'
|
|
10
12
|
import type { PollChannelJobPayload } from '../../../../../workers/poll-channel'
|
|
11
13
|
import { validateRouteMutationGuard } from '../../../../../lib/route-mutation-guard'
|
|
12
14
|
|
|
@@ -118,6 +120,25 @@ export async function POST(req: Request, context: RouteContext): Promise<Respons
|
|
|
118
120
|
{ status: 409 },
|
|
119
121
|
)
|
|
120
122
|
}
|
|
123
|
+
// The poll worker returns immediately for a channel whose adapter declares
|
|
124
|
+
// real-time push, so enqueueing a job here would answer 202 for work that can
|
|
125
|
+
// never run and the UI would promise messages that never arrive (#4980).
|
|
126
|
+
if (!isHubPolledChannel(channel.capabilities)) {
|
|
127
|
+
// The page flashes this string verbatim, so it is operator-facing. Localize
|
|
128
|
+
// it when a request locale is resolvable and fall back to English rather
|
|
129
|
+
// than failing the request if the i18n registry is uninitialized — the same
|
|
130
|
+
// defensive shape the module's command interceptors use.
|
|
131
|
+
const fallback =
|
|
132
|
+
'Channel is push-driven — polling does not apply. Inbound messages arrive through the provider push connection.'
|
|
133
|
+
let message = fallback
|
|
134
|
+
try {
|
|
135
|
+
const { translate } = await resolveTranslations()
|
|
136
|
+
message = translate('communication_channels.errors.pollNowPushDriven', fallback)
|
|
137
|
+
} catch {
|
|
138
|
+
message = fallback
|
|
139
|
+
}
|
|
140
|
+
return NextResponse.json({ error: message }, { status: 409 })
|
|
141
|
+
}
|
|
121
142
|
|
|
122
143
|
const guard = await validateRouteMutationGuard({
|
|
123
144
|
container,
|
|
@@ -166,7 +187,7 @@ export const openApi = {
|
|
|
166
187
|
{ status: 400, description: 'Invalid channel id' },
|
|
167
188
|
{ status: 401, description: 'Unauthorized' },
|
|
168
189
|
{ status: 404, description: 'Channel not found' },
|
|
169
|
-
{ status: 409, description: 'Channel disabled or
|
|
190
|
+
{ status: 409, description: 'Channel disabled, not connected, or push-driven (never polled)' },
|
|
170
191
|
],
|
|
171
192
|
},
|
|
172
193
|
},
|