Skip to content

Commit

Permalink
Events and event classes cannot be state
Browse files Browse the repository at this point in the history
  • Loading branch information
soxtoby committed Mar 24, 2024
1 parent a986486 commit cc92159
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 14 deletions.
13 changes: 13 additions & 0 deletions packages/event-reduce/src/derivation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { EventFn, IEventClass, isEvent } from "./events";
import { log, sourceTree } from "./logging";
import { isEventsClass } from "./models";
import { IObservable } from "./observable";
import { IObservableValue, ObservableValue, collectAccessedValues, withInnerTrackingScope } from "./observableValue";
import { Unsubscribe } from "./types";
Expand Down Expand Up @@ -87,6 +89,10 @@ export class Derivation<T> extends ObservableValue<T> implements IObservableValu
currentlyRunningDerivation = previouslyRunningDerivation;
}
});

if (isEvent(value) || isEventsClass(value))
throw new DerivedEventsError(this, value);

for (let source of newSources)
this._sources.set(source, this.subscribeTo(source));
this._sourceVersion = Math.max(0, ...this.sources.map(s => s.version));
Expand Down Expand Up @@ -152,4 +158,11 @@ export class SideEffectInDerivationError extends Error {
public derivation: IObservableValue<unknown>,
public sideEffect: string
) { super(`Derivation ${derivation.displayName} triggered side effect ${sideEffect}. Derivations cannot have side effects.`); }
}

export class DerivedEventsError extends Error {
constructor(
public derivation: IObservableValue<unknown>,
public value: EventFn<IEventClass> | object
) { super(`Derivation ${derivation.displayName} returned event or events class ${isEvent(value) ? value.displayName : value}. Events cannot be state.`); }
}
11 changes: 10 additions & 1 deletion packages/event-reduce/src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,18 +164,26 @@ class ScopedAsyncEvent<Result, ContextIn extends Scope, ContextOut extends Scope
/** Converts an event class into a callable function */
export function makeEventFunction<Event extends IEventClass>(event: Event) {
Object.setPrototypeOf(eventFn, event);
eventFn[eventBrand] = true;
eventFn.apply = Function.prototype.apply;
Object.defineProperty(eventFn, 'displayName', { // Delegate to prototype, since already initialised Subjects' displayNames will have captured the prototype as 'this'
get() { return event.displayName; },
set(value: string) { event.displayName = value; }
});
return eventFn as Event & Event['next'];
return eventFn as EventFn<Event>;

function eventFn(...args: any) {
return event.next.apply(eventFn, args);
}
}

export function isEvent(event: unknown): event is EventFn<IEventClass> {
return typeof event == 'function'
&& eventBrand in event;
}

export type EventFn<Event extends IEventClass> = Event & Event['next'] & { [eventBrand]: true };

/** Logs event and ensures no other events are run at the same time. */
export function fireEvent(type: string, displayName: string, arg: any, getInfo: (() => object) | undefined, runEvent: () => void) {
logEvent(type, displayName, arg, getInfo, () => {
Expand All @@ -195,6 +203,7 @@ export function fireEvent(type: string, displayName: string, arg: any, getInfo:

let currentlyFiringEvent = null as string | null;
const anonymousEvent = '(anonymous event)';
const eventBrand = Symbol('IsEvent');

export class ChainedEventsError extends Error {
constructor(
Expand Down
36 changes: 26 additions & 10 deletions packages/event-reduce/src/models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { changeOwnedValue } from "./cleanup";
import { Derivation, derive } from "./derivation";
import { IEventBase } from "./events";
import { isEvent } from "./events";
import { ObservableValue, ValueIsNotObservableError, getUnderlyingObservable, startTrackingScope } from "./observableValue";
import { Reduction, reduce } from "./reduction";
import { StringKey } from "./types";
Expand Down Expand Up @@ -29,10 +29,14 @@ export function model<T extends { new(...args: any[]): any }>(target: T): T {
}

for (let value of Object.values(getObservableValues(this)))
changeOwnedValue(this, undefined, value)
changeOwnedValue(this, undefined, value);

for (let key of getStateProperties(this))
for (let key of getStateProperties(this)) {
let value = this[key];
if (isEvent(value) || isEventsClass(value))
throw new EventsMarkedAsStateError(this, key);
changeOwnedValue(this, undefined, this[key]);
}
}
}
}[className];
Expand All @@ -51,7 +55,7 @@ export function derived(targetOrValuesEqual: Object | ((previous: any, next: any

return valuesEqual
? decorate as PropertyDecorator
: decorate(targetOrValuesEqual, key!) as any
: decorate(targetOrValuesEqual, key!) as any

function decorate(target: Object, key: string | symbol) {
let property = Object.getOwnPropertyDescriptor(target, key)!;
Expand Down Expand Up @@ -186,33 +190,45 @@ export let events = <T extends { new(...args: any[]): any }>(target: T): T => {
super(...args);
Object.keys(this).forEach(key => {
let prop = this[key];
if (hasDisplayName(prop)) {
if (isEvent(prop)) {
prop.displayName = key;
prop.container = this;
}
});
(this as any)[eventsClassBrand] = true;
}
}
}[className];
}

function hasDisplayName(e: any): e is IEventBase {
return e instanceof Object
&& 'displayName' in e;
export function isEventsClass(value: unknown): value is Object {
return value instanceof Object
&& eventsClassBrand in value;
}

const eventsClassBrand = Symbol('IsEventsClass');

export class EventsMarkedAsStateError extends Error {
constructor(
public model: object,
public property: string | symbol
) {
super(`Model property ${String(property)} returns an event or events class, and cannot be marked as @state.`);
}
}

export class InvalidReducedPropertyError extends Error {
constructor(
public property: string | symbol
) {
super(`@reduced property '${String(property)}' can only be set to the value of a reduction`);
super(`@reduced property '${String(property)}' can only be set to the value of a reduction.`);
}
}

export class InvalidDerivedPropertyError extends Error {
constructor(
public property: string | symbol
) {
super(`@derived property ${String(property)} must have a getter or be set to the value of a derivation`);
super(`@derived property ${String(property)} must have a getter or be set to the value of a derivation.`);
}
}
28 changes: 26 additions & 2 deletions tests/DerivationTests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { derive, event } from "event-reduce";
import { SideEffectInDerivationError } from "event-reduce/lib/derivation";
import { derive, event, events } from "event-reduce";
import { DerivedEventsError, SideEffectInDerivationError } from "event-reduce/lib/derivation";
import { consumeLastAccessed, ObservableValue } from "event-reduce/lib/observableValue";
import { spy, stub } from "sinon";
import { describe, it, then, when } from "wattle";
Expand Down Expand Up @@ -75,4 +75,28 @@ describe(derive.name, () => {
err.has.property('sideEffect', 'some event');
});
});

when("derivation returns an event", () => {
let eventValue = event('derived event');
let sut = derive(() => sourceA.value && eventValue);

then("accessing value throws", () => {
let err = (() => sut.value).should.throw(DerivedEventsError);
err.has.property('derivation', sut);
err.has.property('value', eventValue);
});
});

when("derivation returns an events class", () => {
@events
class Events { }
let eventsValue = new Events();
let sut = derive(() => sourceA.value && eventsValue);

then("accesing value throws", () => {
let err = (() => sut.value).should.throw(DerivedEventsError);
err.has.property('derivation', sut);
err.has.property('value', eventsValue);
});
});
});
31 changes: 30 additions & 1 deletion tests/ModelTests.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { asyncEvent, derive, derived, event, events, extend, model, reduce, reduced } from "event-reduce";
import { asyncEvent, derive, derived, event, events, extend, model, reduce, reduced, state } from "event-reduce";
import { EventsMarkedAsStateError } from "event-reduce/lib/models";
import { AccessedValueWithCommonSourceError, valueChanged } from "event-reduce/lib/observableValue";
import { describe, it, test, then, when } from "wattle";

Expand Down Expand Up @@ -108,6 +109,20 @@ describe("models", function () {

it("throws", () => increment.should.throw(AccessedValueWithCommonSourceError));
});

when("event is marked as state", () => {
@model
class BadModel {
@state
event = event('bad state');
}

it("throws when constructed", () => {
let err = (() => new BadModel()).should.throw(EventsMarkedAsStateError);
err.has.property('model');
err.has.property('property', 'event');
});
});
});

describe("events decorator", function () {
Expand All @@ -127,4 +142,18 @@ describe("events decorator", function () {
});

it("sets event container", () => (sut.promiseEvent as any).container.should.equal(sut));

when("event class is marked as state", () => {
@model
class BadModel {
@state
events = new TestEvents();
}

it("throws when constructed", () => {
let err = (() => new BadModel()).should.throw(EventsMarkedAsStateError);
err.has.property('model');
err.has.property('property', 'events');
});
})
});

0 comments on commit cc92159

Please sign in to comment.