Make the node state delta authoritative in existing projections.

https://github.com/the-draupnir-project/planning/issues/123
This commit is contained in:
gnuxie
2026-04-20 13:09:24 +01:00
parent ec2a6f152c
commit 614fc8936d
2 changed files with 60 additions and 34 deletions
@@ -39,14 +39,17 @@ export type MemberBanInputProjectionNode = ProjectionNode<
> &
MembershipPolicyRevision;
export interface MemberBanIntentProjectionDelta {
ban: StringUserID[];
recall: StringUserID[];
}
// use add/remove for steady state intents
// When the intent becomes effectual, matches will be removed
// upstream and so this model will remain consistent
export interface MemberBanIntentProjectionDelta {
export interface MemberBanIntentProjectionStateDelta {
add: MemberPolicyMatch[];
remove: MemberPolicyMatch[];
ban: StringUserID[];
recall: StringUserID[];
}
function isPolicyRelevant(policy: LiteralPolicyRule | GlobPolicyRule): boolean {
@@ -59,7 +62,7 @@ function isPolicyRelevant(policy: LiteralPolicyRule | GlobPolicyRule): boolean {
export type MemberBanIntentProjectionNode = ProjectionNode<
[MemberBanInputProjectionNode],
MemberBanIntentProjectionDelta,
undefined,
MemberBanIntentProjectionStateDelta,
{
allMembersWithRules(): MemberPolicyMatches[];
allRulesMatchingMember(
@@ -71,8 +74,8 @@ export type MemberBanIntentProjectionNode = ProjectionNode<
export const MemberBanIntentProjectionNodeHelper = Object.freeze({
reduceMembershipPolicyDelta(
input: MembershipPolicyRevisionDelta
): Pick<MemberBanIntentProjectionDelta, "add" | "remove"> {
const output: Pick<MemberBanIntentProjectionDelta, "add" | "remove"> = {
): MemberBanIntentProjectionStateDelta {
const output: MemberBanIntentProjectionStateDelta = {
add: [],
remove: [],
};
@@ -89,7 +92,7 @@ export const MemberBanIntentProjectionNodeHelper = Object.freeze({
return output;
},
reduceIntentDelta(
input: Pick<MemberBanIntentProjectionDelta, "add" | "remove">,
input: MemberBanIntentProjectionStateDelta,
policies: PersistentMap<
StringUserID,
List<LiteralPolicyRule | GlobPolicyRule>
@@ -102,7 +105,6 @@ export const MemberBanIntentProjectionNodeHelper = Object.freeze({
(rule) => rule.entity as StringUserID
);
return {
...input,
ban: intents.intend,
recall: intents.recall,
};
@@ -138,23 +140,28 @@ export class StandardMemberBanIntentProjectionNode implements MemberBanIntentPro
reduceInput(
input: ExtractInputDeltaShapes<[MemberBanInputProjectionNode]>
): ProjectionNodeDelta<MemberBanIntentProjectionDelta, undefined> {
): ProjectionNodeDelta<
MemberBanIntentProjectionDelta,
MemberBanIntentProjectionStateDelta
> {
const nodeStateDelta =
MemberBanIntentProjectionNodeHelper.reduceMembershipPolicyDelta(input);
return {
downstreamDelta: MemberBanIntentProjectionNodeHelper.reduceIntentDelta(
MemberBanIntentProjectionNodeHelper.reduceMembershipPolicyDelta(input),
nodeStateDelta,
this.intents
),
nodeStateDelta: undefined,
nodeStateDelta,
};
}
reduceDelta(
projectionNodeDelta: ProjectionNodeDelta<
MemberBanIntentProjectionDelta,
undefined
MemberBanIntentProjectionStateDelta
>
): MemberBanIntentProjectionNode {
const input = projectionNodeDelta.downstreamDelta;
const input = projectionNodeDelta.nodeStateDelta;
let nextIntents = this.intents;
nextIntents = ListMultiMap.addValues(
nextIntents,
@@ -174,7 +181,10 @@ export class StandardMemberBanIntentProjectionNode implements MemberBanIntentPro
reduceInitialInputs([membershipPolicyRevision]: [
MemberBanInputProjectionNode,
]): ProjectionNodeDelta<MemberBanIntentProjectionDelta, undefined> {
]): ProjectionNodeDelta<
MemberBanIntentProjectionDelta,
MemberBanIntentProjectionStateDelta
> {
if (!this.isEmpty()) {
throw new TypeError(
"This can only be called on an empty projection node"
@@ -183,17 +193,21 @@ export class StandardMemberBanIntentProjectionNode implements MemberBanIntentPro
const matches = membershipPolicyRevision
.allMembersWithRules()
.map((member) =>
member.policies.map((policy) => ({ userID: member.userID, policy }))
member.policies
.filter(isPolicyRelevant)
.map((policy) => ({ userID: member.userID, policy }))
)
.flat();
const nodeStateDelta = {
add: matches,
remove: [],
};
return {
downstreamDelta: {
add: matches,
ban: matches.map((match) => match.userID),
remove: [],
ban: [...new Set(matches.map((match) => match.userID))],
recall: [],
},
nodeStateDelta: undefined,
nodeStateDelta,
};
}
@@ -31,6 +31,9 @@ import { ListMultiMap } from "../../../Projection/ListMultiMap";
export type ServerBanIntentProjectionDelta = {
deny: StringServerName[];
recall: StringServerName[];
};
export type ServerBanIntentProjectionStateDelta = {
add: (LiteralPolicyRule | GlobPolicyRule)[];
remove: (LiteralPolicyRule | GlobPolicyRule)[];
};
@@ -41,7 +44,7 @@ export type ServerBanIntentProjectionDelta = {
export type ServerBanIntentProjectionNode = ProjectionNode<
[PolicyListBridgeProjectionNode],
ServerBanIntentProjectionDelta,
undefined,
ServerBanIntentProjectionStateDelta,
{
deny: StringServerName[];
}
@@ -50,11 +53,11 @@ export type ServerBanIntentProjectionNode = ProjectionNode<
export const ServerBanIntentProjectionHelper = Object.freeze({
reducePolicyDelta(
input: PolicyRuleChange[]
): Pick<ServerBanIntentProjectionDelta, "add" | "remove"> {
const output: Pick<ServerBanIntentProjectionDelta, "add" | "remove"> = {
): ServerBanIntentProjectionStateDelta {
const output: ServerBanIntentProjectionStateDelta = {
add: [],
remove: [],
} satisfies Pick<ServerBanIntentProjectionDelta, "add" | "remove">;
};
for (const change of input) {
if (change.rule.kind !== PolicyRuleType.Server) {
continue;
@@ -85,7 +88,7 @@ export const ServerBanIntentProjectionHelper = Object.freeze({
},
reduceIntentDelta(
input: Pick<ServerBanIntentProjectionDelta, "add" | "remove">,
input: ServerBanIntentProjectionStateDelta,
policies: PersistentMap<
StringServerName,
List<GlobPolicyRule | LiteralPolicyRule>
@@ -98,7 +101,6 @@ export const ServerBanIntentProjectionHelper = Object.freeze({
(rule) => rule.entity as StringServerName
);
return {
...input,
deny: intents.intend,
recall: intents.recall,
};
@@ -128,19 +130,27 @@ export class StandardServerBanIntentProjectionNode implements ServerBanIntentPro
reduceInput(
input: PolicyRuleChange[]
): ProjectionNodeDelta<ServerBanIntentProjectionDelta, undefined> {
): ProjectionNodeDelta<
ServerBanIntentProjectionDelta,
ServerBanIntentProjectionStateDelta
> {
const nodeStateDelta =
ServerBanIntentProjectionHelper.reducePolicyDelta(input);
return {
downstreamDelta: ServerBanIntentProjectionHelper.reduceIntentDelta(
ServerBanIntentProjectionHelper.reducePolicyDelta(input),
nodeStateDelta,
this.policies
),
nodeStateDelta: undefined,
nodeStateDelta,
};
}
reduceInitialInputs([policyListRevision]: [
PolicyListBridgeProjectionNode,
]): ProjectionNodeDelta<ServerBanIntentProjectionDelta, undefined> {
]): ProjectionNodeDelta<
ServerBanIntentProjectionDelta,
ServerBanIntentProjectionStateDelta
> {
if (!this.isEmpty()) {
throw new TypeError("Cannot reduce initial inputs when inialised");
}
@@ -156,14 +166,16 @@ export class StandardServerBanIntentProjectionNode implements ServerBanIntentPro
].filter((rule) => rule.matchType !== PolicyRuleMatchType.HashedLiteral);
const names = new Set(serverPolicies.map((policy) => policy.entity));
const downstreamDelta = {
add: serverPolicies,
deny: [...names] as StringServerName[],
remove: [],
recall: [],
};
const nodeStateDelta = {
add: serverPolicies,
remove: [],
};
return {
downstreamDelta,
nodeStateDelta: undefined,
nodeStateDelta,
};
}
@@ -172,10 +184,10 @@ export class StandardServerBanIntentProjectionNode implements ServerBanIntentPro
}
reduceDelta({
downstreamDelta: input,
nodeStateDelta: input,
}: ProjectionNodeDelta<
ServerBanIntentProjectionDelta,
undefined
ServerBanIntentProjectionStateDelta
>): ServerBanIntentProjectionNode {
let nextPolicies = this.policies;
nextPolicies = ListMultiMap.addValues(