mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Create a GenericEventEmitter interface
This allows us to have typed EventEmitters.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
[
|
||||
"AccessMap",
|
||||
"Adapter",
|
||||
"BaseActivityEmitter",
|
||||
"BaseHttpError",
|
||||
"BasicConditions",
|
||||
"BasicRepresentation",
|
||||
@@ -9,6 +10,7 @@
|
||||
"Dict",
|
||||
"Error",
|
||||
"EventEmitter",
|
||||
"GenericEventEmitter",
|
||||
"HashMap",
|
||||
"HttpErrorOptions",
|
||||
"HttpResponse",
|
||||
|
||||
@@ -300,6 +300,9 @@ export * from './server/middleware/HeaderHandler';
|
||||
export * from './server/middleware/StaticAssetHandler';
|
||||
export * from './server/middleware/WebSocketAdvertiser';
|
||||
|
||||
// Server/Notifications
|
||||
export * from './server/notifications/ActivityEmitter';
|
||||
|
||||
// Server/Util
|
||||
export * from './server/util/RedirectingHttpHandler';
|
||||
export * from './server/util/RouterHandler';
|
||||
@@ -470,6 +473,7 @@ export * from './util/templates/TemplateUtil';
|
||||
// Util
|
||||
export * from './util/ContentTypes';
|
||||
export * from './util/FetchUtil';
|
||||
export * from './util/GenericEventEmitter';
|
||||
export * from './util/GuardedStream';
|
||||
export * from './util/HeaderUtil';
|
||||
export * from './util/IterableUtil';
|
||||
|
||||
18
src/server/notifications/ActivityEmitter.ts
Normal file
18
src/server/notifications/ActivityEmitter.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { ResourceIdentifier } from '../../http/representation/ResourceIdentifier';
|
||||
import type { GenericEventEmitter } from '../../util/GenericEventEmitter';
|
||||
import { createGenericEventEmitterClass } from '../../util/GenericEventEmitter';
|
||||
import type { AS, VocabularyTerm, VocabularyValue } from '../../util/Vocabularies';
|
||||
|
||||
/**
|
||||
* An event emitter used to report changes made to resources.
|
||||
* Both generic `change` events and ActivityStream-specific events are emitted.
|
||||
*/
|
||||
export type ActivityEmitter =
|
||||
GenericEventEmitter<'changed', (target: ResourceIdentifier, activity: VocabularyTerm<typeof AS>) => void> &
|
||||
GenericEventEmitter<VocabularyValue<typeof AS>, (target: ResourceIdentifier) => void>;
|
||||
|
||||
/**
|
||||
* A class implementation of {@link ActivityEmitter}.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export const BaseActivityEmitter = createGenericEventEmitterClass<ActivityEmitter>();
|
||||
@@ -1,21 +1,22 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type { Term } from '@rdfjs/types';
|
||||
import type { Patch } from '../http/representation/Patch';
|
||||
import type { Representation } from '../http/representation/Representation';
|
||||
import type { RepresentationPreferences } from '../http/representation/RepresentationPreferences';
|
||||
import type { ResourceIdentifier } from '../http/representation/ResourceIdentifier';
|
||||
import { BaseActivityEmitter } from '../server/notifications/ActivityEmitter';
|
||||
import { AS, SOLID_AS } from '../util/Vocabularies';
|
||||
import type { Conditions } from './Conditions';
|
||||
import type { ResourceStore, ChangeMap } from './ResourceStore';
|
||||
|
||||
// The ActivityStream terms for which we emit an event
|
||||
const emittedActivities: Set<string> = new Set([ AS.Create, AS.Delete, AS.Update ]);
|
||||
const knownActivities = [ AS.terms.Create, AS.terms.Delete, AS.terms.Update ];
|
||||
|
||||
/**
|
||||
* Store that notifies listeners of changes to its source
|
||||
* by emitting a `changed` event.
|
||||
*/
|
||||
export class MonitoringStore<T extends ResourceStore = ResourceStore>
|
||||
extends EventEmitter implements ResourceStore {
|
||||
extends BaseActivityEmitter implements ResourceStore {
|
||||
private readonly source: T;
|
||||
|
||||
public constructor(source: T) {
|
||||
@@ -55,12 +56,16 @@ export class MonitoringStore<T extends ResourceStore = ResourceStore>
|
||||
private emitChanged(changes: ChangeMap): ChangeMap {
|
||||
for (const [ identifier, metadata ] of changes) {
|
||||
const activity = metadata.get(SOLID_AS.terms.Activity);
|
||||
this.emit('changed', identifier, activity);
|
||||
if (activity && emittedActivities.has(activity.value)) {
|
||||
if (this.isKnownActivity(activity)) {
|
||||
this.emit('changed', identifier, activity);
|
||||
this.emit(activity.value, identifier);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private isKnownActivity(term?: Term): term is typeof knownActivities[number] {
|
||||
return Boolean(term && knownActivities.some((entry): boolean => entry.equals(term)));
|
||||
}
|
||||
}
|
||||
|
||||
36
src/util/GenericEventEmitter.ts
Normal file
36
src/util/GenericEventEmitter.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
/**
|
||||
* A typed interface of {@link EventEmitter}.
|
||||
*
|
||||
* Use the `&` operator to combine multiple event/function pairs into a single event emitter.
|
||||
* The result needs to be a type and not an interface because of https://github.com/microsoft/TypeScript/issues/16936.
|
||||
*
|
||||
* Use the {@link createGenericEventEmitterClass} function to generate an event emitter class with the correct typings
|
||||
* in case {@link EventEmitter} needs to be extended.
|
||||
*/
|
||||
export interface GenericEventEmitter<TEvent extends string | symbol, TFunc extends (...args: any[]) => void>
|
||||
extends EventEmitter {
|
||||
addListener: (event: TEvent, listener: TFunc) => this;
|
||||
on: (event: TEvent, listener: TFunc) => this;
|
||||
once: (event: TEvent, listener: TFunc) => this;
|
||||
removeListener: (event: TEvent, listener: TFunc) => this;
|
||||
off: (event: TEvent, listener: TFunc) => this;
|
||||
removeAllListeners: (event: TEvent) => this;
|
||||
listeners: (event: TEvent) => TFunc[];
|
||||
rawListeners: (event: TEvent) => TFunc[];
|
||||
emit: (event: TEvent, ...args: Parameters<TFunc>) => boolean;
|
||||
listenerCount: (event: TEvent) => number;
|
||||
prependListener: (event: TEvent, listener: TFunc) => this;
|
||||
prependOnceListener: (event: TEvent, listener: TFunc) => this;
|
||||
eventNames: () => TEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a class that is an implementation of {@link EventEmitter}
|
||||
* but with specific typings based on {@link GenericEventEmitter}.
|
||||
* Useful in case a class needs to extend {@link EventEmitter} and wants specific internal typings.
|
||||
*/
|
||||
export function createGenericEventEmitterClass<T extends EventEmitter>(): (new() => T) {
|
||||
return EventEmitter as unknown as new() => T;
|
||||
}
|
||||
@@ -155,14 +155,14 @@ describe('A MonitoringStore', (): void => {
|
||||
expect(source.hasResource).toHaveBeenLastCalledWith({ path: 'http://example.org/foo/bar' });
|
||||
});
|
||||
|
||||
it('should not emit an extra event when the Activity is not a valid AS value.', async(): Promise<void> => {
|
||||
it('should not emit an event when the Activity is not a valid AS value.', async(): Promise<void> => {
|
||||
source.addResource = jest.fn().mockResolvedValue(new IdentifierMap([
|
||||
[{ path: 'http://example.org/path' }, new RepresentationMetadata({ [SOLID_AS.Activity]: 'SomethingRandom' }) ],
|
||||
]));
|
||||
|
||||
await store.addResource({ path: 'http://example.org/foo/bar' }, {} as Patch);
|
||||
|
||||
expect(changedCallback).toHaveBeenCalledTimes(1);
|
||||
expect(changedCallback).toHaveBeenCalledTimes(0);
|
||||
expect(createdCallback).toHaveBeenCalledTimes(0);
|
||||
expect(updatedCallback).toHaveBeenCalledTimes(0);
|
||||
expect(deletedCallback).toHaveBeenCalledTimes(0);
|
||||
|
||||
Reference in New Issue
Block a user