@outbuild-company/schedule-core 1.9.2 → 1.9.4

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
@@ -340,6 +340,10 @@ var FIELD_REGISTRY = {
340
340
  valueKind: "number",
341
341
  extract: (activity) => activity.expectedProgressBaseline
342
342
  },
343
+ expectedProgress: {
344
+ valueKind: "number",
345
+ extract: (activity) => activity.expectedProgress
346
+ },
343
347
  baselineDurationDays: {
344
348
  valueKind: "number",
345
349
  extract: (activity) => activity.baselineSnapshot?.durationDays ?? null
@@ -446,12 +450,20 @@ function matchNumber(value, target, operator) {
446
450
  if (operator === "lessOrEqual") return value <= target;
447
451
  return false;
448
452
  }
453
+ function matchNumberBetween(value, minimum, maximum) {
454
+ return typeof value === "number" && value >= minimum && value <= maximum;
455
+ }
449
456
  function matchDate(value, target, operator) {
450
457
  if (!(value instanceof Date)) return false;
451
458
  if (operator === "after") return value.getTime() > target.getTime();
452
459
  if (operator === "before") return value.getTime() < target.getTime();
453
460
  return false;
454
461
  }
462
+ function matchDateBetween(value, minimum, maximum) {
463
+ if (!(value instanceof Date)) return false;
464
+ const time = value.getTime();
465
+ return time >= minimum.getTime() && time <= maximum.getTime();
466
+ }
455
467
  function matchIdArray(value, allowed, operator) {
456
468
  const ids = Array.isArray(value) ? value : [];
457
469
  const intersects = ids.some((id) => allowed.has(String(id)));
@@ -474,6 +486,23 @@ function matchCriterion(activity, criterion, context) {
474
486
  if (descriptor === null) return false;
475
487
  const value = descriptor.extract(activity, context);
476
488
  const { operator } = criterion;
489
+ if (operator === "between") {
490
+ if (descriptor.valueKind === "number") {
491
+ return matchNumberBetween(
492
+ value,
493
+ Number(criterion.value),
494
+ Number(criterion.valueTo)
495
+ );
496
+ }
497
+ if (descriptor.valueKind === "date") {
498
+ return matchDateBetween(
499
+ value,
500
+ toTargetDate(criterion.value),
501
+ toTargetDate(criterion.valueTo)
502
+ );
503
+ }
504
+ return false;
505
+ }
477
506
  if (descriptor.valueKind === "string") {
478
507
  return matchString(value, String(criterion.value), operator);
479
508
  }
@@ -653,9 +682,10 @@ var OPERATORS_BY_KIND = {
653
682
  "greaterThan",
654
683
  "lessThan",
655
684
  "greaterOrEqual",
656
- "lessOrEqual"
685
+ "lessOrEqual",
686
+ "between"
657
687
  ]),
658
- date: /* @__PURE__ */ new Set(["after", "before"]),
688
+ date: /* @__PURE__ */ new Set(["after", "before", "between"]),
659
689
  "id-array": /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
660
690
  enum: /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
661
691
  boolean: /* @__PURE__ */ new Set(["someOf", "notSomeOf"]),
@@ -686,6 +716,9 @@ function normalizeCriterion(criterion) {
686
716
  if (!OPERATORS_BY_KIND[descriptor.valueKind].has(criterion.operator)) {
687
717
  return { ok: false, reason: "unknown_operator" };
688
718
  }
719
+ if (criterion.operator === "between") {
720
+ return normalizeBetweenCriterion(criterion, descriptor.valueKind);
721
+ }
689
722
  if (descriptor.valueKind === "number") {
690
723
  const value = normalizeNumberValue(criterion.value);
691
724
  return value === null ? { ok: false, reason: "invalid_value" } : {
@@ -726,6 +759,31 @@ function normalizeCriterion(criterion) {
726
759
  }
727
760
  return { ok: true, criterion };
728
761
  }
762
+ function normalizeBetweenCriterion(criterion, valueKind) {
763
+ if (valueKind === "number") {
764
+ const value = normalizeNumberValue(criterion.value);
765
+ const valueTo = normalizeNumberValue(criterion.valueTo);
766
+ if (value === null || valueTo === null || value > valueTo) {
767
+ return { ok: false, reason: "invalid_value" };
768
+ }
769
+ return {
770
+ ok: true,
771
+ criterion: { ...criterion, value, valueTo }
772
+ };
773
+ }
774
+ if (valueKind === "date") {
775
+ const value = normalizeDateValue(criterion.value);
776
+ const valueTo = normalizeDateValue(criterion.valueTo);
777
+ if (value === null || valueTo === null || value.getTime() > valueTo.getTime()) {
778
+ return { ok: false, reason: "invalid_value" };
779
+ }
780
+ return {
781
+ ok: true,
782
+ criterion: { ...criterion, value, valueTo }
783
+ };
784
+ }
785
+ return { ok: false, reason: "unknown_operator" };
786
+ }
729
787
  function validateValueForKind(criterion, valueKind) {
730
788
  const { value } = criterion;
731
789
  if (valueKind === "boolean") {
@@ -12543,28 +12601,6 @@ function assertValidAssignments(assignments, entityName, readCurrent, ownerByBac
12543
12601
  );
12544
12602
  }
12545
12603
  }
12546
- function assertValidReconciledProgress(entries, deps) {
12547
- const seen = /* @__PURE__ */ new Set();
12548
- for (const entry of entries) {
12549
- const id = String(entry.activityId);
12550
- if (id.length === 0 || seen.has(id)) {
12551
- throw new Error(
12552
- `persistence-acknowledge: duplicate or empty reconciled activity id "${id}"`
12553
- );
12554
- }
12555
- if (!Number.isFinite(entry.progress) || entry.progress < 0 || entry.progress > 100) {
12556
- throw new Error(
12557
- `persistence-acknowledge: invalid reconciled progress ${String(entry.progress)} for "${id}"`
12558
- );
12559
- }
12560
- if (!deps.adapter.getActivity(id)) {
12561
- throw new Error(
12562
- `persistence-acknowledge: unknown reconciled activity id "${id}"`
12563
- );
12564
- }
12565
- seen.add(id);
12566
- }
12567
- }
12568
12604
  async function applyReconciledProgress(entries, deps) {
12569
12605
  const progressToApply = entries.filter((entry) => {
12570
12606
  const current = deps.adapter.getActivity(String(entry.activityId));
@@ -12595,19 +12631,25 @@ async function applyReconciledProgress(entries, deps) {
12595
12631
  async function dispatchPersistenceAcknowledge(action, deps) {
12596
12632
  const activities = action.activities ?? [];
12597
12633
  const links = action.links ?? [];
12598
- const reconciledProgress = action.reconciledProgress ?? [];
12599
- assertValidReconciledProgress(reconciledProgress, deps);
12600
12634
  const currentActivities = deps.adapter.getAllActivities();
12601
12635
  const currentLinks = deps.adapter.getAllLinks();
12602
- const activityOwnerByBackendId = persistedOwnersOf(currentActivities);
12603
- const linkOwnerByBackendId = persistedOwnersOf(currentLinks);
12636
+ const checkpointActivities = action.checkpoint?.activities ?? currentActivities;
12637
+ const checkpointLinks = action.checkpoint?.links ?? currentLinks;
12638
+ const checkpointActivityById = new Map(
12639
+ checkpointActivities.map((activity) => [String(activity.id), activity])
12640
+ );
12641
+ const checkpointLinkById = new Map(
12642
+ checkpointLinks.map((link) => [String(link.id), link])
12643
+ );
12644
+ const activityOwnerByBackendId = persistedOwnersOf(checkpointActivities);
12645
+ const linkOwnerByBackendId = persistedOwnersOf(checkpointLinks);
12604
12646
  assertValidAssignments(
12605
12647
  activities,
12606
12648
  "activity",
12607
12649
  (id) => {
12608
- const activity = deps.adapter.getActivity(id);
12650
+ const activity = checkpointActivityById.get(id);
12609
12651
  return {
12610
- exists: activity !== null,
12652
+ exists: activity !== void 0,
12611
12653
  current: activity?.proplannerId ?? null
12612
12654
  };
12613
12655
  },
@@ -12617,17 +12659,21 @@ async function dispatchPersistenceAcknowledge(action, deps) {
12617
12659
  links,
12618
12660
  "link",
12619
12661
  (id) => {
12620
- const link = deps.adapter.getLink(id);
12621
- return { exists: link !== null, current: link?.proplannerId ?? null };
12662
+ const link = checkpointLinkById.get(id);
12663
+ return {
12664
+ exists: link !== void 0,
12665
+ current: link?.proplannerId ?? null
12666
+ };
12622
12667
  },
12623
12668
  linkOwnerByBackendId
12624
12669
  );
12625
- assertAllNewEntitiesAcknowledged(currentActivities, activities, "activity");
12626
- assertAllNewEntitiesAcknowledged(currentLinks, links, "link");
12627
- const activityChanges = await applyReconciledProgress(
12628
- reconciledProgress,
12629
- deps
12670
+ assertAllNewEntitiesAcknowledged(
12671
+ checkpointActivities,
12672
+ activities,
12673
+ "activity"
12630
12674
  );
12675
+ assertAllNewEntitiesAcknowledged(checkpointLinks, links, "link");
12676
+ const activityChanges = [];
12631
12677
  const linkChanges = [];
12632
12678
  deps.adapter.batchUpdate(() => {
12633
12679
  for (const assignment of activities) {
@@ -18441,7 +18487,9 @@ function mergeCoalesced(top, next) {
18441
18487
  return buildUndoEntry(changeSet);
18442
18488
  }
18443
18489
  function getDispatchHistoryPolicy(action) {
18444
- if (action.kind === "persistence-acknowledge") return "clear-on-success";
18490
+ if (action.kind === "persistence-acknowledge") {
18491
+ return "checkpoint-on-success";
18492
+ }
18445
18493
  if (action.kind === "sir-sync" || action.kind === "activity-lookahead-sync" || action.kind === "baseline-apply" || action.kind === "ponderator-criterion-set" || action.kind === "status-criteria-set" || action.kind === "activity-clipboard-clear" || action.kind === "activity-copy" || action.kind === "selection-toggle" || action.kind === "selection-replace" || action.kind === "visibility-set" || action.kind === "filter-set" || action.kind === "sort-set") {
18446
18494
  return "skip";
18447
18495
  }
@@ -18697,22 +18745,25 @@ var UndoRecorder = class _UndoRecorder {
18697
18745
  maxDepth;
18698
18746
  undoStack = [];
18699
18747
  redoStack = [];
18748
+ sequence = 0;
18700
18749
  _lastKey = null;
18701
18750
  _lastTime = 0;
18702
18751
  fork() {
18703
18752
  const fork = new _UndoRecorder(this.maxDepth);
18704
18753
  fork.undoStack.push(...cloneDomainValue(this.undoStack));
18705
18754
  fork.redoStack.push(...cloneDomainValue(this.redoStack));
18755
+ fork.sequence = this.sequence;
18706
18756
  fork._lastKey = this._lastKey;
18707
18757
  fork._lastTime = this._lastTime;
18708
18758
  return fork;
18709
18759
  }
18710
18760
  record(entry, coalesceKey = null, now = 0) {
18711
18761
  const top = this.undoStack[this.undoStack.length - 1];
18712
- if (coalesceKey != null && coalesceKey === this._lastKey && now - this._lastTime <= COALESCE_WINDOW_MS && top !== void 0 && canCoalesce(top, entry)) {
18713
- this.undoStack[this.undoStack.length - 1] = mergeCoalesced(top, entry);
18762
+ if (coalesceKey != null && coalesceKey === this._lastKey && now - this._lastTime <= COALESCE_WINDOW_MS && top !== void 0 && canCoalesce(top.entry, entry)) {
18763
+ top.entry = mergeCoalesced(top.entry, entry);
18764
+ top.touchedAt = this.touch();
18714
18765
  } else {
18715
- this.undoStack.push(entry);
18766
+ this.undoStack.push({ entry, touchedAt: this.touch() });
18716
18767
  if (this.undoStack.length > this.maxDepth) {
18717
18768
  this.undoStack.splice(0, this.undoStack.length - this.maxDepth);
18718
18769
  }
@@ -18725,16 +18776,16 @@ var UndoRecorder = class _UndoRecorder {
18725
18776
  this._lastKey = null;
18726
18777
  }
18727
18778
  takeUndo() {
18728
- return this.undoStack.pop() ?? null;
18779
+ return this.undoStack.pop()?.entry ?? null;
18729
18780
  }
18730
18781
  takeRedo() {
18731
- return this.redoStack.pop() ?? null;
18782
+ return this.redoStack.pop()?.entry ?? null;
18732
18783
  }
18733
18784
  pushRedo(entry) {
18734
- this.redoStack.push(entry);
18785
+ this.redoStack.push({ entry, touchedAt: this.touch() });
18735
18786
  }
18736
18787
  pushUndo(entry) {
18737
- this.undoStack.push(entry);
18788
+ this.undoStack.push({ entry, touchedAt: this.touch() });
18738
18789
  }
18739
18790
  canUndo() {
18740
18791
  return this.undoStack.length > 0;
@@ -18745,11 +18796,64 @@ var UndoRecorder = class _UndoRecorder {
18745
18796
  undoDepth() {
18746
18797
  return this.undoStack.length;
18747
18798
  }
18799
+ capturePersistenceBoundary() {
18800
+ this.resetCoalesce();
18801
+ return this.sequence;
18802
+ }
18803
+ discardThrough(boundary) {
18804
+ this.removeEntriesThrough(this.undoStack, boundary);
18805
+ this.removeEntriesThrough(this.redoStack, boundary);
18806
+ this.resetCoalesce();
18807
+ }
18808
+ applyPersistedIdentities(activities, links) {
18809
+ const activityIds = new Map(
18810
+ activities.map(({ id, proplannerId }) => [String(id), proplannerId])
18811
+ );
18812
+ const linkIds = new Map(
18813
+ links.map(({ id, proplannerId }) => [String(id), proplannerId])
18814
+ );
18815
+ for (const stored of [...this.undoStack, ...this.redoStack]) {
18816
+ stored.entry = this.withPersistedIdentities(
18817
+ stored.entry,
18818
+ activityIds,
18819
+ linkIds
18820
+ );
18821
+ }
18822
+ }
18748
18823
  clear() {
18749
18824
  this.undoStack.length = 0;
18750
18825
  this.redoStack.length = 0;
18751
18826
  this.resetCoalesce();
18752
18827
  }
18828
+ touch() {
18829
+ this.sequence += 1;
18830
+ return this.sequence;
18831
+ }
18832
+ removeEntriesThrough(stack, boundary) {
18833
+ const retained = stack.filter(({ touchedAt }) => touchedAt > boundary);
18834
+ stack.splice(0, stack.length, ...retained);
18835
+ }
18836
+ withPersistedIdentities(entry, activityIds, linkIds) {
18837
+ const patchActivities = (snapshots) => snapshots === void 0 ? void 0 : new Map(
18838
+ Array.from(snapshots, ([id, activity]) => [
18839
+ id,
18840
+ activityIds.has(id) ? { ...activity, proplannerId: activityIds.get(id) } : activity
18841
+ ])
18842
+ );
18843
+ const patchLinks = (snapshots) => snapshots === void 0 ? void 0 : new Map(
18844
+ Array.from(snapshots, ([id, link]) => [
18845
+ id,
18846
+ linkIds.has(id) ? { ...link, proplannerId: linkIds.get(id) } : link
18847
+ ])
18848
+ );
18849
+ return {
18850
+ ...entry,
18851
+ beforeSnap: patchActivities(entry.beforeSnap),
18852
+ afterSnap: patchActivities(entry.afterSnap),
18853
+ beforeLinks: patchLinks(entry.beforeLinks),
18854
+ afterLinks: patchLinks(entry.afterLinks)
18855
+ };
18856
+ }
18753
18857
  };
18754
18858
 
18755
18859
  // src/dispatch/shared/resequenced-parents.ts
@@ -18907,10 +19011,12 @@ function snapshotFieldValue(field, value) {
18907
19011
  return value;
18908
19012
  }
18909
19013
  function activityChangedSince(current, saved) {
18910
- for (const field of PERSISTABLE_ACTIVITY_FIELDS) {
18911
- if (!fieldEquals(field, current[field], saved[field])) return true;
18912
- }
18913
- return false;
19014
+ return changedPersistableActivityFields(current, saved).length > 0;
19015
+ }
19016
+ function changedPersistableActivityFields(current, saved) {
19017
+ return PERSISTABLE_ACTIVITY_FIELDS.filter(
19018
+ (field) => !fieldEquals(field, current[field], saved[field])
19019
+ );
18914
19020
  }
18915
19021
  function fieldEquals(field, currentValue, savedValue) {
18916
19022
  if (DATE_FIELDS2.has(field)) return dateEquals(currentValue, savedValue);
@@ -19019,11 +19125,60 @@ var SaveTracker = class _SaveTracker {
19019
19125
  }
19020
19126
  this.initialized = true;
19021
19127
  }
19022
- acknowledge(activities, links) {
19128
+ acknowledge(activities, links, activityIdentities = [], linkIdentities = [], currentActivities, currentLinks) {
19023
19129
  if (!this.trackingEnabled) return;
19024
- assertValidPersistedIdentities(activities, links);
19025
- this.snapshotActivities(activities);
19026
- this.snapshotLinks(links);
19130
+ const activityIds = new Map(
19131
+ activityIdentities.map(({ id, proplannerId }) => [
19132
+ String(id),
19133
+ proplannerId
19134
+ ])
19135
+ );
19136
+ const linkIds = new Map(
19137
+ linkIdentities.map(({ id, proplannerId }) => [String(id), proplannerId])
19138
+ );
19139
+ const persistedActivities = activities.map((activity) => ({
19140
+ ...activity,
19141
+ proplannerId: activityIds.get(String(activity.id)) ?? activity.proplannerId
19142
+ }));
19143
+ const persistedLinks = links.map((link) => {
19144
+ const proplannerId = linkIds.get(String(link.id));
19145
+ return proplannerId === void 0 ? { ...link } : { ...link, proplannerId };
19146
+ });
19147
+ assertValidPersistedIdentities(persistedActivities, persistedLinks);
19148
+ this.snapshotActivities(persistedActivities);
19149
+ this.snapshotLinks(persistedLinks);
19150
+ if (!currentActivities || !currentLinks) return;
19151
+ const currentActivityById = new Map(
19152
+ currentActivities.map((activity) => [String(activity.id), activity])
19153
+ );
19154
+ const currentLinkById = new Map(
19155
+ currentLinks.map((link) => [String(link.id), link])
19156
+ );
19157
+ for (const activity of persistedActivities) {
19158
+ const activityId = String(activity.id);
19159
+ if (this.activityIsDirty(
19160
+ activityId,
19161
+ currentActivityById.get(activityId) ?? null
19162
+ )) {
19163
+ this.dirtyActivityIds.add(activityId);
19164
+ }
19165
+ }
19166
+ for (const activity of currentActivities) {
19167
+ const activityId = String(activity.id);
19168
+ if (this.activityIsDirty(activityId, activity)) {
19169
+ this.dirtyActivityIds.add(activityId);
19170
+ }
19171
+ }
19172
+ for (const link of persistedLinks) {
19173
+ const linkId = String(link.id);
19174
+ if (this.linkIsDirty(linkId, currentLinkById.get(linkId) ?? null)) {
19175
+ this.dirtyLinkIds.add(linkId);
19176
+ }
19177
+ }
19178
+ for (const link of currentLinks) {
19179
+ const linkId = String(link.id);
19180
+ if (this.linkIsDirty(linkId, link)) this.dirtyLinkIds.add(linkId);
19181
+ }
19027
19182
  }
19028
19183
  snapshotActivities(activities) {
19029
19184
  if (!this.trackingEnabled) return;
@@ -19207,6 +19362,21 @@ var SaveTracker = class _SaveTracker {
19207
19362
  }
19208
19363
  return modified;
19209
19364
  }
19365
+ modifiedActivityFields(getActivity) {
19366
+ const result = /* @__PURE__ */ new Map();
19367
+ if (!this.trackingEnabled || !this.initialized) return result;
19368
+ for (const activityId of this.dirtyActivityIds) {
19369
+ const current = getActivity(activityId);
19370
+ const saved = this.activitiesAtLastSave.get(activityId);
19371
+ if (!current || !saved) continue;
19372
+ const fields = changedPersistableActivityFields(
19373
+ current,
19374
+ saved.persistable
19375
+ );
19376
+ if (fields.length > 0) result.set(activityId, fields);
19377
+ }
19378
+ return result;
19379
+ }
19210
19380
  modifiedLinks(getLink) {
19211
19381
  if (!this.trackingEnabled || !this.initialized) return [];
19212
19382
  const modified = [];
@@ -21299,6 +21469,14 @@ var ScheduleCore = class _ScheduleCore {
21299
21469
  (activityId) => this.coreRuntime.state.getActivity(activityId)
21300
21470
  ).map(cloneDomainValue);
21301
21471
  }
21472
+ getModifiedActivityFields() {
21473
+ this.assertReady();
21474
+ return cloneDomainValue(
21475
+ this._saveTracker.modifiedActivityFields(
21476
+ (activityId) => this.coreRuntime.state.getActivity(activityId)
21477
+ )
21478
+ );
21479
+ }
21302
21480
  getNewActivities() {
21303
21481
  this.assertReady();
21304
21482
  return this._saveTracker.newActivities(
@@ -21312,6 +21490,10 @@ var ScheduleCore = class _ScheduleCore {
21312
21490
  getScheduleRevision() {
21313
21491
  return this._scheduleRevision;
21314
21492
  }
21493
+ capturePersistenceHistoryBoundary() {
21494
+ this.assertReady();
21495
+ return this._undo.capturePersistenceBoundary();
21496
+ }
21315
21497
  getCanonicalDigest() {
21316
21498
  this.assertReady();
21317
21499
  const byId = (left, right) => String(left.id).localeCompare(String(right.id));
@@ -21482,12 +21664,29 @@ var ScheduleCore = class _ScheduleCore {
21482
21664
  );
21483
21665
  }
21484
21666
  if (result.ok) {
21485
- if (historyPolicy === "clear-on-success") {
21667
+ if (historyPolicy === "checkpoint-on-success") {
21668
+ if (action.kind !== "persistence-acknowledge") {
21669
+ throw new Error(
21670
+ "checkpoint-on-success requires persistence acknowledge"
21671
+ );
21672
+ }
21673
+ const currentActivities = this.coreRuntime.state.getAllActivities();
21674
+ const currentLinks = this.coreRuntime.state.getAllLinks();
21675
+ const checkpoint = action.checkpoint;
21676
+ const boundary = checkpoint?.historyBoundary ?? this._undo.capturePersistenceBoundary();
21486
21677
  this._saveTracker.acknowledge(
21487
- this.coreRuntime.state.getAllActivities(),
21488
- this.coreRuntime.state.getAllLinks()
21678
+ checkpoint?.activities ?? currentActivities,
21679
+ checkpoint?.links ?? currentLinks,
21680
+ action.activities,
21681
+ action.links,
21682
+ currentActivities,
21683
+ currentLinks
21684
+ );
21685
+ this._undo.discardThrough(boundary);
21686
+ this._undo.applyPersistedIdentities(
21687
+ action.activities ?? [],
21688
+ action.links ?? []
21489
21689
  );
21490
- this._undo.clear();
21491
21690
  } else if (action.kind === "activity-lookahead-sync" && action.acknowledgePersistedLookahead === true) {
21492
21691
  this._saveTracker.acknowledgePersistedLookahead(
21493
21692
  result.changes.activities,