@outbuild-company/schedule-core 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -345,6 +345,7 @@ var DISPATCH_ACTION_KIND = {
345
345
  ACTIVITY_COPY: "activity-copy",
346
346
  CALENDAR_RECONCILE: "calendar-reconcile",
347
347
  ACTIVITY_PASTE_COPIED: "activity-paste-copied",
348
+ ACTIVITY_DUPLICATE: "activity-duplicate",
348
349
  PERSISTENCE_ACKNOWLEDGE: "persistence-acknowledge",
349
350
  BASELINE_APPLY: "baseline-apply",
350
351
  PONDERATOR_CRITERION_SET: "ponderator-criterion-set",
@@ -593,6 +594,11 @@ function createActivitiesApi(engine) {
593
594
  kind: DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED,
594
595
  destination: { afterSiblingId }
595
596
  }),
597
+ duplicate: (activityIds, context = {}) => engine.dispatch({
598
+ ...context,
599
+ kind: DISPATCH_ACTION_KIND.ACTIVITY_DUPLICATE,
600
+ activityIds
601
+ }),
596
602
  clearClipboard: (context = {}) => engine.dispatch({
597
603
  ...context,
598
604
  kind: DISPATCH_ACTION_KIND.ACTIVITY_CLIPBOARD_CLEAR
@@ -919,6 +925,7 @@ var REJECTION_REASON = {
919
925
  NO_ACTIVITY_SELECTION: "no_activity_selection",
920
926
  CLIPBOARD_EMPTY: "clipboard_empty",
921
927
  ACTIVITY_PASTE_INVARIANT_FAILED: "activity_paste_invariant_failed",
928
+ SOLE_SCHEDULE_ROOT: "sole_schedule_root",
922
929
  ANCHOR_SIBLING_CONFLICT: "anchor_sibling_conflict",
923
930
  ANCHOR_SIBLING_NOT_FOUND: "anchor_sibling_not_found",
924
931
  ANCHOR_SIBLING_WRONG_PARENT: "anchor_sibling_wrong_parent",
@@ -11935,12 +11942,34 @@ function draftRemainingDuration(activity) {
11935
11942
  }
11936
11943
 
11937
11944
  // src/p6/inactive-drafts.ts
11938
- function refreshUnimportedDataDateDrafts(state, input) {
11945
+ function trackInactiveDataDateDrafts(state, input) {
11946
+ const p6Input = input ?? void 0;
11947
+ const beforeInitialPasses = snapshotInactiveDataDateDrafts(state, p6Input);
11948
+ return () => refreshInactiveDataDateDrafts(state, p6Input, beforeInitialPasses);
11949
+ }
11950
+ function snapshotInactiveDataDateDrafts(state, input) {
11951
+ if (input === void 0 || hasActiveDataDate(input.settings)) {
11952
+ return /* @__PURE__ */ new Map();
11953
+ }
11954
+ return new Map(
11955
+ state.getAllActivities().filter(
11956
+ (activity) => activity.p6 !== void 0 && Object.hasOwn(input.activities, activity.id)
11957
+ ).map((activity) => [activity.id, structuredCloneActivity(activity)])
11958
+ );
11959
+ }
11960
+ function refreshInactiveDataDateDrafts(state, input, beforeInitialPasses) {
11939
11961
  if (input === void 0 || hasActiveDataDate(input.settings)) return;
11940
11962
  for (const activity of state.getAllActivities()) {
11941
- if (Object.hasOwn(input.activities, activity.id)) continue;
11942
- const p6 = createOutbuildP6ActivityDraft(activity);
11943
- if (p6 !== void 0) state.setActivityField(activity.id, "p6", p6);
11963
+ if (!Object.hasOwn(input.activities, activity.id)) {
11964
+ const p6 = createOutbuildP6ActivityDraft(activity);
11965
+ if (p6 !== void 0) state.setActivityField(activity.id, "p6", p6);
11966
+ continue;
11967
+ }
11968
+ const before = beforeInitialPasses.get(activity.id);
11969
+ if (before?.p6 === void 0 || activity.p6 === void 0) continue;
11970
+ const patch = ordinaryInputChanges(before, activity);
11971
+ if (Object.keys(patch).length === 0) continue;
11972
+ state.setActivityField(activity.id, "p6", { ...activity.p6, ...patch });
11944
11973
  }
11945
11974
  }
11946
11975
  function synchronizeInactiveDataDateDrafts(state) {
@@ -18460,6 +18489,293 @@ async function dispatchActivityPasteCopied(action, options, deps) {
18460
18489
  };
18461
18490
  }
18462
18491
 
18492
+ // src/activity-copy-paste/duplicate.ts
18493
+ var CLONE_YIELD_EVERY_N = 200;
18494
+ var CREATE_OPTIONS = {
18495
+ skipCorrelativeRecompute: true,
18496
+ skipBeforeSnap: true,
18497
+ skipCacheInvalidation: true,
18498
+ skipRollupCascades: true
18499
+ };
18500
+ async function dispatchActivityDuplicate(action, options, deps) {
18501
+ const resolved = resolveDuplicateRuns(action.activityIds, deps.adapter);
18502
+ if (!resolved.ok) return resolved;
18503
+ const frozenParent = firstFrozenRunParent(resolved.runs, deps.adapter);
18504
+ if (frozenParent) {
18505
+ return { ok: false, reason: REJECTION_REASON.PARENT_FROZEN_BY_SIR };
18506
+ }
18507
+ deps.scheduler.invalidateAllCaches();
18508
+ const beforeSnapshot = await snapshotActivities(
18509
+ deps.adapter,
18510
+ /* @__PURE__ */ new Set()
18511
+ );
18512
+ const state = {
18513
+ originalToNew: /* @__PURE__ */ new Map(),
18514
+ createdIds: [],
18515
+ createdParentIds: []
18516
+ };
18517
+ for (const run of resolved.runs) {
18518
+ const lastOriginal = run.originals.at(-1);
18519
+ if (!lastOriginal) continue;
18520
+ let anchorId = lastOriginal.id;
18521
+ for (const original of run.originals) {
18522
+ anchorId = await cloneSubtree(
18523
+ original,
18524
+ run.parentId,
18525
+ anchorId,
18526
+ action,
18527
+ deps,
18528
+ state
18529
+ );
18530
+ }
18531
+ }
18532
+ recomputeCreatedParentRollups(state, deps);
18533
+ const correlativeShifts = recomputeCorrelativeIds(deps.adapter);
18534
+ const createdIdSet = new Set(state.createdIds.map(String));
18535
+ const touched = /* @__PURE__ */ new Set();
18536
+ seedJournalBeforeImages(
18537
+ deps.adapter,
18538
+ correlativeShifts,
18539
+ createdIdSet,
18540
+ beforeSnapshot
18541
+ );
18542
+ foldCorrelativeShifts(
18543
+ deps.adapter,
18544
+ correlativeShifts,
18545
+ createdIdSet,
18546
+ beforeSnapshot,
18547
+ touched
18548
+ );
18549
+ for (const createdId of state.createdIds) touched.add(String(createdId));
18550
+ const applied = replicateInternalLinks(state, deps);
18551
+ const { scheduledIds } = await runPostMutation(
18552
+ {
18553
+ adapter: deps.adapter,
18554
+ scheduler: deps.scheduler,
18555
+ sector: deps.sector,
18556
+ defaultBaseCalendarId: deps.defaultBaseCalendarId
18557
+ },
18558
+ {
18559
+ action,
18560
+ autoscheduleFrom: state.createdIds.length > 0 ? AUTOSCHEDULE_SCOPE.ROOTS : null,
18561
+ recomputeParentsFrom: state.createdIds,
18562
+ preventSummaryTargetPullback: true,
18563
+ now: deps.now,
18564
+ options
18565
+ }
18566
+ );
18567
+ return {
18568
+ ok: true,
18569
+ changes: await assembleChangeSet(deps.adapter, {
18570
+ source: action,
18571
+ beforeSnap: beforeSnapshot,
18572
+ touchedIds: touched,
18573
+ scheduledIds,
18574
+ links: buildLinkChangesForBatch(
18575
+ applied,
18576
+ /* @__PURE__ */ new Map(),
18577
+ deps.adapter
18578
+ ),
18579
+ hoursPerDay: deps.sector.hoursPerDay
18580
+ })
18581
+ };
18582
+ }
18583
+ function resolveDuplicateRuns(activityIds, adapter) {
18584
+ if (activityIds.length === 0) {
18585
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_IDS_EMPTY };
18586
+ }
18587
+ const requested = /* @__PURE__ */ new Set();
18588
+ const originals = [];
18589
+ for (const rawId of activityIds) {
18590
+ const activityId = String(rawId);
18591
+ if (requested.has(activityId)) continue;
18592
+ const activity = adapter.getActivity(activityId);
18593
+ if (!activity) {
18594
+ return { ok: false, reason: REJECTION_REASON.ACTIVITY_NOT_FOUND };
18595
+ }
18596
+ requested.add(activityId);
18597
+ originals.push(activity);
18598
+ }
18599
+ const roots = originals.filter(
18600
+ (activity) => !hasAncestorInSet(activity.id, requested, adapter)
18601
+ );
18602
+ const duplicatesSoleScheduleRoot = adapter.getRootIds().length === 1 && roots.some((activity) => activity.parentId == null);
18603
+ if (duplicatesSoleScheduleRoot) {
18604
+ return { ok: false, reason: REJECTION_REASON.SOLE_SCHEDULE_ROOT };
18605
+ }
18606
+ return { ok: true, runs: groupIntoContiguousRuns(roots, adapter) };
18607
+ }
18608
+ function firstFrozenRunParent(runs, adapter) {
18609
+ const checked = /* @__PURE__ */ new Set();
18610
+ for (const run of runs) {
18611
+ if (run.parentId === ROOT_PARENT_ID || checked.has(String(run.parentId))) {
18612
+ continue;
18613
+ }
18614
+ checked.add(String(run.parentId));
18615
+ const parent = adapter.getActivity(run.parentId);
18616
+ if (parent && isFrozen(parent)) return parent;
18617
+ }
18618
+ return null;
18619
+ }
18620
+ function groupIntoContiguousRuns(roots, adapter) {
18621
+ const byParent = /* @__PURE__ */ new Map();
18622
+ for (const activity of roots) {
18623
+ const parentKey = normalizeParentKey(activity.parentId);
18624
+ const group = byParent.get(parentKey) ?? [];
18625
+ group.push(activity);
18626
+ byParent.set(parentKey, group);
18627
+ }
18628
+ const runs = [];
18629
+ for (const [parentKey, group] of byParent) {
18630
+ const parentId = parentKey === ROOT_PARENT_ID ? ROOT_PARENT_ID : parentKey;
18631
+ const positions = new Map(
18632
+ childrenInCorrelativeOrder(adapter, parentKey).map(
18633
+ (sibling, index) => [String(sibling.id), index]
18634
+ )
18635
+ );
18636
+ const positionOf = (activity) => positions.get(String(activity.id)) ?? -1;
18637
+ const ordered = [...group].sort(
18638
+ (left, right) => positionOf(left) - positionOf(right)
18639
+ );
18640
+ for (const originals of splitContiguous(ordered, positionOf)) {
18641
+ runs.push({ parentId, originals });
18642
+ }
18643
+ }
18644
+ return runs;
18645
+ }
18646
+ function splitContiguous(ordered, positionOf) {
18647
+ const runs = [];
18648
+ let previousPosition = null;
18649
+ for (const activity of ordered) {
18650
+ const position = positionOf(activity);
18651
+ const lastRun = runs.at(-1);
18652
+ if (lastRun && previousPosition !== null && position === previousPosition + 1) {
18653
+ lastRun.push(activity);
18654
+ } else {
18655
+ runs.push([activity]);
18656
+ }
18657
+ previousPosition = position;
18658
+ }
18659
+ return runs;
18660
+ }
18661
+ function seedJournalBeforeImages(adapter, shifts, excludeIds, beforeSnap) {
18662
+ const journal = adapter.peekWriteCapture();
18663
+ if (!journal) return;
18664
+ for (const shift of shifts) {
18665
+ if (excludeIds.has(shift.activityId) || beforeSnap.has(shift.activityId)) {
18666
+ continue;
18667
+ }
18668
+ const beforeImage = journal.before.get(shift.activityId);
18669
+ if (beforeImage) beforeSnap.set(shift.activityId, beforeImage);
18670
+ }
18671
+ }
18672
+ function childrenInCorrelativeOrder(adapter, parentKey) {
18673
+ return collectSiblings(adapter, parentKey).sort((left, right) => {
18674
+ const diff = readCorrelativeId(left) - readCorrelativeId(right);
18675
+ return diff || String(left.id).localeCompare(String(right.id));
18676
+ });
18677
+ }
18678
+ async function cloneSubtree(original, parentId, anchorId, action, deps, state) {
18679
+ const root = await createActivityCore(
18680
+ {
18681
+ kind: DISPATCH_ACTION_KIND.ACTIVITY_CREATE,
18682
+ parentId,
18683
+ afterSiblingId: anchorId,
18684
+ overrides: copyOverrides(original),
18685
+ ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
18686
+ },
18687
+ deps,
18688
+ { ...CREATE_OPTIONS, customIdReferenceId: original.id }
18689
+ );
18690
+ if (!root.ok) throw invariantViolation("create", root.reason);
18691
+ registerCreated(state, original, root.newId, parentId);
18692
+ await cloneDescendantsIteratively(original, root.newId, action, deps, state);
18693
+ return root.newId;
18694
+ }
18695
+ async function cloneDescendantsIteratively(rootOriginal, rootNewParentId, action, deps, state) {
18696
+ const pending = new Queue();
18697
+ pending.enqueue({ original: rootOriginal, newParentId: rootNewParentId });
18698
+ let processed = 0;
18699
+ while (!pending.isEmpty()) {
18700
+ const { original, newParentId } = pending.dequeue();
18701
+ for (const child of childrenInCorrelativeOrder(
18702
+ deps.adapter,
18703
+ String(original.id)
18704
+ )) {
18705
+ const created = await createActivityCore(
18706
+ {
18707
+ kind: DISPATCH_ACTION_KIND.ACTIVITY_CREATE,
18708
+ parentId: newParentId,
18709
+ overrides: copyOverrides(child),
18710
+ ...action.eventSource === void 0 ? {} : { eventSource: action.eventSource }
18711
+ },
18712
+ deps,
18713
+ CREATE_OPTIONS
18714
+ );
18715
+ if (!created.ok) throw invariantViolation("create", created.reason);
18716
+ registerCreated(state, child, created.newId, newParentId);
18717
+ pending.enqueue({ original: child, newParentId: created.newId });
18718
+ processed += 1;
18719
+ if (processed % CLONE_YIELD_EVERY_N === 0) await yieldToBrowser();
18720
+ }
18721
+ }
18722
+ }
18723
+ function invariantViolation(step, reason) {
18724
+ return new Error(
18725
+ `[ScheduleCore] activity-duplicate: unexpected ${step} rejection after preflight passed (${reason})`
18726
+ );
18727
+ }
18728
+ function registerCreated(state, original, newId, parentId) {
18729
+ state.originalToNew.set(String(original.id), newId);
18730
+ state.createdIds.push(newId);
18731
+ state.createdParentIds.push(parentId);
18732
+ }
18733
+ function recomputeCreatedParentRollups(state, deps) {
18734
+ const recomputed = /* @__PURE__ */ new Set();
18735
+ for (const parentId of [...state.createdParentIds].reverse()) {
18736
+ if (parentId === ROOT_PARENT_ID || recomputed.has(String(parentId))) {
18737
+ continue;
18738
+ }
18739
+ recomputed.add(String(parentId));
18740
+ recomputeRollupCascadesForParent(parentId, deps.adapter);
18741
+ }
18742
+ }
18743
+ function replicateInternalLinks(state, deps) {
18744
+ const applied = [];
18745
+ for (const originalId of state.originalToNew.keys()) {
18746
+ for (const linkId of deps.adapter.getOutgoingLinkIds(originalId)) {
18747
+ const link = deps.adapter.getLink(linkId);
18748
+ if (!link) continue;
18749
+ const source = state.originalToNew.get(String(link.source));
18750
+ const target = state.originalToNew.get(String(link.target));
18751
+ if (source === void 0 || target === void 0) continue;
18752
+ const operation = {
18753
+ kind: LINK_OPERATION_KIND.CREATE,
18754
+ source,
18755
+ target,
18756
+ type: link.type,
18757
+ lag: link.lag
18758
+ };
18759
+ const result = applyLinkOperation(operation, {
18760
+ port: deps.adapter,
18761
+ newLinkId: deps.linkIdGen.next
18762
+ });
18763
+ if (!result.applied) {
18764
+ throw invariantViolation(
18765
+ "link",
18766
+ result.rejected ?? REJECTION_REASON.INVALID
18767
+ );
18768
+ }
18769
+ applied.push({
18770
+ op: operation,
18771
+ finalLinkId: result.linkId,
18772
+ rejected: null
18773
+ });
18774
+ }
18775
+ }
18776
+ return applied;
18777
+ }
18778
+
18463
18779
  // src/dispatch/dispatch.ts
18464
18780
  async function performDispatch(action, options, deps) {
18465
18781
  if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_COPY) {
@@ -18477,6 +18793,9 @@ async function performDispatch(action, options, deps) {
18477
18793
  if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_PASTE_COPIED) {
18478
18794
  return dispatchActivityPasteCopied(action, options, deps);
18479
18795
  }
18796
+ if (action.kind === DISPATCH_ACTION_KIND.ACTIVITY_DUPLICATE) {
18797
+ return dispatchActivityDuplicate(action, options, deps);
18798
+ }
18480
18799
  if (action.kind === ACTIVITY_BATCH_ACTION_KIND) {
18481
18800
  return dispatchActivityBatch(action, options, deps);
18482
18801
  }
@@ -29781,13 +30100,14 @@ function initializeCore(input) {
29781
30100
  loadDiagnostics.record
29782
30101
  );
29783
30102
  }
30103
+ const settleInactiveDrafts = trackInactiveDataDateDrafts(state, input.p6);
29784
30104
  const initPromise = runInitialPasses({
29785
30105
  state,
29786
30106
  sector,
29787
30107
  scheduler,
29788
30108
  skipAutoSchedule: input.skipInitialAutoSchedule === true,
29789
30109
  now: loadNow ?? void 0
29790
- }).then(() => refreshUnimportedDataDateDrafts(state, input.p6 ?? void 0));
30110
+ }).then(settleInactiveDrafts);
29791
30111
  return {
29792
30112
  dataDateCapability: dataDate?.capability ?? null,
29793
30113
  sector,