diff --git a/apps/draupnir/src/DraupnirBotMode.ts b/apps/draupnir/src/DraupnirBotMode.ts index 5761e7bc..204216a6 100644 --- a/apps/draupnir/src/DraupnirBotMode.ts +++ b/apps/draupnir/src/DraupnirBotMode.ts @@ -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, diff --git a/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts b/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts index 2be87123..c4888901 100644 --- a/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts +++ b/apps/draupnir/src/appservice/AppServiceDraupnirManager.ts @@ -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); } diff --git a/apps/draupnir/src/draupnirfactory/DraupnirFactory.ts b/apps/draupnir/src/draupnirfactory/DraupnirFactory.ts index f75f464f..7abd6bc7 100644 --- a/apps/draupnir/src/draupnirfactory/DraupnirFactory.ts +++ b/apps/draupnir/src/draupnirfactory/DraupnirFactory.ts @@ -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 ) { // nothing to do. } @@ -102,6 +105,7 @@ export class DraupnirFactory { client, clientPlatform, clientUserID, + this.projectionAllocator, config, configLogTracker ); diff --git a/apps/draupnir/src/draupnirfactory/DraupnirProtectedRoomsSet.ts b/apps/draupnir/src/draupnirfactory/DraupnirProtectedRoomsSet.ts index c885457b..f55388f2 100644 --- a/apps/draupnir/src/draupnirfactory/DraupnirProtectedRoomsSet.ts +++ b/apps/draupnir/src/draupnirfactory/DraupnirProtectedRoomsSet.ts @@ -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, config: IConfig, loggableConfigTracker: LoggableConfigTracker ): Promise> { @@ -226,6 +228,7 @@ export async function makeProtectedRoomsSet( protectionsConfig.ok, userID, DefaultMixinExtractor, + projectionAllocator, makeHandleMissingProtectionPermissions( clientPlatform.toRoomMessageSender(), managementRoom.toRoomIDOrAlias() diff --git a/packages/matrix-protection-suite/src/Projection/ProjectionAllocator.test.ts b/packages/matrix-protection-suite/src/Projection/ProjectionAllocator.test.ts new file mode 100644 index 00000000..636b9b3c --- /dev/null +++ b/packages/matrix-protection-suite/src/Projection/ProjectionAllocator.test.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Gnuxie +// +// 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()); + }); +}); diff --git a/packages/matrix-protection-suite/src/Projection/ProjectionAllocator.ts b/packages/matrix-protection-suite/src/Projection/ProjectionAllocator.ts new file mode 100644 index 00000000..c4eff0a6 --- /dev/null +++ b/packages/matrix-protection-suite/src/Projection/ProjectionAllocator.ts @@ -0,0 +1,414 @@ +// SPDX-FileCopyrightText: 2026 Gnuxie +// +// 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 & Disposable; + +export type ProjectionDescriptionFor = + ExtractProjectionNode extends ProjectionNode + ? 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 { + registerProjection( + description: ProjectionDescriptionFor, + factory: ( + context: Context, + partition: ProjectionPartition> + ) => Result + ): this; + + allocate( + lifetime: AllocatableLifetime, + context: Context, + description: ProjectionDescriptionFor, + partition: ProjectionPartition> + ): Result; +} + +export const ProjectionAllocator = SemanticType( + "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> + >; + const projection = { + [Symbol.dispose]() { + // nothing to dispose. + }, + } as TestProjection; + const context = { source: "semantic context" }; + allocator.registerProjection( + 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( + 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> + >; + let disposalCount = 0; + allocator.registerProjection(description, () => { + return Ok({ + [Symbol.dispose]() { + disposalCount += 1; + }, + } as TestProjection); + }); + { + await using lifetime = new StandardLifetime(); + allocator + .allocate(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> + >; + let allocationCount = 0; + allocator.registerProjection(description, () => { + allocationCount += 1; + return Ok({ [Symbol.dispose]() {} } as TestProjection); + }); + await using lifetime = new StandardLifetime(); + const first = allocator + .allocate(lifetime, {}, description, {}) + .expect("First projection allocation should succeed"); + const second = allocator + .allocate(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> + >; + let factoryInvocations = 0; + allocator.registerProjection(description, () => { + factoryInvocations += 1; + return Ok({ [Symbol.dispose]() {} } as TestProjection); + }); + const lifetime = new StandardLifetime(); + await lifetime[Symbol.asyncDispose](); + const result = allocator.allocate( + 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> + >; + allocator.registerProjection(description, () => + Ok({ [Symbol.dispose]() {} } as TestProjection) + ); + await using lifetime = new StandardLifetime(); + const result = allocator.allocate( + 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> + >; + const factoryError = ResultError.Result("Projection creation failed"); + allocator.registerProjection( + description, + () => factoryError + ); + await using lifetime = new StandardLifetime(); + const result = allocator.allocate( + 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> + >; + await using lifetime = new StandardLifetime(); + const result = allocator.allocate( + 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> + >; + await using lifetime = new StandardLifetime(); + const result = allocator.allocate( + 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> + >; + await using lifetime = new StandardLifetime(); + const result = allocator.allocate( + lifetime, + {}, + description, + { + roomID: "!room:example.org", + unknownKey: "value", + } as never + ); + expect(isError(result)).toBe(true); + }, + }); + +type AnyProjectionFactory = ( + context: Context, + partition: ProjectionPartition +) => Result; + +function validateProjectionPartition( + description: AnyProjectionDescription, + partition: ProjectionPartition +): Result { + 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 { + private readonly factories = new Map< + AnyProjectionDescription, + AnyProjectionFactory + >(); + private readonly registeredNames = new Set(); + + public registerProjection( + description: ProjectionDescriptionFor, + factory: ( + context: Context, + partition: ProjectionPartition> + ) => Result + ): 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 + ); + return this; + } + + public allocate( + lifetime: AllocatableLifetime, + context: Context, + description: ProjectionDescriptionFor, + partition: ProjectionPartition> + ): Result { + 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; + }); + } +} diff --git a/packages/matrix-protection-suite/src/Projection/ProjectionLocator.test.ts b/packages/matrix-protection-suite/src/Projection/ProjectionLocator.test.ts deleted file mode 100644 index 4a315d02..00000000 --- a/packages/matrix-protection-suite/src/Projection/ProjectionLocator.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Gnuxie -// -// 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; -type TestProjection = Projection; - -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({ - projectionDescription: TestProjectionDescription, - factory(providerContext, location) { - expect(providerContext).toBe(context); - expect(location.partition.roomID).toBe("!room:example.org"); - return Ok(projection); - }, - }); - const result = locator.locate({ - description: TestProjectionDescription, - partition: { - roomID: "!room:example.org", - }, - }); - expect(result.expect("Projection location should succeed")).toBe(projection); -}); diff --git a/packages/matrix-protection-suite/src/Projection/ProjectionLocator.ts b/packages/matrix-protection-suite/src/Projection/ProjectionLocator.ts deleted file mode 100644 index d3a3deec..00000000 --- a/packages/matrix-protection-suite/src/Projection/ProjectionLocator.ts +++ /dev/null @@ -1,240 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Gnuxie -// -// 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 & Disposable; - -export type ProjectionDescriptionFor = - ExtractProjectionNode extends ProjectionNode - ? TDescription - : never; - -export type ProjectionLocation< - TProjectionDescription extends AnyProjectionDescription = - AnyProjectionDescription, -> = { - readonly description: TProjectionDescription; - readonly partition: ProjectionPartition; -}; - -export type ProjectionProviderDescription< - Context = unknown, - TProjectionDescription extends AnyProjectionDescription = - AnyProjectionDescription, - TProjection extends Projection = Projection, -> = { - readonly projectionDescription: TProjectionDescription; - factory( - context: Context, - location: ProjectionLocation - ): Result; -}; - -// 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 { - registerProjection( - provider: ProjectionProviderDescription< - Context, - ProjectionDescriptionFor, - TProjection - > - ): this; - - locate( - location: ProjectionLocation> - ): Result; -} - -export const ProjectionLocator = SemanticType( - "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>; - const projection = {} as TestProjection; - locator.registerProjection({ - projectionDescription: description, - factory(_context, location) { - expect(location.partition.roomID).toBe("!room:example.org"); - return Ok(projection); - }, - }); - const result = locator.locate({ - 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>; - const result = locator.locate({ - 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>; - const result = locator.locate({ - 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>; - const result = locator.locate({ - description, - partition: { - roomID: "!room:example.org", - unknownKey: "value", - }, - } as never); - expect(isError(result)).toBe(true); - }, - }); - -type AnyProjectionProviderDescription = - ProjectionProviderDescription; - -function validateProjectionLocation( - location: ProjectionLocation -): Result { - 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 { - private readonly providers = new Map< - string, - AnyProjectionProviderDescription - >(); - - public constructor( - private readonly context: Context, - providers: readonly AnyProjectionProviderDescription[] = [] - ) { - for (const provider of providers) { - this.registerProjection(provider); - } - } - - public registerProjection( - provider: ProjectionProviderDescription< - Context, - ProjectionDescriptionFor, - 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 - ); - return this; - } - - public locate( - location: ProjectionLocation> - ): Result { - 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; - } -} diff --git a/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSet.ts b/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSet.ts index 9c4a2d11..a92ab631 100644 --- a/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSet.ts +++ b/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSet.ts @@ -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; + readonly projectionAllocator: ProjectionAllocator; 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; 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, 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; diff --git a/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSetProjectionLocator.ts b/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSetProjectionAllocator.ts similarity index 60% rename from packages/matrix-protection-suite/src/Protection/ProtectedRoomsSetProjectionLocator.ts rename to packages/matrix-protection-suite/src/Protection/ProtectedRoomsSetProjectionAllocator.ts index e128c038..00d5636e 100644 --- a/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSetProjectionLocator.ts +++ b/packages/matrix-protection-suite/src/Protection/ProtectedRoomsSetProjectionAllocator.ts @@ -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 { - return new StandardProjectionLocator(protectedRoomsSet) - .registerProjection>({ - 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 { + return new StandardProjectionAllocator() + .registerProjection>( + MemberBanIntentProjectionDescription, + (context) => { return Ok( new StandardMemberBanIntentProjection( context.setPoliciesMatchingMembership ) ); - }, - }) - .registerProjection>({ - projectionDescription: ServerBanIntentProjectionDescription, - factory(context) { + } + ) + .registerProjection>( + ServerBanIntentProjectionDescription, + (context) => { return Ok( new StandardServerBanIntentProjection( context.watchedPolicyRooms.revisionIssuer ) ); - }, - }); + } + ); } diff --git a/packages/matrix-protection-suite/src/Protection/StandardProtections/MemberBanSynchronisation/MemberBanSynchronisation.ts b/packages/matrix-protection-suite/src/Protection/StandardProtections/MemberBanSynchronisation/MemberBanSynchronisation.ts index bc4f2cab..d0c4f82e 100644 --- a/packages/matrix-protection-suite/src/Protection/StandardProtections/MemberBanSynchronisation/MemberBanSynchronisation.ts +++ b/packages/matrix-protection-suite/src/Protection/StandardProtections/MemberBanSynchronisation/MemberBanSynchronisation.ts @@ -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({ _settings, capabilitySet ) => { - const intentProjection = protectedRoomsSet.projectionLocator.locate< + const intentProjection = protectedRoomsSet.projectionAllocator.allocate< DisposableProjection - >({ - 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( diff --git a/packages/matrix-protection-suite/src/Protection/StandardProtections/ServerBanSynchronisation/ServerBanSynchronisation.ts b/packages/matrix-protection-suite/src/Protection/StandardProtections/ServerBanSynchronisation/ServerBanSynchronisation.ts index 09a80cfb..f9e07aec 100644 --- a/packages/matrix-protection-suite/src/Protection/StandardProtections/ServerBanSynchronisation/ServerBanSynchronisation.ts +++ b/packages/matrix-protection-suite/src/Protection/StandardProtections/ServerBanSynchronisation/ServerBanSynchronisation.ts @@ -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({ _settings, capabilities ) => { - const intentProjection = protectedRoomsSet.projectionLocator.locate< + const intentProjection = protectedRoomsSet.projectionAllocator.allocate< DisposableProjection - >({ - 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( diff --git a/packages/matrix-protection-suite/src/StateTracking/DeclareRoomState.ts b/packages/matrix-protection-suite/src/StateTracking/DeclareRoomState.ts index e5abc669..9785a4fa 100644 --- a/packages/matrix-protection-suite/src/StateTracking/DeclareRoomState.ts +++ b/packages/matrix-protection-suite/src/StateTracking/DeclareRoomState.ts @@ -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; }; export type ProtectedRoomsSetDescription = { @@ -69,6 +72,7 @@ export async function describeProtectedRoomsSet({ rooms = [], lists = [], clientUserID = randomUserID(), + projectionAllocator = makeProtectedRoomsSetProjectionAllocator(), }: DescribeProtectedRoomsSet): Promise { 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, diff --git a/packages/matrix-protection-suite/src/index.ts b/packages/matrix-protection-suite/src/index.ts index 2b528cfb..21ecb0b9 100644 --- a/packages/matrix-protection-suite/src/index.ts +++ b/packages/matrix-protection-suite/src/index.ts @@ -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";