mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-08-15 15:09:43 +00:00
Allow projections to pass node state in the delta.
https://github.com/the-draupnir-project/planning/issues/123. The existing projections have been changed to the new API, but they probably could be optimised now that we have this feature.
This commit is contained in:
@@ -38,14 +38,15 @@ function watchDeltaForMemberBanIntents(
|
||||
);
|
||||
return memberBanIntentProjectionNode.reduceInput(
|
||||
setMembershipPoliciesRevisionDelta
|
||||
);
|
||||
).downstreamDelta;
|
||||
}
|
||||
|
||||
function watchDeltaForServerBanIntents(
|
||||
watchedPoliciesDelta: PolicyRuleChange[],
|
||||
serverBanIntentProjectionNode: ServerBanIntentProjectionNode
|
||||
) {
|
||||
return serverBanIntentProjectionNode.reduceInput(watchedPoliciesDelta);
|
||||
return serverBanIntentProjectionNode.reduceInput(watchedPoliciesDelta)
|
||||
.downstreamDelta;
|
||||
}
|
||||
|
||||
export type WatchPolicyRoomPreview = {
|
||||
|
||||
@@ -70,9 +70,14 @@ export class ProjectionOutputHelper<
|
||||
const delta = previousNode.reduceInput(input);
|
||||
this.currentNode = previousNode.reduceDelta(delta) as TProjectionNode;
|
||||
for (const output of this.outputs) {
|
||||
output.applyInput(delta);
|
||||
output.applyInput(delta.downstreamDelta);
|
||||
}
|
||||
this.emitter.emit("projection", this.currentNode, delta, previousNode);
|
||||
this.emitter.emit(
|
||||
"projection",
|
||||
this.currentNode,
|
||||
delta.downstreamDelta,
|
||||
previousNode
|
||||
);
|
||||
}
|
||||
|
||||
addOutput(projection: Projection): this {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: 2025 Gnuxie <Gnuxie@protonmail.com>
|
||||
// SPDX-FileCopyrightText: 2025 - 2026 Gnuxie <Gnuxie@protonmail.com>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
@@ -25,25 +25,75 @@ export type ExtractInputProjectionNodes<
|
||||
TProjectionNode extends ProjectionNode,
|
||||
> = TProjectionNode extends ProjectionNode<infer TInputs> ? TInputs : never;
|
||||
|
||||
export type ProjectionNodeDelta<
|
||||
TDownstreamDeltaShape = unknown,
|
||||
TNodeStateDeltaShape = unknown,
|
||||
> = {
|
||||
/**
|
||||
* The externally visible delta produced by this node. This is the only
|
||||
* delta that should be propagated to downstream projection inputs and
|
||||
* projection listeners.
|
||||
*/
|
||||
readonly downstreamDelta: TDownstreamDeltaShape;
|
||||
/**
|
||||
* The authoritative state transition for this node. This delta must contain
|
||||
* all information needed by `reduceDelta` to update the node's internal
|
||||
* state, even when the externally visible downstream delta is empty.
|
||||
*/
|
||||
readonly nodeStateDelta: TNodeStateDeltaShape;
|
||||
};
|
||||
|
||||
export type ProjectionNode<
|
||||
TInputs extends ProjectionNode[] | unknown[] = unknown[],
|
||||
TDeltaShape = unknown,
|
||||
TDownstreamDeltaShape = unknown,
|
||||
TNodeStateDeltaShape = unknown,
|
||||
TAccessMixin = Record<never, never>,
|
||||
> = {
|
||||
readonly ulid: ULID;
|
||||
// Whether the projection has no state at all.
|
||||
isEmpty(): boolean;
|
||||
reduceInput(input: ExtractInputDeltaShapes<TInputs>): TDeltaShape;
|
||||
/**
|
||||
* Reduces an input delta into a full projection-node delta.
|
||||
*
|
||||
* The downstream delta is the published effect for downstream projections.
|
||||
* The node-state delta is the complete internal state transition for this
|
||||
* node. These must be derived from the same input and previous node state so
|
||||
* that persisted nodes can be rebuilt by replaying or recomputing node-state
|
||||
* deltas while still producing corrective downstream deltas.
|
||||
*/
|
||||
reduceInput(
|
||||
input: ExtractInputDeltaShapes<TInputs>
|
||||
): ProjectionNodeDelta<TDownstreamDeltaShape, TNodeStateDeltaShape>;
|
||||
/**
|
||||
* Applies a full projection-node delta to produce the next node.
|
||||
*
|
||||
* Implementations must update internal state from `nodeStateDelta`, not from
|
||||
* `downstreamDelta`. The downstream delta can omit internal transitions that
|
||||
* do not cross a downstream-visible boundary, so using it as the source of
|
||||
* truth can make the node inconsistent with its persisted/rebuilt state.
|
||||
*/
|
||||
reduceDelta(
|
||||
input: TDeltaShape
|
||||
): ProjectionNode<TInputs, TDeltaShape, TAccessMixin>;
|
||||
projectionNodeDelta: ProjectionNodeDelta<
|
||||
TDownstreamDeltaShape,
|
||||
TNodeStateDeltaShape
|
||||
>
|
||||
): ProjectionNode<
|
||||
TInputs,
|
||||
TDownstreamDeltaShape,
|
||||
TNodeStateDeltaShape,
|
||||
TAccessMixin
|
||||
>;
|
||||
/**
|
||||
* Produces the initial delta, can only be used when the revision is empty.
|
||||
* Otherwise you must use reduceRebuild.
|
||||
*/
|
||||
reduceInitialInputs(input: TInputs): TDeltaShape;
|
||||
reduceInitialInputs(
|
||||
input: TInputs
|
||||
): ProjectionNodeDelta<TDownstreamDeltaShape, TNodeStateDeltaShape>;
|
||||
// only needed for persistent storage
|
||||
reduceRebuild?(inputs: TInputs): TDeltaShape;
|
||||
reduceRebuild?(
|
||||
inputs: TInputs
|
||||
): ProjectionNodeDelta<TDownstreamDeltaShape, TNodeStateDeltaShape>;
|
||||
} & TAccessMixin;
|
||||
|
||||
export type AnyProjectionNode = ProjectionNode<never>;
|
||||
|
||||
+26
-13
@@ -11,6 +11,7 @@ import { ULID, ULIDFactory } from "ulidx";
|
||||
import {
|
||||
ExtractInputDeltaShapes,
|
||||
ProjectionNode,
|
||||
ProjectionNodeDelta,
|
||||
} from "../../../Projection/ProjectionNode";
|
||||
import {
|
||||
MemberPolicyMatch,
|
||||
@@ -33,7 +34,8 @@ import { ListMultiMap } from "../../../Projection/ListMultiMap";
|
||||
*/
|
||||
export type MemberBanInputProjectionNode = ProjectionNode<
|
||||
never[],
|
||||
MembershipPolicyRevisionDelta
|
||||
MembershipPolicyRevisionDelta,
|
||||
undefined
|
||||
> &
|
||||
MembershipPolicyRevision;
|
||||
|
||||
@@ -57,6 +59,7 @@ function isPolicyRelevant(policy: LiteralPolicyRule | GlobPolicyRule): boolean {
|
||||
export type MemberBanIntentProjectionNode = ProjectionNode<
|
||||
[MemberBanInputProjectionNode],
|
||||
MemberBanIntentProjectionDelta,
|
||||
undefined,
|
||||
{
|
||||
allMembersWithRules(): MemberPolicyMatches[];
|
||||
allRulesMatchingMember(
|
||||
@@ -135,16 +138,23 @@ export class StandardMemberBanIntentProjectionNode implements MemberBanIntentPro
|
||||
|
||||
reduceInput(
|
||||
input: ExtractInputDeltaShapes<[MemberBanInputProjectionNode]>
|
||||
): MemberBanIntentProjectionDelta {
|
||||
return MemberBanIntentProjectionNodeHelper.reduceIntentDelta(
|
||||
MemberBanIntentProjectionNodeHelper.reduceMembershipPolicyDelta(input),
|
||||
this.intents
|
||||
);
|
||||
): ProjectionNodeDelta<MemberBanIntentProjectionDelta, undefined> {
|
||||
return {
|
||||
downstreamDelta: MemberBanIntentProjectionNodeHelper.reduceIntentDelta(
|
||||
MemberBanIntentProjectionNodeHelper.reduceMembershipPolicyDelta(input),
|
||||
this.intents
|
||||
),
|
||||
nodeStateDelta: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
reduceDelta(
|
||||
input: MemberBanIntentProjectionDelta
|
||||
): StandardMemberBanIntentProjectionNode {
|
||||
projectionNodeDelta: ProjectionNodeDelta<
|
||||
MemberBanIntentProjectionDelta,
|
||||
undefined
|
||||
>
|
||||
): MemberBanIntentProjectionNode {
|
||||
const input = projectionNodeDelta.downstreamDelta;
|
||||
let nextIntents = this.intents;
|
||||
nextIntents = ListMultiMap.addValues(
|
||||
nextIntents,
|
||||
@@ -164,7 +174,7 @@ export class StandardMemberBanIntentProjectionNode implements MemberBanIntentPro
|
||||
|
||||
reduceInitialInputs([membershipPolicyRevision]: [
|
||||
MemberBanInputProjectionNode,
|
||||
]): MemberBanIntentProjectionDelta {
|
||||
]): ProjectionNodeDelta<MemberBanIntentProjectionDelta, undefined> {
|
||||
if (!this.isEmpty()) {
|
||||
throw new TypeError(
|
||||
"This can only be called on an empty projection node"
|
||||
@@ -177,10 +187,13 @@ export class StandardMemberBanIntentProjectionNode implements MemberBanIntentPro
|
||||
)
|
||||
.flat();
|
||||
return {
|
||||
add: matches,
|
||||
ban: matches.map((match) => match.userID),
|
||||
remove: [],
|
||||
recall: [],
|
||||
downstreamDelta: {
|
||||
add: matches,
|
||||
ban: matches.map((match) => match.userID),
|
||||
remove: [],
|
||||
recall: [],
|
||||
},
|
||||
nodeStateDelta: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -14,5 +14,6 @@ import { ProjectionNode } from "../../../Projection/ProjectionNode";
|
||||
export type PolicyListBridgeProjectionNode = ProjectionNode<
|
||||
[],
|
||||
PolicyRuleChange[],
|
||||
undefined,
|
||||
PolicyListRevision
|
||||
>;
|
||||
|
||||
+28
-11
@@ -8,7 +8,10 @@
|
||||
// </text>
|
||||
|
||||
import { StringServerName } from "@the-draupnir-project/matrix-basic-types";
|
||||
import { ProjectionNode } from "../../../Projection/ProjectionNode";
|
||||
import {
|
||||
ProjectionNode,
|
||||
ProjectionNodeDelta,
|
||||
} from "../../../Projection/ProjectionNode";
|
||||
import { PolicyListBridgeProjectionNode } from "./PolicyListBridgeProjection";
|
||||
import {
|
||||
PolicyRuleChange,
|
||||
@@ -38,6 +41,7 @@ export type ServerBanIntentProjectionDelta = {
|
||||
export type ServerBanIntentProjectionNode = ProjectionNode<
|
||||
[PolicyListBridgeProjectionNode],
|
||||
ServerBanIntentProjectionDelta,
|
||||
undefined,
|
||||
{
|
||||
deny: StringServerName[];
|
||||
}
|
||||
@@ -122,15 +126,21 @@ export class StandardServerBanIntentProjectionNode implements ServerBanIntentPro
|
||||
);
|
||||
}
|
||||
|
||||
reduceInput(input: PolicyRuleChange[]): ServerBanIntentProjectionDelta {
|
||||
return ServerBanIntentProjectionHelper.reduceIntentDelta(
|
||||
ServerBanIntentProjectionHelper.reducePolicyDelta(input),
|
||||
this.policies
|
||||
);
|
||||
reduceInput(
|
||||
input: PolicyRuleChange[]
|
||||
): ProjectionNodeDelta<ServerBanIntentProjectionDelta, undefined> {
|
||||
return {
|
||||
downstreamDelta: ServerBanIntentProjectionHelper.reduceIntentDelta(
|
||||
ServerBanIntentProjectionHelper.reducePolicyDelta(input),
|
||||
this.policies
|
||||
),
|
||||
nodeStateDelta: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
reduceInitialInputs([policyListRevision]: [
|
||||
PolicyListBridgeProjectionNode,
|
||||
]): ServerBanIntentProjectionDelta {
|
||||
]): ProjectionNodeDelta<ServerBanIntentProjectionDelta, undefined> {
|
||||
if (!this.isEmpty()) {
|
||||
throw new TypeError("Cannot reduce initial inputs when inialised");
|
||||
}
|
||||
@@ -145,21 +155,28 @@ export class StandardServerBanIntentProjectionNode implements ServerBanIntentPro
|
||||
),
|
||||
].filter((rule) => rule.matchType !== PolicyRuleMatchType.HashedLiteral);
|
||||
const names = new Set(serverPolicies.map((policy) => policy.entity));
|
||||
return {
|
||||
const downstreamDelta = {
|
||||
add: serverPolicies,
|
||||
deny: [...names] as StringServerName[],
|
||||
remove: [],
|
||||
recall: [],
|
||||
};
|
||||
return {
|
||||
downstreamDelta,
|
||||
nodeStateDelta: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.policies.size === 0;
|
||||
}
|
||||
|
||||
reduceDelta(
|
||||
input: ServerBanIntentProjectionDelta
|
||||
): ServerBanIntentProjectionNode {
|
||||
reduceDelta({
|
||||
downstreamDelta: input,
|
||||
}: ProjectionNodeDelta<
|
||||
ServerBanIntentProjectionDelta,
|
||||
undefined
|
||||
>): ServerBanIntentProjectionNode {
|
||||
let nextPolicies = this.policies;
|
||||
nextPolicies = ListMultiMap.addValues(
|
||||
nextPolicies,
|
||||
|
||||
Reference in New Issue
Block a user