Rename ProjectionLocator to ProjectionAllocator and wire in.

The reason why we can't have an orchestrator own the projects yet is
simply because currently the projections are created from _protected
rooms set_ specific revision issuers that are disposed alongside
it. And of course if we were naive and implemented the global
orchestrator, we'd be tied to a stale protected rooms set if a
Draupnir was restarted via safe mode or whatever.
This commit is contained in:
gnuxie
2026-09-16 12:42:34 +01:00
parent 341640d639
commit 2b655aa0ea
14 changed files with 486 additions and 334 deletions
+3 -1
View File
@@ -14,6 +14,7 @@ import {
ClientsInRoomMap,
Task,
Logger,
makeProtectedRoomsSetProjectionAllocator,
ActionException,
ActionExceptionKind,
ConfigRecoverableError,
@@ -171,7 +172,8 @@ export class DraupnirBotModeToggle implements BotModeTogle {
clientCapabilityFactory,
clientProvider,
roomStateManagerFactory,
stores
stores,
makeProtectedRoomsSetProjectionAllocator()
);
return new DraupnirBotModeToggle(
clientUserID,
@@ -24,6 +24,7 @@ import {
ActionExceptionKind,
ActionResult,
ClientsInRoomMap,
makeProtectedRoomsSetProjectionAllocator,
Ok,
Task,
assertThrowableIsError,
@@ -85,7 +86,8 @@ export class AppServiceDraupnirManager {
this.clientCapabilityFactory,
clientProvider,
this.roomStateManagerFactory,
stores
stores,
makeProtectedRoomsSetProjectionAllocator()
);
this.baseManager = new StandardDraupnirManager(draupnirFactory);
}
@@ -7,6 +7,8 @@ import {
ClientsInRoomMap,
Logger,
Ok,
ProjectionAllocator,
ProtectedRoomsSet,
StandardLoggableConfigTracker,
isError,
} from "@the-draupnir-project/matrix-protection-suite";
@@ -64,7 +66,8 @@ export class DraupnirFactory {
private readonly clientCapabilityFactory: ClientCapabilityFactory,
private readonly clientProvider: ClientForUserID,
private readonly roomStateManagerFactory: RoomStateManagerFactory,
private readonly stores: TopLevelStores
private readonly stores: TopLevelStores,
private readonly projectionAllocator: ProjectionAllocator<ProtectedRoomsSet>
) {
// nothing to do.
}
@@ -102,6 +105,7 @@ export class DraupnirFactory {
client,
clientPlatform,
clientUserID,
this.projectionAllocator,
config,
configLogTracker
);
@@ -26,6 +26,7 @@ import {
Ok,
PolicyListConfig,
PolicyRoomManager,
ProjectionAllocator,
ProtectedRoomsConfig,
ProtectedRoomsSet,
ProtectionHandleRegistryDescription,
@@ -173,6 +174,7 @@ export async function makeProtectedRoomsSet(
client: MatrixSendClient,
clientPlatform: ClientPlatform,
userID: StringUserID,
projectionAllocator: ProjectionAllocator<ProtectedRoomsSet>,
config: IConfig,
loggableConfigTracker: LoggableConfigTracker
): Promise<ActionResult<ProtectedRoomsSet>> {
@@ -226,6 +228,7 @@ export async function makeProtectedRoomsSet(
protectionsConfig.ok,
userID,
DefaultMixinExtractor,
projectionAllocator,
makeHandleMissingProtectionPermissions(
clientPlatform.toRoomMessageSender(),
managementRoom.toRoomIDOrAlias()
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2026 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0
import { Ok } from "@gnuxie/typescript-result";
import {
ProjectionAllocator,
StandardProjectionAllocator,
} from "./ProjectionAllocator";
test("ProjectionAllocator semantics are implemented by the standard allocator", async () => {
await ProjectionAllocator.check(async () => {
return Ok(new StandardProjectionAllocator());
});
});
@@ -0,0 +1,414 @@
// SPDX-FileCopyrightText: 2026 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0
import { Ok, Result, ResultError, isError } from "@gnuxie/typescript-result";
import {
AnyProjectionDescription,
ProjectionPartition,
describeProjection,
} from "./ProjectionDescription";
import { ExtractProjectionNode, Projection } from "./Projection";
import { ProjectionNode } from "./ProjectionNode";
import {
AllocatableLifetime,
Disposable,
StandardLifetime,
} from "../Interface/Lifetime";
import { SemanticType } from "../Interface/SemanticType";
export type DisposableProjection<TProjection extends Projection = Projection> =
TProjection & Disposable;
export type ProjectionDescriptionFor<TProjection extends Projection> =
ExtractProjectionNode<TProjection> extends ProjectionNode<infer TDescription>
? TDescription
: never;
/**
* Constructs registered projections and allocates them against a supplied lifetime.
*
* The allocator exists because currently we can't make a global orchestrator
* own projections while there are context specific dependencies required by
* the projection, specifically dependencies that are't invariant between protected
* room set instances, such as legacy revision issuers, as opposed to the draupnir
* user ID.
*/
export interface ProjectionAllocator<Context = unknown> {
registerProjection<TProjection extends DisposableProjection>(
description: ProjectionDescriptionFor<TProjection>,
factory: (
context: Context,
partition: ProjectionPartition<ProjectionDescriptionFor<TProjection>>
) => Result<TProjection>
): this;
allocate<TProjection extends DisposableProjection>(
lifetime: AllocatableLifetime,
context: Context,
description: ProjectionDescriptionFor<TProjection>,
partition: ProjectionPartition<ProjectionDescriptionFor<TProjection>>
): Result<TProjection>;
}
export const ProjectionAllocator = SemanticType<ProjectionAllocator>(
"ProjectionAllocator"
)
.declare({
allocation: {
what: "allocator can allocate registered projections",
why: "allows callers to construct projections without depending on their concrete implementations",
},
lifetimeOwnership: {
what: "allocated projections are owned by the supplied lifetime",
why: "ensures projection resources are disposed with their caller",
},
freshAllocation: {
what: "each allocation invokes the registered projection factory",
why: "keeps projection identity and ownership independent between callers",
},
failWhenLifetimeInDisposal: {
what: "allocation fails without invoking the projection factory when the supplied lifetime is in disposal",
why: "prevents projections from being created without an owner that can dispose them",
},
canonicalDescription: {
what: "only the projection description registered with a factory can be used to allocate that projection",
why: "prevents a different description with the same name from selecting a factory with an incompatible partition schema",
},
factoryFailure: {
what: "an error returned by a projection factory is returned by allocation",
why: "allows callers to handle projection construction failures without receiving a partially allocated projection",
},
failWhenNotRegistered: {
what: "allocating a projection that is unregistered fails early and errors",
why: "prevents mistakes where the wrong projection is requested or registration code is forgotten",
},
partitionIsEssential: {
what: "allocating a projection requires its partition information",
why: "prevents leaking projections to the wrong code and catches bugs early",
},
failWhenUnexpectedPartition: {
what: "allocating a projection fails if extra or unexpected partition data is provided",
why: "prevents accidentally leaking projections to the wrong code and catches the problem early",
},
})
.verify({
async allocation(makeSubject) {
const allocator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticTestProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjection = DisposableProjection<
Projection<ProjectionNode<typeof description>>
>;
const projection = {
[Symbol.dispose]() {
// nothing to dispose.
},
} as TestProjection;
const context = { source: "semantic context" };
allocator.registerProjection<TestProjection>(
description,
(factoryContext, partition) => {
expect(factoryContext).toBe(context);
expect(partition.roomID).toBe("!room:example.org");
return Ok(projection);
}
);
await using lifetime = new StandardLifetime();
const result = allocator.allocate<TestProjection>(
lifetime,
context,
description,
{
roomID: "!room:example.org",
}
);
expect(result.expect("Projection allocation should succeed")).toBe(
projection
);
},
async lifetimeOwnership(makeSubject) {
const allocator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticLifetimeProjection",
partitionKeys: [],
}).build();
type TestProjection = DisposableProjection<
Projection<ProjectionNode<typeof description>>
>;
let disposalCount = 0;
allocator.registerProjection<TestProjection>(description, () => {
return Ok({
[Symbol.dispose]() {
disposalCount += 1;
},
} as TestProjection);
});
{
await using lifetime = new StandardLifetime();
allocator
.allocate<TestProjection>(lifetime, {}, description, {})
.expect("Projection allocation should succeed");
}
expect(disposalCount).toBe(1);
},
async freshAllocation(makeSubject) {
const allocator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticFreshProjection",
partitionKeys: [],
}).build();
type TestProjection = DisposableProjection<
Projection<ProjectionNode<typeof description>>
>;
let allocationCount = 0;
allocator.registerProjection<TestProjection>(description, () => {
allocationCount += 1;
return Ok({ [Symbol.dispose]() {} } as TestProjection);
});
await using lifetime = new StandardLifetime();
const first = allocator
.allocate<TestProjection>(lifetime, {}, description, {})
.expect("First projection allocation should succeed");
const second = allocator
.allocate<TestProjection>(lifetime, {}, description, {})
.expect("Second projection allocation should succeed");
expect(first).not.toBe(second);
expect(allocationCount).toBe(2);
},
async failWhenLifetimeInDisposal(makeSubject) {
const allocator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticDisposedLifetimeProjection",
partitionKeys: [],
}).build();
type TestProjection = DisposableProjection<
Projection<ProjectionNode<typeof description>>
>;
let factoryInvocations = 0;
allocator.registerProjection<TestProjection>(description, () => {
factoryInvocations += 1;
return Ok({ [Symbol.dispose]() {} } as TestProjection);
});
const lifetime = new StandardLifetime();
await lifetime[Symbol.asyncDispose]();
const result = allocator.allocate<TestProjection>(
lifetime,
{},
description,
{}
);
expect(isError(result)).toBe(true);
expect(factoryInvocations).toBe(0);
},
async canonicalDescription(makeSubject) {
const allocator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticCanonicalProjection",
partitionKeys: ["roomID"],
}).build();
const equivalentDescription = describeProjection({
name: "SemanticCanonicalProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjection = DisposableProjection<
Projection<ProjectionNode<typeof description>>
>;
allocator.registerProjection<TestProjection>(description, () =>
Ok({ [Symbol.dispose]() {} } as TestProjection)
);
await using lifetime = new StandardLifetime();
const result = allocator.allocate<TestProjection>(
lifetime,
{},
equivalentDescription,
{ roomID: "!room:example.org" }
);
expect(isError(result)).toBe(true);
},
async factoryFailure(makeSubject) {
const allocator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticFailingFactoryProjection",
partitionKeys: [],
}).build();
type TestProjection = DisposableProjection<
Projection<ProjectionNode<typeof description>>
>;
const factoryError = ResultError.Result("Projection creation failed");
allocator.registerProjection<TestProjection>(
description,
() => factoryError
);
await using lifetime = new StandardLifetime();
const result = allocator.allocate<TestProjection>(
lifetime,
{},
description,
{}
);
expect(result).toBe(factoryError);
},
async failWhenNotRegistered(makeSubject) {
const allocator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticUnregisteredProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjection = DisposableProjection<
Projection<ProjectionNode<typeof description>>
>;
await using lifetime = new StandardLifetime();
const result = allocator.allocate<TestProjection>(
lifetime,
{},
description,
{
roomID: "!room:example.org",
}
);
expect(isError(result)).toBe(true);
},
async partitionIsEssential(makeSubject) {
const allocator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticRequiredPartitionProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjection = DisposableProjection<
Projection<ProjectionNode<typeof description>>
>;
await using lifetime = new StandardLifetime();
const result = allocator.allocate<TestProjection>(
lifetime,
{},
description,
{} as never
);
expect(isError(result)).toBe(true);
},
async failWhenUnexpectedPartition(makeSubject) {
const allocator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticUnexpectedPartitionProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjection = DisposableProjection<
Projection<ProjectionNode<typeof description>>
>;
await using lifetime = new StandardLifetime();
const result = allocator.allocate<TestProjection>(
lifetime,
{},
description,
{
roomID: "!room:example.org",
unknownKey: "value",
} as never
);
expect(isError(result)).toBe(true);
},
});
type AnyProjectionFactory<Context> = (
context: Context,
partition: ProjectionPartition
) => Result<DisposableProjection>;
function validateProjectionPartition(
description: AnyProjectionDescription,
partition: ProjectionPartition
): Result<void> {
const expectedKeys = new Set(description.partitionKeys);
const unexpectedKey = Object.keys(partition).find(
(key) => !expectedKeys.has(key)
);
if (unexpectedKey !== undefined) {
return ResultError.Result(
`Partition for projection ${description.name} has unexpected key ${unexpectedKey}`
);
}
const missingKey = description.partitionKeys.find(
(key) => partition[key] === undefined
);
if (missingKey !== undefined) {
return ResultError.Result(
`Partition for projection ${description.name} is missing key ${missingKey}`
);
}
return Ok(undefined);
}
export class StandardProjectionAllocator<
Context = unknown,
> implements ProjectionAllocator<Context> {
private readonly factories = new Map<
AnyProjectionDescription,
AnyProjectionFactory<Context>
>();
private readonly registeredNames = new Set<string>();
public registerProjection<TProjection extends DisposableProjection>(
description: ProjectionDescriptionFor<TProjection>,
factory: (
context: Context,
partition: ProjectionPartition<ProjectionDescriptionFor<TProjection>>
) => Result<TProjection>
): this {
if (this.registeredNames.has(description.name)) {
throw new TypeError(
`There is already a projection registered with the name ${description.name}`
);
}
this.registeredNames.add(description.name);
this.factories.set(
description,
factory as unknown as AnyProjectionFactory<Context>
);
return this;
}
public allocate<TProjection extends DisposableProjection>(
lifetime: AllocatableLifetime,
context: Context,
description: ProjectionDescriptionFor<TProjection>,
partition: ProjectionPartition<ProjectionDescriptionFor<TProjection>>
): Result<TProjection> {
return lifetime.allocateDisposable(() => {
const validationResult = validateProjectionPartition(
description,
partition
);
if (isError(validationResult)) {
return validationResult;
}
const factory = this.factories.get(description);
if (factory === undefined) {
return ResultError.Result(
`No projection factory registered for the supplied ${description.name} description`
);
}
return factory(context, partition) as Result<TProjection>;
});
}
}
@@ -1,49 +0,0 @@
// SPDX-FileCopyrightText: 2026 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0
import { Ok } from "@gnuxie/typescript-result";
import { Projection } from "./Projection";
import { describeProjection } from "./ProjectionDescription";
import {
ProjectionLocator,
StandardProjectionLocator,
} from "./ProjectionLocator";
import { ProjectionNode } from "./ProjectionNode";
const TestProjectionDescription = describeProjection({
name: "TestProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjectionDescription = typeof TestProjectionDescription;
type TestProjectionNode = ProjectionNode<TestProjectionDescription>;
type TestProjection = Projection<TestProjectionNode>;
test("ProjectionLocator semantics are implemented by the standard locator", async () => {
await ProjectionLocator.check(async () => {
return Ok(new StandardProjectionLocator({}));
});
});
test("passes the constructor context to the projection provider", () => {
const context = { source: "context" };
const projection = {} as TestProjection;
const locator = new StandardProjectionLocator(
context
).registerProjection<TestProjection>({
projectionDescription: TestProjectionDescription,
factory(providerContext, location) {
expect(providerContext).toBe(context);
expect(location.partition.roomID).toBe("!room:example.org");
return Ok(projection);
},
});
const result = locator.locate<TestProjection>({
description: TestProjectionDescription,
partition: {
roomID: "!room:example.org",
},
});
expect(result.expect("Projection location should succeed")).toBe(projection);
});
@@ -1,240 +0,0 @@
// SPDX-FileCopyrightText: 2026 Gnuxie <Gnuxie@protonmail.com>
//
// SPDX-License-Identifier: Apache-2.0
import { Ok, Result, ResultError, isError } from "@gnuxie/typescript-result";
import {
AnyProjectionDescription,
ProjectionPartition,
describeProjection,
} from "./ProjectionDescription";
import { ExtractProjectionNode, Projection } from "./Projection";
import { ProjectionNode } from "./ProjectionNode";
import { Disposable } from "../Interface/Lifetime";
import { SemanticType } from "../Interface/SemanticType";
export type DisposableProjection<TProjection extends Projection = Projection> =
TProjection & Disposable;
export type ProjectionDescriptionFor<TProjection extends Projection> =
ExtractProjectionNode<TProjection> extends ProjectionNode<infer TDescription>
? TDescription
: never;
export type ProjectionLocation<
TProjectionDescription extends AnyProjectionDescription =
AnyProjectionDescription,
> = {
readonly description: TProjectionDescription;
readonly partition: ProjectionPartition<TProjectionDescription>;
};
export type ProjectionProviderDescription<
Context = unknown,
TProjectionDescription extends AnyProjectionDescription =
AnyProjectionDescription,
TProjection extends Projection = Projection,
> = {
readonly projectionDescription: TProjectionDescription;
factory(
context: Context,
location: ProjectionLocation<TProjectionDescription>
): Result<TProjection>;
};
// FIXME: We need to be able to attenuate locators to specific
// draupnir instances for internal code/security defence in depth (mostly
// to prevent mistakes).
export interface ProjectionLocator<Context = unknown> {
registerProjection<TProjection extends Projection>(
provider: ProjectionProviderDescription<
Context,
ProjectionDescriptionFor<TProjection>,
TProjection
>
): this;
locate<TProjection extends Projection>(
location: ProjectionLocation<ProjectionDescriptionFor<TProjection>>
): Result<TProjection>;
}
export const ProjectionLocator = SemanticType<ProjectionLocator>(
"ProjectionLocator"
)
.declare({
location: {
what: "locator can locate registered projections and return them",
why: "allows the orchestrator to own projections rather than draupnir instances",
},
failWhenNotLocated: {
what: "locating a projection that is unregistered fails early and errors",
why: "prevents mistakes where the wrong projection is sought or registration code is forgotten",
},
partitionIsEssential: {
what: "locating a projection requires the partition information to be provided",
why: "prevents leaking projections to the wrong code and catches bugs early",
},
failWhenUnexpectedPartition: {
what: "locating a projection fails if extra or unexpected partition data is provided",
why: "prevents accidentally leaking projections to the wrong code (security issue) and catches the problem early",
},
})
.verify({
async location(makeSubject) {
const locator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticTestProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjection = Projection<ProjectionNode<typeof description>>;
const projection = {} as TestProjection;
locator.registerProjection<TestProjection>({
projectionDescription: description,
factory(_context, location) {
expect(location.partition.roomID).toBe("!room:example.org");
return Ok(projection);
},
});
const result = locator.locate<TestProjection>({
description,
partition: {
roomID: "!room:example.org",
},
});
expect(result.expect("Projection location should succeed")).toBe(
projection
);
},
async failWhenNotLocated(makeSubject) {
const locator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticTestProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjection = Projection<ProjectionNode<typeof description>>;
const result = locator.locate<TestProjection>({
description,
partition: {
roomID: "!room:example.org",
},
});
expect(isError(result)).toBe(true);
},
async partitionIsEssential(makeSubject) {
const locator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticTestProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjection = Projection<ProjectionNode<typeof description>>;
const result = locator.locate<TestProjection>({
description,
partition: {},
} as never);
expect(isError(result)).toBe(true);
},
async failWhenUnexpectedPartition(makeSubject) {
const locator = (await makeSubject()).expect(
"Should be able to make the subject"
);
const description = describeProjection({
name: "SemanticTestProjection",
partitionKeys: ["roomID"],
}).build();
type TestProjection = Projection<ProjectionNode<typeof description>>;
const result = locator.locate<TestProjection>({
description,
partition: {
roomID: "!room:example.org",
unknownKey: "value",
},
} as never);
expect(isError(result)).toBe(true);
},
});
type AnyProjectionProviderDescription<Context> =
ProjectionProviderDescription<Context>;
function validateProjectionLocation(
location: ProjectionLocation
): Result<void> {
const expectedKeys = new Set(location.description.partitionKeys);
const unexpectedKey = Object.keys(location.partition).find(
(key) => !expectedKeys.has(key)
);
if (unexpectedKey !== undefined) {
return ResultError.Result(
`Projection location for ${location.description.name} has unexpected partition key ${unexpectedKey}`
);
}
const missingKey = location.description.partitionKeys.find(
(key) => location.partition[key] === undefined
);
if (missingKey !== undefined) {
return ResultError.Result(
`Projection location for ${location.description.name} is missing partition key ${missingKey}`
);
}
return Ok(undefined);
}
export class StandardProjectionLocator<
Context = unknown,
> implements ProjectionLocator<Context> {
private readonly providers = new Map<
string,
AnyProjectionProviderDescription<Context>
>();
public constructor(
private readonly context: Context,
providers: readonly AnyProjectionProviderDescription<Context>[] = []
) {
for (const provider of providers) {
this.registerProjection(provider);
}
}
public registerProjection<TProjection extends Projection>(
provider: ProjectionProviderDescription<
Context,
ProjectionDescriptionFor<TProjection>,
TProjection
>
): this {
if (this.providers.has(provider.projectionDescription.name)) {
throw new TypeError(
`There is already a projection registered with the name ${provider.projectionDescription.name}`
);
}
this.providers.set(
provider.projectionDescription.name,
provider as unknown as AnyProjectionProviderDescription<Context>
);
return this;
}
public locate<TProjection extends Projection>(
location: ProjectionLocation<ProjectionDescriptionFor<TProjection>>
): Result<TProjection> {
const validationResult = validateProjectionLocation(location);
if (isError(validationResult)) {
return validationResult;
}
const provider = this.providers.get(location.description.name);
if (provider === undefined) {
return ResultError.Result(
`No projection registered with the name ${location.description.name}`
);
}
return provider.factory(this.context, location) as Result<TProjection>;
}
}
@@ -58,14 +58,13 @@ import {
import { WatchedPolicyRooms } from "./WatchedPolicyRooms/WatchedPolicyRooms";
import { MixinExtractor } from "../SafeMatrixEvents/EventMixinExtraction/EventMixinExtraction";
import { RoomCreateEvent, RoomVersionMirror } from "../MatrixTypes/CreateRoom";
import { ProjectionLocator } from "../Projection/ProjectionLocator";
import { makeProtectedRoomsSetProjectionLocator } from "./ProtectedRoomsSetProjectionLocator";
import { ProjectionAllocator } from "../Projection/ProjectionAllocator";
export interface ProtectedRoomsSet {
readonly watchedPolicyRooms: WatchedPolicyRooms;
readonly protectedRoomsManager: ProtectedRoomsManager;
readonly protections: ProtectionsManager;
readonly projectionLocator: ProjectionLocator<ProtectedRoomsSet>;
readonly projectionAllocator: ProjectionAllocator<ProtectedRoomsSet>;
readonly setRoomMembership: SetRoomMembership;
readonly setMembership: SetMembershipRevisionIssuer;
readonly setRoomState: SetRoomState;
@@ -103,7 +102,6 @@ export class StandardProtectedRoomsSet implements ProtectedRoomsSet {
private readonly setMembershipPolicyRevisionListener =
this.setMembershipPolicyRevision.bind(this);
public readonly setPoliciesMatchingMembership: SetMembershipPolicyRevisionIssuer;
public readonly projectionLocator: ProjectionLocator<ProtectedRoomsSet>;
constructor(
public readonly watchedPolicyRooms: WatchedPolicyRooms,
@@ -111,6 +109,7 @@ export class StandardProtectedRoomsSet implements ProtectedRoomsSet {
public readonly protections: ProtectionsManager,
public readonly userID: StringUserID,
public readonly eventMixinExtractor: MixinExtractor,
public readonly projectionAllocator: ProjectionAllocator<ProtectedRoomsSet>,
private readonly handleMissingProtectionPermissions?: HandleMissingProtectionPermissions
) {
this.setRoomMembership.on("membership", this.membershipChangeListener);
@@ -127,7 +126,6 @@ export class StandardProtectedRoomsSet implements ProtectedRoomsSet {
"revision",
this.setMembershipPolicyRevisionListener
);
this.projectionLocator = makeProtectedRoomsSetProjectionLocator(this);
}
public get setRoomState() {
return this.protectedRoomsManager.setRoomState;
@@ -5,9 +5,9 @@
import { Ok } from "@gnuxie/typescript-result";
import {
DisposableProjection,
ProjectionLocator,
StandardProjectionLocator,
} from "../Projection/ProjectionLocator";
ProjectionAllocator,
StandardProjectionAllocator,
} from "../Projection/ProjectionAllocator";
import {
MemberBanIntentProjection,
StandardMemberBanIntentProjection,
@@ -20,28 +20,33 @@ import {
import { ServerBanIntentProjectionDescription } from "./StandardProtections/ServerBanSynchronisation/ServerBanIntentProjectionNode";
import type { ProtectedRoomsSet } from "./ProtectedRoomsSet";
export function makeProtectedRoomsSetProjectionLocator(
protectedRoomsSet: ProtectedRoomsSet
): ProjectionLocator<ProtectedRoomsSet> {
return new StandardProjectionLocator<ProtectedRoomsSet>(protectedRoomsSet)
.registerProjection<DisposableProjection<MemberBanIntentProjection>>({
projectionDescription: MemberBanIntentProjectionDescription,
factory(context) {
/**
* This is used at the top level of the appservice and Draupnir to provide instances
* of projections that depend on context from a protected rooms set to construct.
* The projections are still owned by the protected rooms set that wants them.
* This is just something that we have to deal with until we port the entire
* data pipeline over unfortunately.
*/
export function makeProtectedRoomsSetProjectionAllocator(): ProjectionAllocator<ProtectedRoomsSet> {
return new StandardProjectionAllocator<ProtectedRoomsSet>()
.registerProjection<DisposableProjection<MemberBanIntentProjection>>(
MemberBanIntentProjectionDescription,
(context) => {
return Ok(
new StandardMemberBanIntentProjection(
context.setPoliciesMatchingMembership
)
);
},
})
.registerProjection<DisposableProjection<ServerBanIntentProjection>>({
projectionDescription: ServerBanIntentProjectionDescription,
factory(context) {
}
)
.registerProjection<DisposableProjection<ServerBanIntentProjection>>(
ServerBanIntentProjectionDescription,
(context) => {
return Ok(
new StandardServerBanIntentProjection(
context.watchedPolicyRooms.revisionIssuer
)
);
},
});
}
);
}
@@ -42,7 +42,7 @@ import {
import { OwnLifetime } from "../../../Interface/Lifetime";
import { MemberBanIntentProjection } from "./MemberBanIntentProjection";
import { MemberBanIntentProjectionDescription } from "./MemberBanIntentProjectionNode";
import { DisposableProjection } from "../../../Projection/ProjectionLocator";
import { DisposableProjection } from "../../../Projection/ProjectionAllocator";
function isRecommendationWorthBanning(policyRule: PolicyRule) {
return (
@@ -203,16 +203,13 @@ describeProtection<MemberBanSynchronisationProtectionCapabilities>({
_settings,
capabilitySet
) => {
const intentProjection = protectedRoomsSet.projectionLocator.locate<
const intentProjection = protectedRoomsSet.projectionAllocator.allocate<
DisposableProjection<MemberBanIntentProjection>
>({
description: MemberBanIntentProjectionDescription,
partition: {
draupnirID: protectedRoomsSet.userID,
},
>(lifetime, protectedRoomsSet, MemberBanIntentProjectionDescription, {
draupnirID: protectedRoomsSet.userID,
});
if (isError(intentProjection)) {
return intentProjection.elaborate("Unable to locate intent projection");
return intentProjection.elaborate("Unable to allocate intent projection");
}
return Ok(
new MemberBanSynchronisationProtection(
@@ -30,7 +30,7 @@ import { ServerBanIntentProjection } from "./ServerBanIntentProjection";
import { ServerBanIntentProjectionDescription } from "./ServerBanIntentProjectionNode";
import { ServerBanSynchronisationCapability } from "./ServerBanSynchronisationCapability";
import { Logger } from "../../../Logging/Logger";
import { DisposableProjection } from "../../../Projection/ProjectionLocator";
import { DisposableProjection } from "../../../Projection/ProjectionAllocator";
const log = new Logger("ServerBanSynchronisationProtection");
@@ -125,18 +125,13 @@ describeProtection<Capabilities>({
_settings,
capabilities
) => {
const intentProjection = protectedRoomsSet.projectionLocator.locate<
const intentProjection = protectedRoomsSet.projectionAllocator.allocate<
DisposableProjection<ServerBanIntentProjection>
>({
description: ServerBanIntentProjectionDescription,
partition: {
draupnirID: protectedRoomsSet.userID,
},
>(lifetime, protectedRoomsSet, ServerBanIntentProjectionDescription, {
draupnirID: protectedRoomsSet.userID,
});
if (isError(intentProjection)) {
return intentProjection.elaborate(
"Unable to locate the intent projection"
);
return intentProjection.elaborate("Unable to allocate intent projection");
}
return Ok(
new ServerBanSynchronisationProtection(
@@ -43,6 +43,8 @@ import { StandardWatchedPolicyRooms } from "../Protection/WatchedPolicyRooms/Sta
import { DefaultEventDecoder } from "../MatrixTypes/DefaultEventDecoder";
import { DefaultMixinExtractor } from "../SafeMatrixEvents/MatrixEventMixinDescriptions/DefaultMixinExtractor";
import { RoomCreateEvent } from "../MatrixTypes/CreateRoom";
import { ProjectionAllocator } from "../Projection/ProjectionAllocator";
import { makeProtectedRoomsSetProjectionAllocator } from "../Protection/ProtectedRoomsSetProjectionAllocator";
const log = new Logger("DeclareRoomState");
@@ -56,6 +58,7 @@ export type DescribeProtectedRoomsSet = {
rooms?: DescribeRoomOptions[];
lists?: DescribeRoomOptions[];
clientUserID?: StringUserID;
projectionAllocator?: ProjectionAllocator<ProtectedRoomsSet>;
};
export type ProtectedRoomsSetDescription = {
@@ -69,6 +72,7 @@ export async function describeProtectedRoomsSet({
rooms = [],
lists = [],
clientUserID = randomUserID(),
projectionAllocator = makeProtectedRoomsSetProjectionAllocator(),
}: DescribeProtectedRoomsSet): Promise<ProtectedRoomsSetDescription> {
const listDescriptions = lists.map(describeRoom);
const roomDescriptions = [...listDescriptions, ...rooms.map(describeRoom)];
@@ -139,7 +143,8 @@ export async function describeProtectedRoomsSet({
protectedRoomsManager.ok,
new FakeProtectionsManager(),
clientUserID,
DefaultMixinExtractor
DefaultMixinExtractor,
projectionAllocator
);
return {
protectedRoomsSet,
@@ -119,7 +119,7 @@ export * from "./PolicyList/StandardPolicyRoomRevisionIssuer";
export * from "./Projection/Projection";
export * from "./Projection/ProjectionDescription";
export * from "./Projection/ProjectionLocator";
export * from "./Projection/ProjectionAllocator";
export * from "./Projection/ProjectionNode";
export * from "./Protection/Capability/StandardCapability/CapabilityMethodSchema";
@@ -192,6 +192,7 @@ export * from "./Protection/DescriptionMeta";
export * from "./Protection/DirectPropagationPolicyListRevisionIssuer";
export * from "./Protection/ProtectedRoomsSet";
export * from "./Protection/ProtectedRoomsSetProjectionAllocator";
export * from "./Protection/Protection";
export * from "./Protection/ProtectionHandles";