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