@open-mercato/core 0.6.8-develop.6986.1.3adb0d0df6 → 0.6.8-develop.6992.1.00c90fecff

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.
Files changed (50) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/components/detail/AssignRoleDialog.js +9 -3
  3. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  4. package/dist/modules/customers/components/detail/DealsSection.js +2 -3
  5. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  6. package/dist/modules/customers/components/detail/assignableStaff.js +2 -1
  7. package/dist/modules/customers/components/detail/assignableStaff.js.map +2 -2
  8. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js +11 -12
  9. package/dist/modules/customers/components/detail/schedule/LinkedEntitiesField.js.map +2 -2
  10. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js +7 -4
  11. package/dist/modules/customers/components/detail/schedule/ParticipantsField.js.map +2 -2
  12. package/dist/modules/data_sync/api/run.js +9 -1
  13. package/dist/modules/data_sync/api/run.js.map +2 -2
  14. package/dist/modules/data_sync/api/runs/[id]/retry.js +9 -1
  15. package/dist/modules/data_sync/api/runs/[id]/retry.js.map +2 -2
  16. package/dist/modules/data_sync/lib/adapter-registry.js +10 -1
  17. package/dist/modules/data_sync/lib/adapter-registry.js.map +2 -2
  18. package/dist/modules/data_sync/lib/start-cursor.js +17 -0
  19. package/dist/modules/data_sync/lib/start-cursor.js.map +7 -0
  20. package/dist/modules/data_sync/lib/sync-engine.js +84 -27
  21. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  22. package/dist/modules/data_sync/lib/sync-run-service.js +86 -10
  23. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  24. package/dist/modules/data_sync/workers/sync-scheduled.js +9 -6
  25. package/dist/modules/data_sync/workers/sync-scheduled.js.map +2 -2
  26. package/dist/modules/progress/lib/progressService.js +2 -0
  27. package/dist/modules/progress/lib/progressService.js.map +2 -2
  28. package/dist/modules/progress/lib/progressServiceImpl.js +78 -34
  29. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  30. package/dist/modules/sales/api/channels/route.js +1 -1
  31. package/dist/modules/sales/api/channels/route.js.map +2 -2
  32. package/package.json +7 -7
  33. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +12 -3
  34. package/src/modules/customers/components/detail/DealsSection.tsx +6 -11
  35. package/src/modules/customers/components/detail/assignableStaff.ts +9 -1
  36. package/src/modules/customers/components/detail/schedule/LinkedEntitiesField.tsx +16 -13
  37. package/src/modules/customers/components/detail/schedule/ParticipantsField.tsx +11 -4
  38. package/src/modules/data_sync/AGENTS.md +6 -1
  39. package/src/modules/data_sync/api/run.ts +9 -1
  40. package/src/modules/data_sync/api/runs/[id]/retry.ts +9 -1
  41. package/src/modules/data_sync/lib/adapter-registry.ts +15 -0
  42. package/src/modules/data_sync/lib/adapter.ts +18 -0
  43. package/src/modules/data_sync/lib/start-cursor.ts +35 -0
  44. package/src/modules/data_sync/lib/sync-engine.ts +101 -27
  45. package/src/modules/data_sync/lib/sync-run-service.ts +118 -10
  46. package/src/modules/data_sync/workers/sync-scheduled.ts +9 -6
  47. package/src/modules/progress/AGENTS.md +2 -1
  48. package/src/modules/progress/lib/progressService.ts +9 -0
  49. package/src/modules/progress/lib/progressServiceImpl.ts +111 -37
  50. package/src/modules/sales/api/channels/route.ts +1 -1
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { Users, X, Clock, CheckCircle2, XCircle } from "lucide-react";
5
5
  import { cn } from "@open-mercato/shared/lib/utils";
6
6
  import { useT } from "@open-mercato/shared/lib/i18n/context";
7
+ import { hasMoreFromPage } from "@open-mercato/shared/lib/pagination/load-more";
7
8
  import { Button } from "@open-mercato/ui/primitives/button";
8
9
  import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
9
10
  import { IconButton } from "@open-mercato/ui/primitives/icon-button";
@@ -12,6 +13,7 @@ import { SearchInput } from "@open-mercato/ui/primitives/search-input";
12
13
  import { fetchAssignableStaffMembersPage } from "../assignableStaff.js";
13
14
  import { isVisible, getFieldLabel } from "./fieldConfig.js";
14
15
  import { PARTICIPANT_COLORS } from "./useScheduleFormState.js";
16
+ const PAGE_SIZE = 20;
15
17
  function ParticipantSearchPopover({
16
18
  existingIds,
17
19
  onAdd,
@@ -22,7 +24,7 @@ function ParticipantSearchPopover({
22
24
  const [query, setQuery] = React.useState("");
23
25
  const [results, setResults] = React.useState([]);
24
26
  const [page, setPage] = React.useState(1);
25
- const [totalPages, setTotalPages] = React.useState(1);
27
+ const [hasMore, setHasMore] = React.useState(false);
26
28
  const [loading, setLoading] = React.useState(false);
27
29
  const [loadError, setLoadError] = React.useState(null);
28
30
  const selectableResults = React.useMemo(
@@ -33,7 +35,7 @@ function ParticipantSearchPopover({
33
35
  if (!open) return;
34
36
  const controller = new AbortController();
35
37
  setLoading(true);
36
- fetchAssignableStaffMembersPage(query, { page, pageSize: 20, signal: controller.signal }).then((result) => {
38
+ fetchAssignableStaffMembersPage(query, { page, pageSize: PAGE_SIZE, signal: controller.signal }).then((result) => {
37
39
  const members = result.items;
38
40
  const nextResults = members.map((member) => ({
39
41
  userId: member.userId,
@@ -46,10 +48,11 @@ function ParticipantSearchPopover({
46
48
  nextResults.forEach((entry) => merged.set(entry.userId, entry));
47
49
  return Array.from(merged.values());
48
50
  });
49
- setTotalPages(result.total > 0 ? Math.max(1, Math.ceil(result.total / result.pageSize)) : 1);
51
+ setHasMore(hasMoreFromPage(result.servedCount, PAGE_SIZE));
50
52
  setLoadError(null);
51
53
  }).catch(() => {
52
54
  setResults([]);
55
+ setHasMore(false);
53
56
  setLoadError(
54
57
  t(
55
58
  "customers.assignableStaff.loadError",
@@ -132,7 +135,7 @@ function ParticipantSearchPopover({
132
135
  r.userId
133
136
  );
134
137
  }),
135
- !loading && !loadError && page < totalPages ? /* @__PURE__ */ jsx("div", { className: "px-2 py-2", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", className: "w-full", onClick: () => setPage((current) => current + 1), children: t("customers.schedule.loadMore", "Load more") }) }) : null
138
+ !loading && !loadError && hasMore ? /* @__PURE__ */ jsx("div", { className: "px-2 py-2", children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "sm", className: "w-full", onClick: () => setPage((current) => current + 1), children: t("customers.schedule.loadMore", "Load more") }) }) : null
136
139
  ] })
137
140
  ] })
138
141
  ] });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/customers/components/detail/schedule/ParticipantsField.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Users, X, Clock, CheckCircle2, XCircle } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SearchInput } from '@open-mercato/ui/primitives/search-input'\nimport { fetchAssignableStaffMembersPage } from '../assignableStaff'\nimport type { ActivityType, ScheduleFieldId } from './fieldConfig'\nimport { isVisible, getFieldLabel } from './fieldConfig'\nimport type { Participant, RsvpStatus } from './useScheduleFormState'\nimport { PARTICIPANT_COLORS } from './useScheduleFormState'\n\nfunction ParticipantSearchPopover({\n existingIds,\n onAdd,\n onAddMany,\n t,\n}: {\n existingIds: Set<string>\n onAdd: (p: Participant) => void\n onAddMany: (participants: Participant[]) => void\n t: (key: string, fallback: string) => string\n}) {\n const [open, setOpen] = React.useState(false)\n const [query, setQuery] = React.useState('')\n const [results, setResults] = React.useState<Array<{ userId: string; name: string; email: string }>>([])\n const [page, setPage] = React.useState(1)\n const [totalPages, setTotalPages] = React.useState(1)\n const [loading, setLoading] = React.useState(false)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const selectableResults = React.useMemo(\n () => results.filter((result) => !existingIds.has(result.userId)),\n [existingIds, results],\n )\n\n React.useEffect(() => {\n if (!open) return\n const controller = new AbortController()\n setLoading(true)\n fetchAssignableStaffMembersPage(query, { page, pageSize: 20, signal: controller.signal })\n .then((result) => {\n const members = result.items\n const nextResults = members.map((member) => ({\n userId: member.userId,\n name: member.displayName,\n email: member.email ?? '',\n }))\n setResults((current) => {\n if (page <= 1) return nextResults\n const merged = new Map(current.map((entry) => [entry.userId, entry]))\n nextResults.forEach((entry) => merged.set(entry.userId, entry))\n return Array.from(merged.values())\n })\n setTotalPages(result.total > 0 ? Math.max(1, Math.ceil(result.total / result.pageSize)) : 1)\n setLoadError(null)\n })\n .catch(() => {\n setResults([])\n setLoadError(\n t(\n 'customers.assignableStaff.loadError',\n 'Unable to load team members. Check your permissions and try again.',\n ),\n )\n })\n .finally(() => setLoading(false))\n return () => controller.abort()\n }, [open, page, query, t])\n\n React.useEffect(() => {\n if (!open) return\n setPage(1)\n }, [open, query])\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"h-auto inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg px-2.5 py-1.5 text-xs font-semibold text-foreground\">\n <Users className=\"size-3\" />\n {t('customers.schedule.addParticipant', 'Add participant')}\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-72 p-2\">\n <SearchInput\n value={query}\n onChange={setQuery}\n placeholder={t('customers.schedule.searchParticipant', 'Search team members...')}\n className=\"mb-2\"\n autoFocus\n />\n {selectableResults.length ? (\n <div className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => {\n onAddMany(\n selectableResults.map((participant, index) => ({\n userId: participant.userId,\n name: participant.name,\n email: participant.email,\n color: PARTICIPANT_COLORS[(existingIds.size + index) % PARTICIPANT_COLORS.length],\n })),\n )\n setOpen(false)\n setQuery('')\n }}\n >\n {t('customers.schedule.addVisibleParticipants', 'Add all visible')}\n </Button>\n </div>\n ) : null}\n <div className=\"max-h-48 overflow-y-auto space-y-0.5\">\n {loading && <p className=\"px-2 py-3 text-xs text-muted-foreground text-center\">{t('customers.schedule.searching', 'Searching...')}</p>}\n {!loading && loadError && <p className=\"px-2 py-3 text-xs text-destructive text-center\">{loadError}</p>}\n {!loading && !loadError && results.length === 0 && <p className=\"px-2 py-3 text-xs text-muted-foreground text-center\">{t('customers.schedule.noResults', 'No results')}</p>}\n {results.map((r) => {\n const alreadyAdded = existingIds.has(r.userId)\n return (\n <Button\n key={r.userId}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={alreadyAdded}\n onClick={() => {\n onAdd({ userId: r.userId, name: r.name, email: r.email, color: PARTICIPANT_COLORS[existingIds.size % PARTICIPANT_COLORS.length] })\n setOpen(false)\n setQuery('')\n }}\n className={cn(\n 'h-auto flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors',\n alreadyAdded ? 'opacity-40 cursor-default' : 'hover:bg-accent cursor-pointer',\n )}\n >\n <span className=\"inline-flex size-6 items-center justify-center rounded-full bg-muted text-xs font-bold shrink-0\">\n {r.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"min-w-0 flex-1 truncate\">{r.name}</span>\n {r.email && <span className=\"text-xs text-muted-foreground truncate\">{r.email}</span>}\n </Button>\n )\n })}\n {!loading && !loadError && page < totalPages ? (\n <div className=\"px-2 py-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"w-full\" onClick={() => setPage((current) => current + 1)}>\n {t('customers.schedule.loadMore', 'Load more')}\n </Button>\n </div>\n ) : null}\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\ninterface ParticipantsFieldProps {\n visible: Set<ScheduleFieldId>\n activityType: ActivityType\n participants: Participant[]\n setParticipants: React.Dispatch<React.SetStateAction<Participant[]>>\n removeParticipant: (userId: string) => void\n guestPermissions: { canInviteOthers: boolean; canModify: boolean; canSeeList: boolean }\n setGuestPermissions: React.Dispatch<React.SetStateAction<{ canInviteOthers: boolean; canModify: boolean; canSeeList: boolean }>>\n}\n\nexport function ParticipantsField({\n visible,\n activityType,\n participants,\n setParticipants,\n removeParticipant,\n guestPermissions,\n setGuestPermissions,\n}: ParticipantsFieldProps) {\n const t = useT()\n\n if (!isVisible(activityType, 'participants')) return null\n\n const sectionLabel = getFieldLabel(\n activityType,\n 'participants',\n t,\n 'customers.schedule.participants',\n 'Participants',\n )\n\n return (\n <div>\n <label className=\"text-overline font-semibold uppercase text-muted-foreground tracking-wider\">\n {sectionLabel}\n </label>\n <div className=\"mt-2.5 flex flex-wrap content-center items-center gap-2 rounded-lg border border-border bg-background px-3 py-2.5\">\n {participants.map((p) => (\n <div key={p.userId} className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted px-2.5 py-1.5\">\n <span className={cn('inline-flex size-5 items-center justify-center rounded-full text-xs font-bold text-white', p.color ?? 'bg-primary')}>\n {p.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"text-xs text-foreground\">{p.name}</span>\n <IconButton type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => removeParticipant(p.userId)} className=\"h-auto text-muted-foreground hover:text-foreground p-0\" aria-label={t('customers.schedule.removeParticipant', 'Remove participant')}>\n <X className=\"size-3\" />\n </IconButton>\n </div>\n ))}\n <ParticipantSearchPopover\n existingIds={new Set(participants.map((p) => p.userId))}\n onAdd={(p) => setParticipants((prev) => [...prev, { ...p, status: 'pending' as RsvpStatus }])}\n onAddMany={(nextParticipants) => {\n setParticipants((prev) => [\n ...prev,\n ...nextParticipants.map((participant) => ({ ...participant, status: 'pending' as RsvpStatus })),\n ])\n }}\n t={t}\n />\n </div>\n\n {/* Guest permissions -- shown when participants exist */}\n {participants.length > 0 && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-[16px] gap-y-[6px] text-xs\">\n <span className=\"font-medium text-muted-foreground\">{t('customers.schedule.guestPermissions', 'Guest permissions:')}</span>\n <label htmlFor=\"guest-perm-invite\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-invite\" checked={guestPermissions.canInviteOthers} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canInviteOthers: checked === true }))} />\n {t('customers.schedule.guestPerm.invite', 'Invite others')}\n </label>\n <label htmlFor=\"guest-perm-modify\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-modify\" checked={guestPermissions.canModify} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canModify: checked === true }))} />\n {t('customers.schedule.guestPerm.modify', 'Modify')}\n </label>\n <label htmlFor=\"guest-perm-see-list\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-see-list\" checked={guestPermissions.canSeeList} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canSeeList: checked === true }))} />\n {t('customers.schedule.guestPerm.seeList', 'See list')}\n </label>\n </div>\n )}\n\n {/* RSVP summary -- shown when participants exist */}\n {participants.length > 0 && (() => {\n const accepted = participants.filter((p) => p.status === 'accepted').length\n const pending = participants.filter((p) => !p.status || p.status === 'pending').length\n const declined = participants.filter((p) => p.status === 'declined').length\n if (accepted === 0 && pending === 0 && declined === 0) return null\n return (\n <div className=\"mt-2 flex items-center gap-3 text-xs\">\n <span className=\"text-muted-foreground\">{t('customers.schedule.rsvp.label', 'Responses:')}</span>\n {accepted > 0 && <span className=\"flex items-center gap-1 font-medium text-status-success-text\"><CheckCircle2 className=\"size-3.5\" /> {accepted} {t('customers.schedule.rsvp.accepted', 'tak')}</span>}\n {pending > 0 && <span className=\"flex items-center gap-1 font-medium text-status-warning-text\"><Clock className=\"size-3.5\" /> {pending} {t('customers.schedule.rsvp.pending', 'czeka')}</span>}\n {declined > 0 && <span className=\"flex items-center gap-1 font-medium text-status-error-text\"><XCircle className=\"size-3.5\" /> {declined} {t('customers.schedule.rsvp.declined', 'nie')}</span>}\n </div>\n )\n })()}\n </div>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Users, X, Clock, CheckCircle2, XCircle } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { hasMoreFromPage } from '@open-mercato/shared/lib/pagination/load-more'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Checkbox } from '@open-mercato/ui/primitives/checkbox'\nimport { IconButton } from '@open-mercato/ui/primitives/icon-button'\nimport { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'\nimport { SearchInput } from '@open-mercato/ui/primitives/search-input'\nimport { fetchAssignableStaffMembersPage } from '../assignableStaff'\nimport type { ActivityType, ScheduleFieldId } from './fieldConfig'\nimport { isVisible, getFieldLabel } from './fieldConfig'\nimport type { Participant, RsvpStatus } from './useScheduleFormState'\nimport { PARTICIPANT_COLORS } from './useScheduleFormState'\n\nconst PAGE_SIZE = 20\n\nfunction ParticipantSearchPopover({\n existingIds,\n onAdd,\n onAddMany,\n t,\n}: {\n existingIds: Set<string>\n onAdd: (p: Participant) => void\n onAddMany: (participants: Participant[]) => void\n t: (key: string, fallback: string) => string\n}) {\n const [open, setOpen] = React.useState(false)\n const [query, setQuery] = React.useState('')\n const [results, setResults] = React.useState<Array<{ userId: string; name: string; email: string }>>([])\n const [page, setPage] = React.useState(1)\n // Short-page termination instead of a `total`-derived page bound \u2014 see\n // `hasMoreFromPage`. Measured on the served count, not on `members`, which is\n // deduped by user id.\n const [hasMore, setHasMore] = React.useState(false)\n const [loading, setLoading] = React.useState(false)\n const [loadError, setLoadError] = React.useState<string | null>(null)\n const selectableResults = React.useMemo(\n () => results.filter((result) => !existingIds.has(result.userId)),\n [existingIds, results],\n )\n\n React.useEffect(() => {\n if (!open) return\n const controller = new AbortController()\n setLoading(true)\n fetchAssignableStaffMembersPage(query, { page, pageSize: PAGE_SIZE, signal: controller.signal })\n .then((result) => {\n const members = result.items\n const nextResults = members.map((member) => ({\n userId: member.userId,\n name: member.displayName,\n email: member.email ?? '',\n }))\n setResults((current) => {\n if (page <= 1) return nextResults\n const merged = new Map(current.map((entry) => [entry.userId, entry]))\n nextResults.forEach((entry) => merged.set(entry.userId, entry))\n return Array.from(merged.values())\n })\n setHasMore(hasMoreFromPage(result.servedCount, PAGE_SIZE))\n setLoadError(null)\n })\n .catch(() => {\n setResults([])\n setHasMore(false)\n setLoadError(\n t(\n 'customers.assignableStaff.loadError',\n 'Unable to load team members. Check your permissions and try again.',\n ),\n )\n })\n .finally(() => setLoading(false))\n return () => controller.abort()\n }, [open, page, query, t])\n\n React.useEffect(() => {\n if (!open) return\n setPage(1)\n }, [open, query])\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"h-auto inline-flex items-center gap-1.5 rounded-full border border-status-success-border bg-status-success-bg px-2.5 py-1.5 text-xs font-semibold text-foreground\">\n <Users className=\"size-3\" />\n {t('customers.schedule.addParticipant', 'Add participant')}\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-72 p-2\">\n <SearchInput\n value={query}\n onChange={setQuery}\n placeholder={t('customers.schedule.searchParticipant', 'Search team members...')}\n className=\"mb-2\"\n autoFocus\n />\n {selectableResults.length ? (\n <div className=\"mb-2\">\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => {\n onAddMany(\n selectableResults.map((participant, index) => ({\n userId: participant.userId,\n name: participant.name,\n email: participant.email,\n color: PARTICIPANT_COLORS[(existingIds.size + index) % PARTICIPANT_COLORS.length],\n })),\n )\n setOpen(false)\n setQuery('')\n }}\n >\n {t('customers.schedule.addVisibleParticipants', 'Add all visible')}\n </Button>\n </div>\n ) : null}\n <div className=\"max-h-48 overflow-y-auto space-y-0.5\">\n {loading && <p className=\"px-2 py-3 text-xs text-muted-foreground text-center\">{t('customers.schedule.searching', 'Searching...')}</p>}\n {!loading && loadError && <p className=\"px-2 py-3 text-xs text-destructive text-center\">{loadError}</p>}\n {!loading && !loadError && results.length === 0 && <p className=\"px-2 py-3 text-xs text-muted-foreground text-center\">{t('customers.schedule.noResults', 'No results')}</p>}\n {results.map((r) => {\n const alreadyAdded = existingIds.has(r.userId)\n return (\n <Button\n key={r.userId}\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={alreadyAdded}\n onClick={() => {\n onAdd({ userId: r.userId, name: r.name, email: r.email, color: PARTICIPANT_COLORS[existingIds.size % PARTICIPANT_COLORS.length] })\n setOpen(false)\n setQuery('')\n }}\n className={cn(\n 'h-auto flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors',\n alreadyAdded ? 'opacity-40 cursor-default' : 'hover:bg-accent cursor-pointer',\n )}\n >\n <span className=\"inline-flex size-6 items-center justify-center rounded-full bg-muted text-xs font-bold shrink-0\">\n {r.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"min-w-0 flex-1 truncate\">{r.name}</span>\n {r.email && <span className=\"text-xs text-muted-foreground truncate\">{r.email}</span>}\n </Button>\n )\n })}\n {!loading && !loadError && hasMore ? (\n <div className=\"px-2 py-2\">\n <Button type=\"button\" variant=\"outline\" size=\"sm\" className=\"w-full\" onClick={() => setPage((current) => current + 1)}>\n {t('customers.schedule.loadMore', 'Load more')}\n </Button>\n </div>\n ) : null}\n </div>\n </PopoverContent>\n </Popover>\n )\n}\n\ninterface ParticipantsFieldProps {\n visible: Set<ScheduleFieldId>\n activityType: ActivityType\n participants: Participant[]\n setParticipants: React.Dispatch<React.SetStateAction<Participant[]>>\n removeParticipant: (userId: string) => void\n guestPermissions: { canInviteOthers: boolean; canModify: boolean; canSeeList: boolean }\n setGuestPermissions: React.Dispatch<React.SetStateAction<{ canInviteOthers: boolean; canModify: boolean; canSeeList: boolean }>>\n}\n\nexport function ParticipantsField({\n visible,\n activityType,\n participants,\n setParticipants,\n removeParticipant,\n guestPermissions,\n setGuestPermissions,\n}: ParticipantsFieldProps) {\n const t = useT()\n\n if (!isVisible(activityType, 'participants')) return null\n\n const sectionLabel = getFieldLabel(\n activityType,\n 'participants',\n t,\n 'customers.schedule.participants',\n 'Participants',\n )\n\n return (\n <div>\n <label className=\"text-overline font-semibold uppercase text-muted-foreground tracking-wider\">\n {sectionLabel}\n </label>\n <div className=\"mt-2.5 flex flex-wrap content-center items-center gap-2 rounded-lg border border-border bg-background px-3 py-2.5\">\n {participants.map((p) => (\n <div key={p.userId} className=\"inline-flex items-center gap-1.5 rounded-full border border-border bg-muted px-2.5 py-1.5\">\n <span className={cn('inline-flex size-5 items-center justify-center rounded-full text-xs font-bold text-white', p.color ?? 'bg-primary')}>\n {p.name.charAt(0).toUpperCase()}\n </span>\n <span className=\"text-xs text-foreground\">{p.name}</span>\n <IconButton type=\"button\" variant=\"ghost\" size=\"sm\" onClick={() => removeParticipant(p.userId)} className=\"h-auto text-muted-foreground hover:text-foreground p-0\" aria-label={t('customers.schedule.removeParticipant', 'Remove participant')}>\n <X className=\"size-3\" />\n </IconButton>\n </div>\n ))}\n <ParticipantSearchPopover\n existingIds={new Set(participants.map((p) => p.userId))}\n onAdd={(p) => setParticipants((prev) => [...prev, { ...p, status: 'pending' as RsvpStatus }])}\n onAddMany={(nextParticipants) => {\n setParticipants((prev) => [\n ...prev,\n ...nextParticipants.map((participant) => ({ ...participant, status: 'pending' as RsvpStatus })),\n ])\n }}\n t={t}\n />\n </div>\n\n {/* Guest permissions -- shown when participants exist */}\n {participants.length > 0 && (\n <div className=\"mt-3 flex flex-wrap items-center gap-x-[16px] gap-y-[6px] text-xs\">\n <span className=\"font-medium text-muted-foreground\">{t('customers.schedule.guestPermissions', 'Guest permissions:')}</span>\n <label htmlFor=\"guest-perm-invite\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-invite\" checked={guestPermissions.canInviteOthers} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canInviteOthers: checked === true }))} />\n {t('customers.schedule.guestPerm.invite', 'Invite others')}\n </label>\n <label htmlFor=\"guest-perm-modify\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-modify\" checked={guestPermissions.canModify} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canModify: checked === true }))} />\n {t('customers.schedule.guestPerm.modify', 'Modify')}\n </label>\n <label htmlFor=\"guest-perm-see-list\" className=\"flex items-center gap-1.5 cursor-pointer\">\n <Checkbox id=\"guest-perm-see-list\" checked={guestPermissions.canSeeList} onCheckedChange={(checked) => setGuestPermissions((p) => ({ ...p, canSeeList: checked === true }))} />\n {t('customers.schedule.guestPerm.seeList', 'See list')}\n </label>\n </div>\n )}\n\n {/* RSVP summary -- shown when participants exist */}\n {participants.length > 0 && (() => {\n const accepted = participants.filter((p) => p.status === 'accepted').length\n const pending = participants.filter((p) => !p.status || p.status === 'pending').length\n const declined = participants.filter((p) => p.status === 'declined').length\n if (accepted === 0 && pending === 0 && declined === 0) return null\n return (\n <div className=\"mt-2 flex items-center gap-3 text-xs\">\n <span className=\"text-muted-foreground\">{t('customers.schedule.rsvp.label', 'Responses:')}</span>\n {accepted > 0 && <span className=\"flex items-center gap-1 font-medium text-status-success-text\"><CheckCircle2 className=\"size-3.5\" /> {accepted} {t('customers.schedule.rsvp.accepted', 'tak')}</span>}\n {pending > 0 && <span className=\"flex items-center gap-1 font-medium text-status-warning-text\"><Clock className=\"size-3.5\" /> {pending} {t('customers.schedule.rsvp.pending', 'czeka')}</span>}\n {declined > 0 && <span className=\"flex items-center gap-1 font-medium text-status-error-text\"><XCircle className=\"size-3.5\" /> {declined} {t('customers.schedule.rsvp.declined', 'nie')}</span>}\n </div>\n )\n })()}\n </div>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -7,6 +7,7 @@ import { getIntegration } from "@open-mercato/shared/modules/integrations/types"
7
7
  import { runSyncSchema } from "../data/validators.js";
8
8
  import { startDataSyncRun } from "../lib/start-run.js";
9
9
  import { getDataSyncAdapter } from "../lib/adapter-registry.js";
10
+ import { resolveStartCursor } from "../lib/start-cursor.js";
10
11
  import {
11
12
  runCrudMutationGuardAfterSuccess,
12
13
  validateCrudMutationGuard
@@ -90,7 +91,14 @@ async function POST(req) {
90
91
  if (guardResult && !guardResult.ok) {
91
92
  return NextResponse.json(guardResult.body, { status: guardResult.status });
92
93
  }
93
- const cursor = parsed.data.fullSync ? null : await syncRunService.resolveCursor(parsed.data.integrationId, parsed.data.entityType, parsed.data.direction, scope);
94
+ const cursor = parsed.data.fullSync ? null : await resolveStartCursor({
95
+ syncRunService,
96
+ adapter,
97
+ integrationId: parsed.data.integrationId,
98
+ entityType: parsed.data.entityType,
99
+ direction: parsed.data.direction,
100
+ scope
101
+ });
94
102
  const { run, progressJob } = await startDataSyncRun({
95
103
  syncRunService,
96
104
  progressService,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/api/run.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport { runSyncSchema } from '../data/validators'\nimport { startDataSyncRun } from '../lib/start-run'\nimport { getDataSyncAdapter } from '../lib/adapter-registry'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'run' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n\nexport const openApi = {\n tags: ['DataSync'],\n summary: 'Start a data sync run',\n}\n\nexport async function POST(req: Request) {\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const payload = await readJsonSafe(req)\n const parsed = runSyncSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressService = container.resolve('progressService') as ProgressService\n const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService\n\n const scope = {\n organizationId,\n tenantId: auth.tenantId,\n }\n\n const integration = getIntegration(parsed.data.integrationId)\n if (!integration?.providerKey) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const adapter = getDataSyncAdapter(integration.providerKey)\n if (!adapter) {\n return NextResponse.json({ error: 'No registered sync adapter for provider' }, { status: 404 })\n }\n if (adapter.runMode === 'provider') {\n return NextResponse.json(\n {\n error: 'This integration must be started from its provider-specific import flow.',\n settingsPath: `/backend/integrations/${encodeURIComponent(parsed.data.integrationId)}`,\n },\n { status: 422 },\n )\n }\n\n if (!adapter.supportedEntities.includes(parsed.data.entityType)) {\n return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })\n }\n\n const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)\n if (!integrationEnabled) {\n return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n parsed.data.integrationId,\n parsed.data.entityType,\n parsed.data.direction,\n scope,\n )\n if (overlap) {\n return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: parsed.data.integrationId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const cursor = parsed.data.fullSync\n ? null\n : await syncRunService.resolveCursor(parsed.data.integrationId, parsed.data.entityType, parsed.data.direction, scope)\n\n const { run, progressJob } = await startDataSyncRun({\n syncRunService,\n progressService,\n scope: {\n ...scope,\n userId: auth.sub,\n },\n input: {\n integrationId: parsed.data.integrationId,\n entityType: parsed.data.entityType,\n direction: parsed.data.direction,\n cursor,\n triggeredBy: parsed.data.triggeredBy ?? auth.sub,\n batchSize: parsed.data.batchSize,\n },\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: run.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n const stack = error instanceof Error ? error.stack : undefined\n logger.error('Unhandled error starting sync run', { message, stack })\n return NextResponse.json(\n { error: 'Failed to start data sync run.' },\n { status: 500 },\n )\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 { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { ProgressService } from '../../progress/lib/progressService'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { SyncRunService } from '../lib/sync-run-service'\nimport { runSyncSchema } from '../data/validators'\nimport { startDataSyncRun } from '../lib/start-run'\nimport { getDataSyncAdapter } from '../lib/adapter-registry'\nimport { resolveStartCursor } from '../lib/start-cursor'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('data_sync').child({ component: 'run' })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n\nexport const openApi = {\n tags: ['DataSync'],\n summary: 'Start a data sync run',\n}\n\nexport async function POST(req: Request) {\n try {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const payload = await readJsonSafe(req)\n const parsed = runSyncSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressService = container.resolve('progressService') as ProgressService\n const integrationStateService = container.resolve('integrationStateService') as IntegrationStateService\n\n const scope = {\n organizationId,\n tenantId: auth.tenantId,\n }\n\n const integration = getIntegration(parsed.data.integrationId)\n if (!integration?.providerKey) {\n return NextResponse.json({ error: 'Integration not found' }, { status: 404 })\n }\n\n const adapter = getDataSyncAdapter(integration.providerKey)\n if (!adapter) {\n return NextResponse.json({ error: 'No registered sync adapter for provider' }, { status: 404 })\n }\n if (adapter.runMode === 'provider') {\n return NextResponse.json(\n {\n error: 'This integration must be started from its provider-specific import flow.',\n settingsPath: `/backend/integrations/${encodeURIComponent(parsed.data.integrationId)}`,\n },\n { status: 422 },\n )\n }\n\n if (!adapter.supportedEntities.includes(parsed.data.entityType)) {\n return NextResponse.json({ error: 'Unsupported entity type for this integration' }, { status: 422 })\n }\n\n const integrationEnabled = await integrationStateService.isEnabled(parsed.data.integrationId, scope)\n if (!integrationEnabled) {\n return NextResponse.json({ error: 'Integration is disabled' }, { status: 409 })\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n parsed.data.integrationId,\n parsed.data.entityType,\n parsed.data.direction,\n scope,\n )\n if (overlap) {\n return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: parsed.data.integrationId,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data,\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const cursor = parsed.data.fullSync\n ? null\n : await resolveStartCursor({\n syncRunService,\n adapter,\n integrationId: parsed.data.integrationId,\n entityType: parsed.data.entityType,\n direction: parsed.data.direction,\n scope,\n })\n\n const { run, progressJob } = await startDataSyncRun({\n syncRunService,\n progressService,\n scope: {\n ...scope,\n userId: auth.sub,\n },\n input: {\n integrationId: parsed.data.integrationId,\n entityType: parsed.data.entityType,\n direction: parsed.data.direction,\n cursor,\n triggeredBy: parsed.data.triggeredBy ?? auth.sub,\n batchSize: parsed.data.batchSize,\n },\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: run.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n const stack = error instanceof Error ? error.stack : undefined\n logger.error('Unhandled error starting sync run', { message, stack })\n return NextResponse.json(\n { error: 'Failed to start data sync run.' },\n { status: 500 },\n )\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,6 +6,7 @@ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
6
6
  import { readJsonSafe } from "@open-mercato/shared/lib/http/readJsonSafe";
7
7
  import { retrySyncSchema } from "../../../data/validators.js";
8
8
  import { startDataSyncRun } from "../../../lib/start-run.js";
9
+ import { resolveAdapterForIntegration, resolveStartCursor } from "../../../lib/start-cursor.js";
9
10
  import {
10
11
  runCrudMutationGuardAfterSuccess,
11
12
  validateCrudMutationGuard
@@ -71,7 +72,14 @@ async function POST(req, ctx) {
71
72
  if (guardResult && !guardResult.ok) {
72
73
  return NextResponse.json(guardResult.body, { status: guardResult.status });
73
74
  }
74
- const cursor = parsedBody.data.fromBeginning ? null : previous.cursor ?? await syncRunService.resolveCursor(previous.integrationId, previous.entityType, previous.direction, scope);
75
+ const cursor = parsedBody.data.fromBeginning ? null : previous.cursor ?? await resolveStartCursor({
76
+ syncRunService,
77
+ adapter: resolveAdapterForIntegration(previous.integrationId),
78
+ integrationId: previous.integrationId,
79
+ entityType: previous.entityType,
80
+ direction: previous.direction,
81
+ scope
82
+ });
75
83
  const { run, progressJob } = await startDataSyncRun({
76
84
  syncRunService,
77
85
  progressService,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/data_sync/api/runs/%5Bid%5D/retry.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { ProgressService } from '../../../../progress/lib/progressService'\nimport type { SyncRunService } from '../../../lib/sync-run-service'\nimport { retrySyncSchema } from '../../../data/validators'\nimport { startDataSyncRun } from '../../../lib/start-run'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\n\nconst paramsSchema = z.object({ id: z.string().uuid() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n\nexport const openApi = {\n tags: ['DataSync'],\n summary: 'Retry a failed sync run',\n}\n\nexport async function POST(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')\n ? await (ctx.params as Promise<{ id?: string }>)\n : (ctx.params as { id?: string } | undefined)\n\n const parsedParams = paramsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid run id' }, { status: 400 })\n }\n\n const payload = await readJsonSafe(req)\n const parsedBody = retrySyncSchema.safeParse(payload ?? {})\n if (!parsedBody.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsedBody.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressService = container.resolve('progressService') as ProgressService\n const scope = { organizationId, tenantId: auth.tenantId }\n\n const previous = await syncRunService.getRun(parsedParams.data.id, scope)\n if (!previous) {\n return NextResponse.json({ error: 'Run not found' }, { status: 404 })\n }\n if (previous.status !== 'failed' && previous.status !== 'cancelled') {\n return NextResponse.json({ error: 'Only failed or cancelled runs can be retried' }, { status: 409 })\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n previous.integrationId,\n previous.entityType,\n previous.direction,\n scope,\n )\n if (overlap) {\n return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: previous.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { action: 'retry', ...parsedBody.data },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const cursor = parsedBody.data.fromBeginning\n ? null\n : previous.cursor ?? await syncRunService.resolveCursor(previous.integrationId, previous.entityType, previous.direction, scope)\n\n const { run, progressJob } = await startDataSyncRun({\n syncRunService,\n progressService,\n scope: {\n ...scope,\n userId: auth.sub,\n },\n input: {\n integrationId: previous.integrationId,\n entityType: previous.entityType,\n direction: previous.direction,\n cursor,\n triggeredBy: auth.sub,\n batchSize: 100,\n progressJob: {\n name: `Retry data sync ${previous.integrationId} \u2014 ${previous.entityType}`,\n },\n },\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: run.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { organizationScopeRequiredResponse, resolveActiveOrganizationId } from '@open-mercato/shared/lib/auth/organizationScope'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport type { ProgressService } from '../../../../progress/lib/progressService'\nimport type { SyncRunService } from '../../../lib/sync-run-service'\nimport { retrySyncSchema } from '../../../data/validators'\nimport { startDataSyncRun } from '../../../lib/start-run'\nimport { resolveAdapterForIntegration, resolveStartCursor } from '../../../lib/start-cursor'\nimport {\n runCrudMutationGuardAfterSuccess,\n validateCrudMutationGuard,\n} from '@open-mercato/shared/lib/crud/mutation-guard'\n\nconst paramsSchema = z.object({ id: z.string().uuid() })\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['data_sync.run'] },\n}\n\nexport const openApi = {\n tags: ['DataSync'],\n summary: 'Retry a failed sync run',\n}\n\nexport async function POST(req: Request, ctx: { params?: Promise<{ id?: string }> | { id?: string } }) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n const organizationId = resolveActiveOrganizationId(auth)\n if (!organizationId) {\n return organizationScopeRequiredResponse()\n }\n\n const rawParams = (ctx.params && typeof (ctx.params as Promise<unknown>).then === 'function')\n ? await (ctx.params as Promise<{ id?: string }>)\n : (ctx.params as { id?: string } | undefined)\n\n const parsedParams = paramsSchema.safeParse(rawParams)\n if (!parsedParams.success) {\n return NextResponse.json({ error: 'Invalid run id' }, { status: 400 })\n }\n\n const payload = await readJsonSafe(req)\n const parsedBody = retrySyncSchema.safeParse(payload ?? {})\n if (!parsedBody.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsedBody.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const syncRunService = container.resolve('dataSyncRunService') as SyncRunService\n const progressService = container.resolve('progressService') as ProgressService\n const scope = { organizationId, tenantId: auth.tenantId }\n\n const previous = await syncRunService.getRun(parsedParams.data.id, scope)\n if (!previous) {\n return NextResponse.json({ error: 'Run not found' }, { status: 404 })\n }\n if (previous.status !== 'failed' && previous.status !== 'cancelled') {\n return NextResponse.json({ error: 'Only failed or cancelled runs can be retried' }, { status: 409 })\n }\n\n const overlap = await syncRunService.findRunningOverlap(\n previous.integrationId,\n previous.entityType,\n previous.direction,\n scope,\n )\n if (overlap) {\n return NextResponse.json({ error: 'A sync run is already in progress for this integration and entity direction' }, { status: 409 })\n }\n\n const guardResult = await validateCrudMutationGuard(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: previous.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: { action: 'retry', ...parsedBody.data },\n })\n if (guardResult && !guardResult.ok) {\n return NextResponse.json(guardResult.body, { status: guardResult.status })\n }\n\n const cursor = parsedBody.data.fromBeginning\n ? null\n : previous.cursor ?? await resolveStartCursor({\n syncRunService,\n adapter: resolveAdapterForIntegration(previous.integrationId),\n integrationId: previous.integrationId,\n entityType: previous.entityType,\n direction: previous.direction,\n scope,\n })\n\n const { run, progressJob } = await startDataSyncRun({\n syncRunService,\n progressService,\n scope: {\n ...scope,\n userId: auth.sub,\n },\n input: {\n integrationId: previous.integrationId,\n entityType: previous.entityType,\n direction: previous.direction,\n cursor,\n triggeredBy: auth.sub,\n batchSize: 100,\n progressJob: {\n name: `Retry data sync ${previous.integrationId} \u2014 ${previous.entityType}`,\n },\n },\n })\n\n if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {\n await runCrudMutationGuardAfterSuccess(container, {\n tenantId: auth.tenantId,\n organizationId: scope.organizationId,\n userId: auth.sub,\n resourceKind: 'data_sync.run',\n resourceId: run.id,\n operation: 'custom',\n requestMethod: req.method,\n requestHeaders: req.headers,\n metadata: guardResult.metadata ?? null,\n })\n }\n\n return NextResponse.json({ id: run.id, progressJobId: progressJob?.id ?? null }, { status: 201 })\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { getIntegration } from "@open-mercato/shared/modules/integrations/types";
1
2
  const DATA_SYNC_ADAPTER_REGISTRY_KEY = /* @__PURE__ */ Symbol.for("@open-mercato/data-sync/adapter-registry");
2
3
  function getAdapterRegistry() {
3
4
  const globalScope = globalThis;
@@ -15,9 +16,17 @@ function getDataSyncAdapter(providerKey) {
15
16
  function getAllDataSyncAdapters() {
16
17
  return Array.from(getAdapterRegistry().values());
17
18
  }
19
+ function resolveProviderKey(integrationId) {
20
+ return getIntegration(integrationId)?.providerKey ?? integrationId;
21
+ }
22
+ function resolveAdapterForIntegration(integrationId) {
23
+ return getDataSyncAdapter(resolveProviderKey(integrationId)) ?? null;
24
+ }
18
25
  export {
19
26
  getAllDataSyncAdapters,
20
27
  getDataSyncAdapter,
21
- registerDataSyncAdapter
28
+ registerDataSyncAdapter,
29
+ resolveAdapterForIntegration,
30
+ resolveProviderKey
22
31
  };
23
32
  //# sourceMappingURL=adapter-registry.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/data_sync/lib/adapter-registry.ts"],
4
- "sourcesContent": ["import type { DataSyncAdapter } from './adapter'\n\nconst DATA_SYNC_ADAPTER_REGISTRY_KEY = Symbol.for('@open-mercato/data-sync/adapter-registry')\n\ntype GlobalWithDataSyncRegistry = typeof globalThis & {\n [DATA_SYNC_ADAPTER_REGISTRY_KEY]?: Map<string, DataSyncAdapter>\n}\n\nfunction getAdapterRegistry(): Map<string, DataSyncAdapter> {\n const globalScope = globalThis as GlobalWithDataSyncRegistry\n if (!globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY]) {\n globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY] = new Map<string, DataSyncAdapter>()\n }\n return globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY]\n}\n\nexport function registerDataSyncAdapter(adapter: DataSyncAdapter): void {\n getAdapterRegistry().set(adapter.providerKey, adapter)\n}\n\nexport function getDataSyncAdapter(providerKey: string): DataSyncAdapter | undefined {\n return getAdapterRegistry().get(providerKey)\n}\n\nexport function getAllDataSyncAdapters(): DataSyncAdapter[] {\n return Array.from(getAdapterRegistry().values())\n}\n"],
5
- "mappings": "AAEA,MAAM,iCAAiC,uBAAO,IAAI,0CAA0C;AAM5F,SAAS,qBAAmD;AAC1D,QAAM,cAAc;AACpB,MAAI,CAAC,YAAY,8BAA8B,GAAG;AAChD,gBAAY,8BAA8B,IAAI,oBAAI,IAA6B;AAAA,EACjF;AACA,SAAO,YAAY,8BAA8B;AACnD;AAEO,SAAS,wBAAwB,SAAgC;AACtE,qBAAmB,EAAE,IAAI,QAAQ,aAAa,OAAO;AACvD;AAEO,SAAS,mBAAmB,aAAkD;AACnF,SAAO,mBAAmB,EAAE,IAAI,WAAW;AAC7C;AAEO,SAAS,yBAA4C;AAC1D,SAAO,MAAM,KAAK,mBAAmB,EAAE,OAAO,CAAC;AACjD;",
4
+ "sourcesContent": ["import { getIntegration } from '@open-mercato/shared/modules/integrations/types'\nimport type { DataSyncAdapter } from './adapter'\n\nconst DATA_SYNC_ADAPTER_REGISTRY_KEY = Symbol.for('@open-mercato/data-sync/adapter-registry')\n\ntype GlobalWithDataSyncRegistry = typeof globalThis & {\n [DATA_SYNC_ADAPTER_REGISTRY_KEY]?: Map<string, DataSyncAdapter>\n}\n\nfunction getAdapterRegistry(): Map<string, DataSyncAdapter> {\n const globalScope = globalThis as GlobalWithDataSyncRegistry\n if (!globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY]) {\n globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY] = new Map<string, DataSyncAdapter>()\n }\n return globalScope[DATA_SYNC_ADAPTER_REGISTRY_KEY]\n}\n\nexport function registerDataSyncAdapter(adapter: DataSyncAdapter): void {\n getAdapterRegistry().set(adapter.providerKey, adapter)\n}\n\nexport function getDataSyncAdapter(providerKey: string): DataSyncAdapter | undefined {\n return getAdapterRegistry().get(providerKey)\n}\n\nexport function getAllDataSyncAdapters(): DataSyncAdapter[] {\n return Array.from(getAdapterRegistry().values())\n}\n\nexport function resolveProviderKey(integrationId: string): string {\n return getIntegration(integrationId)?.providerKey ?? integrationId\n}\n\n/**\n * The adapter serving an integration. Single source of truth on purpose: the\n * engine uses it to decide whether to WRITE the shared cursor row and the start\n * paths use it to decide whether to READ one, and those two decisions must agree\n * for every integration or an opted-out entity type silently resumes wrong.\n */\nexport function resolveAdapterForIntegration(integrationId: string): DataSyncAdapter | null {\n return getDataSyncAdapter(resolveProviderKey(integrationId)) ?? null\n}\n"],
5
+ "mappings": "AAAA,SAAS,sBAAsB;AAG/B,MAAM,iCAAiC,uBAAO,IAAI,0CAA0C;AAM5F,SAAS,qBAAmD;AAC1D,QAAM,cAAc;AACpB,MAAI,CAAC,YAAY,8BAA8B,GAAG;AAChD,gBAAY,8BAA8B,IAAI,oBAAI,IAA6B;AAAA,EACjF;AACA,SAAO,YAAY,8BAA8B;AACnD;AAEO,SAAS,wBAAwB,SAAgC;AACtE,qBAAmB,EAAE,IAAI,QAAQ,aAAa,OAAO;AACvD;AAEO,SAAS,mBAAmB,aAAkD;AACnF,SAAO,mBAAmB,EAAE,IAAI,WAAW;AAC7C;AAEO,SAAS,yBAA4C;AAC1D,SAAO,MAAM,KAAK,mBAAmB,EAAE,OAAO,CAAC;AACjD;AAEO,SAAS,mBAAmB,eAA+B;AAChE,SAAO,eAAe,aAAa,GAAG,eAAe;AACvD;AAQO,SAAS,6BAA6B,eAA+C;AAC1F,SAAO,mBAAmB,mBAAmB,aAAa,CAAC,KAAK;AAClE;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,17 @@
1
+ import { resolveAdapterForIntegration } from "./adapter-registry.js";
2
+ function persistsSharedCursor(adapter, entityType) {
3
+ return adapter?.persistsSharedCursor?.(entityType) ?? true;
4
+ }
5
+ async function resolveStartCursor(params) {
6
+ const { syncRunService, adapter, integrationId, entityType, direction, scope } = params;
7
+ if (persistsSharedCursor(adapter, entityType)) {
8
+ return syncRunService.resolveCursor(integrationId, entityType, direction, scope);
9
+ }
10
+ return syncRunService.resolveResumeCursor(integrationId, entityType, direction, scope);
11
+ }
12
+ export {
13
+ persistsSharedCursor,
14
+ resolveAdapterForIntegration,
15
+ resolveStartCursor
16
+ };
17
+ //# sourceMappingURL=start-cursor.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/data_sync/lib/start-cursor.ts"],
4
+ "sourcesContent": ["import type { DataSyncAdapter } from './adapter'\nimport type { SyncRunService } from './sync-run-service'\n\ntype SyncScope = {\n organizationId: string\n tenantId: string\n}\n\nexport { resolveAdapterForIntegration } from './adapter-registry'\n\nexport function persistsSharedCursor(adapter: DataSyncAdapter | null | undefined, entityType: string): boolean {\n return adapter?.persistsSharedCursor?.(entityType) ?? true\n}\n\n/**\n * Start position for a non-full run. Entity types that mirror their cursor into\n * the shared `sync_cursors` row read it from there. Entity types whose adapter\n * opted out never write that row, so reading it would silently turn every\n * incremental run into a full one \u2014 they resume from their own last run\n * instead.\n */\nexport async function resolveStartCursor(params: {\n syncRunService: SyncRunService\n adapter?: DataSyncAdapter | null\n integrationId: string\n entityType: string\n direction: 'import' | 'export'\n scope: SyncScope\n}): Promise<string | null> {\n const { syncRunService, adapter, integrationId, entityType, direction, scope } = params\n if (persistsSharedCursor(adapter, entityType)) {\n return syncRunService.resolveCursor(integrationId, entityType, direction, scope)\n }\n return syncRunService.resolveResumeCursor(integrationId, entityType, direction, scope)\n}\n"],
5
+ "mappings": "AAQA,SAAS,oCAAoC;AAEtC,SAAS,qBAAqB,SAA6C,YAA6B;AAC7G,SAAO,SAAS,uBAAuB,UAAU,KAAK;AACxD;AASA,eAAsB,mBAAmB,QAOd;AACzB,QAAM,EAAE,gBAAgB,SAAS,eAAe,YAAY,WAAW,MAAM,IAAI;AACjF,MAAI,qBAAqB,SAAS,UAAU,GAAG;AAC7C,WAAO,eAAe,cAAc,eAAe,YAAY,WAAW,KAAK;AAAA,EACjF;AACA,SAAO,eAAe,oBAAoB,eAAe,YAAY,WAAW,KAAK;AACvF;",
6
+ "names": []
7
+ }
@@ -1,13 +1,10 @@
1
- import { getIntegration } from "@open-mercato/shared/modules/integrations/types";
1
+ import { STALE_JOB_TIMEOUT_SECONDS } from "../../progress/lib/progressService.js";
2
2
  import { refreshCoverageSnapshot } from "../../query_index/lib/coverage.js";
3
3
  import { emitDataSyncEvent } from "../events.js";
4
- import { getDataSyncAdapter } from "./adapter-registry.js";
4
+ import { getDataSyncAdapter, resolveProviderKey } from "./adapter-registry.js";
5
5
  import { SyncRunOwnershipConflictError } from "./sync-run-service.js";
6
6
  import { createLogger } from "@open-mercato/shared/lib/logger";
7
7
  const logger = createLogger("data_sync").child({ component: "sync-engine" });
8
- function resolveProviderKey(integrationId) {
9
- return getIntegration(integrationId)?.providerKey ?? integrationId;
10
- }
11
8
  function applyImportCounters(batch) {
12
9
  let createdCount = 0;
13
10
  let updatedCount = 0;
@@ -37,6 +34,25 @@ function applyExportCounters(batch) {
37
34
  processedCount: batch.results.length
38
35
  };
39
36
  }
37
+ const HEARTBEAT_TICK_MS = STALE_JOB_TIMEOUT_SECONDS * 1e3 / 4;
38
+ async function* withHeartbeat(source, tick, intervalMs) {
39
+ const iterator = source[Symbol.asyncIterator]();
40
+ try {
41
+ while (true) {
42
+ const timer = setInterval(tick, intervalMs);
43
+ let result;
44
+ try {
45
+ result = await iterator.next();
46
+ } finally {
47
+ clearInterval(timer);
48
+ }
49
+ if (result.done) return;
50
+ yield result.value;
51
+ }
52
+ } finally {
53
+ await iterator.return?.();
54
+ }
55
+ }
40
56
  function createSyncEngine(deps) {
41
57
  const { syncRunService, integrationCredentialsService, integrationLogService, integrationStateService, progressService } = deps;
42
58
  async function resolveMapping(adapter, entityType, scope) {
@@ -60,6 +76,37 @@ function createSyncEngine(deps) {
60
76
  }
61
77
  );
62
78
  }
79
+ async function seedProcessedCount(progressJobId, scope) {
80
+ if (!progressJobId) return 0;
81
+ const job = await progressService.getJob(progressJobId, {
82
+ tenantId: scope.tenantId,
83
+ organizationId: scope.organizationId,
84
+ userId: scope.userId
85
+ });
86
+ return job?.processedCount ?? 0;
87
+ }
88
+ function makeHeartbeatTick(progressJobId, scope) {
89
+ const touchJobHeartbeat = progressService.touchJobHeartbeat?.bind(progressService);
90
+ if (!progressJobId || !touchJobHeartbeat) return () => {
91
+ };
92
+ let inFlight = false;
93
+ return () => {
94
+ if (inFlight) return;
95
+ inFlight = true;
96
+ touchJobHeartbeat(progressJobId, {
97
+ tenantId: scope.tenantId,
98
+ organizationId: scope.organizationId,
99
+ userId: scope.userId
100
+ }).catch((error) => {
101
+ logger.warn("Progress heartbeat failed", {
102
+ progressJobId,
103
+ error: error instanceof Error ? error.message : String(error)
104
+ });
105
+ }).finally(() => {
106
+ inFlight = false;
107
+ });
108
+ };
109
+ }
63
110
  async function refreshCoverageSnapshots(entityTypes, scope) {
64
111
  if (!entityTypes || entityTypes.length === 0) return;
65
112
  await Promise.allSettled(
@@ -312,6 +359,7 @@ function createSyncEngine(deps) {
312
359
  throw new Error(`No import adapter registered for provider ${providerKey}`);
313
360
  }
314
361
  const operationalTelemetry = adapter.operationalTelemetry === true;
362
+ const persistSharedCursor = adapter.persistsSharedCursor?.(run.entityType) ?? true;
315
363
  const credentials = await integrationCredentialsService.resolve(run.integrationId, scope);
316
364
  if (!credentials) {
317
365
  throw new Error(`Integration ${run.integrationId} is missing credentials`);
@@ -358,19 +406,23 @@ function createSyncEngine(deps) {
358
406
  });
359
407
  }
360
408
  const mapping = await resolveMapping(adapter, run.entityType, scope);
361
- let processedCount = 0;
409
+ let processedCount = await seedProcessedCount(run.progressJobId, scope);
362
410
  let totalCount = null;
363
411
  let committedBatches = activeRun.batchesCompleted ?? 0;
364
412
  try {
365
- for await (const batch of adapter.streamImport({
366
- entityType: run.entityType,
367
- cursor: run.cursor ?? void 0,
368
- batchSize,
369
- credentials,
370
- mapping,
371
- scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
372
- runId: run.id
373
- })) {
413
+ for await (const batch of withHeartbeat(
414
+ adapter.streamImport({
415
+ entityType: run.entityType,
416
+ cursor: run.cursor ?? void 0,
417
+ batchSize,
418
+ credentials,
419
+ mapping,
420
+ scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
421
+ runId: run.id
422
+ }),
423
+ makeHeartbeatTick(run.progressJobId, scope),
424
+ HEARTBEAT_TICK_MS
425
+ )) {
374
426
  if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
375
427
  await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
376
428
  return;
@@ -387,7 +439,7 @@ function createSyncEngine(deps) {
387
439
  },
388
440
  batch.cursor,
389
441
  scope,
390
- committedBatches
442
+ { expectedBatchesCompleted: committedBatches, persistSharedCursor }
391
443
  );
392
444
  committedBatches += 1;
393
445
  await updateProgress(run.progressJobId, processedCount, totalCount, scope);
@@ -455,6 +507,7 @@ function createSyncEngine(deps) {
455
507
  throw new Error(`No export adapter registered for provider ${providerKey}`);
456
508
  }
457
509
  const operationalTelemetry = adapter.operationalTelemetry === true;
510
+ const persistSharedCursor = adapter.persistsSharedCursor?.(run.entityType) ?? true;
458
511
  const credentials = await integrationCredentialsService.resolve(run.integrationId, scope);
459
512
  if (!credentials) {
460
513
  throw new Error(`Integration ${run.integrationId} is missing credentials`);
@@ -501,18 +554,22 @@ function createSyncEngine(deps) {
501
554
  });
502
555
  }
503
556
  const mapping = await resolveMapping(adapter, run.entityType, scope);
504
- let processedCount = 0;
557
+ let processedCount = await seedProcessedCount(run.progressJobId, scope);
505
558
  let committedBatches = activeRun.batchesCompleted ?? 0;
506
559
  try {
507
- for await (const batch of adapter.streamExport({
508
- entityType: run.entityType,
509
- cursor: run.cursor ?? void 0,
510
- batchSize,
511
- credentials,
512
- mapping,
513
- scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
514
- runId: run.id
515
- })) {
560
+ for await (const batch of withHeartbeat(
561
+ adapter.streamExport({
562
+ entityType: run.entityType,
563
+ cursor: run.cursor ?? void 0,
564
+ batchSize,
565
+ credentials,
566
+ mapping,
567
+ scope: { organizationId: scope.organizationId, tenantId: scope.tenantId },
568
+ runId: run.id
569
+ }),
570
+ makeHeartbeatTick(run.progressJobId, scope),
571
+ HEARTBEAT_TICK_MS
572
+ )) {
516
573
  if (run.progressJobId && await progressService.isCancellationRequested(run.progressJobId, scope.tenantId, scope.organizationId)) {
517
574
  await finalizeRun(run.id, "cancelled", scope, void 0, operationalTelemetry);
518
575
  return;
@@ -530,7 +587,7 @@ function createSyncEngine(deps) {
530
587
  },
531
588
  batch.cursor,
532
589
  scope,
533
- committedBatches
590
+ { expectedBatchesCompleted: committedBatches, persistSharedCursor }
534
591
  );
535
592
  committedBatches += 1;
536
593
  await updateProgress(run.progressJobId, processedCount, null, scope);