@knpkv/herdr-work 0.8.1 → 0.9.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/README.md +1 -1
- package/dist/model.d.ts +26 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +19 -0
- package/dist/model.js.map +1 -1
- package/dist/service.d.ts +30 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +2 -0
- package/dist/service.js.map +1 -1
- package/dist/store.d.ts +37 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +69 -12
- package/dist/store.js.map +1 -1
- package/package.json +2 -2
package/dist/store.js
CHANGED
|
@@ -2812,7 +2812,15 @@ export class WorkStore {
|
|
|
2812
2812
|
* succeeds), so a writer that planned from the head cannot bury a newer
|
|
2813
2813
|
* owner checkpoint.
|
|
2814
2814
|
*/
|
|
2815
|
-
|
|
2815
|
+
/**
|
|
2816
|
+
* Appends one checkpoint. With `mode` `check`, every check runs in the same
|
|
2817
|
+
* transaction and it is rolled back instead of written: the checkpoint is
|
|
2818
|
+
* returned when it would have been appended.
|
|
2819
|
+
*/
|
|
2820
|
+
appendAt = Effect.fn("WorkStore.append")(function* (event, guard, mode = "write",
|
|
2821
|
+
// In a check, the checkpoints earlier steps of the same plan would have
|
|
2822
|
+
// written: limits are checked as if they were already stored.
|
|
2823
|
+
planned = []) {
|
|
2816
2824
|
const decoded = yield* Schema.decodeUnknownEffect(WorkGoalCheckpoint)(event).pipe(Effect.mapError(storeError("append.decode")));
|
|
2817
2825
|
// The reconciler's event and activity ids are its provenance; no other
|
|
2818
2826
|
// writer may use them.
|
|
@@ -2827,11 +2835,12 @@ export class WorkStore {
|
|
|
2827
2835
|
const decision = yield* Effect.try({
|
|
2828
2836
|
try: () => {
|
|
2829
2837
|
let transaction = false;
|
|
2838
|
+
const abort = () => this.#database.exec("ROLLBACK");
|
|
2830
2839
|
try {
|
|
2831
2840
|
this.#database.exec("BEGIN IMMEDIATE");
|
|
2832
2841
|
transaction = true;
|
|
2833
2842
|
const reject = (error) => {
|
|
2834
|
-
|
|
2843
|
+
abort();
|
|
2835
2844
|
transaction = false;
|
|
2836
2845
|
return { _tag: "rejected", error };
|
|
2837
2846
|
};
|
|
@@ -2840,7 +2849,7 @@ export class WorkStore {
|
|
|
2840
2849
|
WHERE event_id = ? OR (goal_id = ? AND occurred_at = ?)`)
|
|
2841
2850
|
.all(decoded.eventId, decoded.goal.id, decoded.occurredAt)).map(({ record }) => Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(record)));
|
|
2842
2851
|
if (collisions.length > 0 && collisions.every((existing) => Equal.equals(existing, decoded))) {
|
|
2843
|
-
|
|
2852
|
+
abort();
|
|
2844
2853
|
transaction = false;
|
|
2845
2854
|
return { _tag: "replayed", event: decoded };
|
|
2846
2855
|
}
|
|
@@ -2868,7 +2877,7 @@ export class WorkStore {
|
|
|
2868
2877
|
}));
|
|
2869
2878
|
}
|
|
2870
2879
|
const count = Schema.decodeUnknownSync(CountRow)(this.#database.prepare("SELECT COUNT(*) AS count FROM work_goal_events").get()).count;
|
|
2871
|
-
if (count + 1 > workHistoryMaxEvents - guard.reserve) {
|
|
2880
|
+
if (count + planned.length + 1 > workHistoryMaxEvents - guard.reserve) {
|
|
2872
2881
|
return reject(new WorkProjectionError({
|
|
2873
2882
|
cause: count,
|
|
2874
2883
|
detail: `the reconciler leaves ${guard.reserve} of ${workHistoryMaxEvents} checkpoints free`,
|
|
@@ -2884,14 +2893,15 @@ export class WorkStore {
|
|
|
2884
2893
|
}));
|
|
2885
2894
|
}
|
|
2886
2895
|
const eventCount = Schema.decodeUnknownSync(CountRow)(this.#database.prepare("SELECT COUNT(*) AS count FROM work_goal_events").get()).count;
|
|
2887
|
-
if (eventCount >= workHistoryMaxEvents) {
|
|
2896
|
+
if (eventCount + planned.length >= workHistoryMaxEvents) {
|
|
2888
2897
|
return reject(new WorkProjectionError({
|
|
2889
2898
|
cause: decoded,
|
|
2890
2899
|
detail: `work history cannot exceed ${workHistoryMaxEvents} checkpoints`,
|
|
2891
2900
|
reason: "capacity_exceeded"
|
|
2892
2901
|
}));
|
|
2893
2902
|
}
|
|
2894
|
-
const history = Schema.decodeUnknownSync(StoredEventRows)(this.#database.prepare("SELECT record FROM work_goal_events").all()).map(({ record }) => Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(record)))
|
|
2903
|
+
const history = Schema.decodeUnknownSync(StoredEventRows)(this.#database.prepare("SELECT record FROM work_goal_events").all()).map(({ record }) => Schema.decodeUnknownSync(WorkGoalCheckpoint)(JSON.parse(record)))
|
|
2904
|
+
.concat(planned);
|
|
2895
2905
|
const familyError = validateGoalFamilyHistory([...history, decoded]);
|
|
2896
2906
|
if (familyError !== undefined)
|
|
2897
2907
|
return reject(familyError);
|
|
@@ -2930,6 +2940,11 @@ export class WorkStore {
|
|
|
2930
2940
|
}));
|
|
2931
2941
|
}
|
|
2932
2942
|
}
|
|
2943
|
+
if (mode === "check") {
|
|
2944
|
+
abort();
|
|
2945
|
+
transaction = false;
|
|
2946
|
+
return { _tag: "checked" };
|
|
2947
|
+
}
|
|
2933
2948
|
const result = this.#database.prepare("INSERT INTO work_goal_events (event_id, goal_id, occurred_at, record) VALUES (?, ?, ?, ?)").run(decoded.eventId, decoded.goal.id, decoded.occurredAt, JSON.stringify(decoded));
|
|
2934
2949
|
// The reconciler's own record of what it wrote: its authorship and its
|
|
2935
2950
|
// once-per-goal rule come from this table, never from an id's spelling.
|
|
@@ -2942,7 +2957,7 @@ export class WorkStore {
|
|
|
2942
2957
|
}
|
|
2943
2958
|
catch (error) {
|
|
2944
2959
|
if (transaction)
|
|
2945
|
-
|
|
2960
|
+
abort();
|
|
2946
2961
|
throw error;
|
|
2947
2962
|
}
|
|
2948
2963
|
},
|
|
@@ -2952,6 +2967,8 @@ export class WorkStore {
|
|
|
2952
2967
|
return yield* decision.error;
|
|
2953
2968
|
if (decision._tag === "replayed")
|
|
2954
2969
|
return decision.event;
|
|
2970
|
+
if (decision._tag === "checked")
|
|
2971
|
+
return decoded;
|
|
2955
2972
|
if (decision.changes !== 1 && decision.changes !== 1n) {
|
|
2956
2973
|
return yield* storeError("append.insert.count")(decision.changes);
|
|
2957
2974
|
}
|
|
@@ -4041,7 +4058,11 @@ export class WorkStore {
|
|
|
4041
4058
|
reconcilerEvents
|
|
4042
4059
|
};
|
|
4043
4060
|
});
|
|
4044
|
-
|
|
4061
|
+
/**
|
|
4062
|
+
* The steps `reconcile` takes, shared by `reconcile` (`write`) and
|
|
4063
|
+
* `planReconcile` (`check`, which runs every check and writes nothing).
|
|
4064
|
+
*/
|
|
4065
|
+
reconcileSteps = Effect.fn("WorkStore.reconcileSteps")(function* (request, mode) {
|
|
4045
4066
|
const options = yield* Schema.decodeUnknownEffect(WorkReconcileOptions)(request).pipe(Effect.mapError(storeError("reconcile.options")));
|
|
4046
4067
|
const source = yield* this.snapshotInput();
|
|
4047
4068
|
if (workHistoryMaxEvents - source.events.length < workReconcilerHeadroom) {
|
|
@@ -4063,7 +4084,13 @@ export class WorkStore {
|
|
|
4063
4084
|
const stamped = new Map(source.reconcilerEvents.map(({ eventId, goalId }) => [goalId, eventId]));
|
|
4064
4085
|
const cryptoService = this.#cryptoService;
|
|
4065
4086
|
const now = yield* Clock.currentTimeMillis;
|
|
4066
|
-
|
|
4087
|
+
// A plan holds no transaction between steps: each check rolls back at once,
|
|
4088
|
+
// and later steps are checked against the checkpoints earlier ones planned.
|
|
4089
|
+
const planned = [];
|
|
4090
|
+
const appendAt = (checkpoint, guard) => this.appendAt(checkpoint, guard, mode, planned).pipe(Effect.tap(() => Effect.sync(() => {
|
|
4091
|
+
if (mode === "check")
|
|
4092
|
+
planned.push(checkpoint);
|
|
4093
|
+
})));
|
|
4067
4094
|
return yield* Effect.forEach(terminalCandidates(source.events, source.facts, source.failures, new Set(options.confirmed.map(({ observationId, subject }) => `${subject}\u0000${observationId}`))), Effect.fnUntraced(function* (candidate) {
|
|
4068
4095
|
const goalId = candidate.head.goal.id;
|
|
4069
4096
|
const previous = stamped.get(goalId);
|
|
@@ -4076,7 +4103,7 @@ export class WorkStore {
|
|
|
4076
4103
|
// holds the reconciler's id, a timestamp past the bound) is left for
|
|
4077
4104
|
// its owner: reported as a conflict, never failing the other goals.
|
|
4078
4105
|
if (Result.isFailure(checkpoint))
|
|
4079
|
-
return { _tag: "conflict", goalId };
|
|
4106
|
+
return { _tag: "conflict", goalId, reason: "checkpoint" };
|
|
4080
4107
|
return yield* appendAt(checkpoint.success, {
|
|
4081
4108
|
fact: { observationId: candidate.fact.observationId, subject: candidate.fact.subject },
|
|
4082
4109
|
head: candidate.head,
|
|
@@ -4085,10 +4112,40 @@ export class WorkStore {
|
|
|
4085
4112
|
_tag: "applied",
|
|
4086
4113
|
eventId,
|
|
4087
4114
|
goalId,
|
|
4088
|
-
state: candidate.pullRequest.state === "merged" ? "completed" : "abandoned"
|
|
4089
|
-
|
|
4115
|
+
state: candidate.pullRequest.state === "merged" ? "completed" : "abandoned",
|
|
4116
|
+
candidate
|
|
4117
|
+
}), Effect.catchTag("WorkGoalRevisionConflictError", () => Effect.succeed({ _tag: "conflict", goalId, reason: "revision" })));
|
|
4090
4118
|
}));
|
|
4091
4119
|
});
|
|
4120
|
+
reconcile = Effect.fn("WorkStore.reconcile")(function* (request) {
|
|
4121
|
+
const steps = yield* this.reconcileSteps(request, "write");
|
|
4122
|
+
return steps.map((step) => step._tag === "applied"
|
|
4123
|
+
? { _tag: "applied", eventId: step.eventId, goalId: step.goalId, state: step.state }
|
|
4124
|
+
: step._tag === "recorded"
|
|
4125
|
+
? step
|
|
4126
|
+
: { _tag: "conflict", goalId: step.goalId });
|
|
4127
|
+
});
|
|
4128
|
+
/**
|
|
4129
|
+
* What `reconcile` would do now with the same confirmations, writing
|
|
4130
|
+
* nothing: every check it makes before writing runs, each in a transaction
|
|
4131
|
+
* that is rolled back at once, with each step checked against the
|
|
4132
|
+
* checkpoints earlier steps planned.
|
|
4133
|
+
*/
|
|
4134
|
+
planReconcile = Effect.fn("WorkStore.planReconcile")(function* (request) {
|
|
4135
|
+
const steps = yield* this.reconcileSteps(request, "check");
|
|
4136
|
+
return steps.map((step) => step._tag === "applied"
|
|
4137
|
+
? {
|
|
4138
|
+
_tag: "would_apply",
|
|
4139
|
+
eventId: step.eventId,
|
|
4140
|
+
goalId: step.goalId,
|
|
4141
|
+
state: step.state,
|
|
4142
|
+
subject: step.candidate.fact.subject,
|
|
4143
|
+
observationId: step.candidate.fact.observationId,
|
|
4144
|
+
goalEventId: step.candidate.head.eventId,
|
|
4145
|
+
goalUpdatedAt: step.candidate.head.goal.updatedAt
|
|
4146
|
+
}
|
|
4147
|
+
: step);
|
|
4148
|
+
});
|
|
4092
4149
|
observe = Effect.fn("WorkStore.observe")(function* (envelopes) {
|
|
4093
4150
|
const decoded = yield* Schema.decodeUnknownEffect(Schema.Array(WorkObservationEnvelope).check(Schema.isMaxLength(workObservedFactMaxRecords)))(envelopes).pipe(Effect.mapError(storeError("observe.decode")));
|
|
4094
4151
|
const cryptoService = this.#cryptoService;
|