-
-
Notifications
You must be signed in to change notification settings - Fork 135
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TASK: Add package
@neos-project/framework-observable
This introduces a very basic implementation of the observable pattern for the Neos UI. This will allow us to replace redux opportunistically in various places.
- Loading branch information
Showing
10 changed files
with
358 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"name": "@neos-project/framework-observable", | ||
"version": "", | ||
"description": "Observable pattern implementation for the Neos UI", | ||
"private": true, | ||
"main": "./src/index.ts", | ||
"license": "GNU GPLv3" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
/* | ||
* This file is part of the Neos.Neos.Ui package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
import {createObservable} from './Observable'; | ||
|
||
describe('Observable', () => { | ||
test('emit some values and subscribe', () => { | ||
const observable$ = createObservable((next) => { | ||
next(1); | ||
next(2); | ||
next(3); | ||
}); | ||
const subscriber = { | ||
next: jest.fn() | ||
}; | ||
|
||
observable$.subscribe(subscriber); | ||
|
||
expect(subscriber.next).toHaveBeenCalledTimes(3); | ||
expect(subscriber.next).toHaveBeenNthCalledWith(1, 1); | ||
expect(subscriber.next).toHaveBeenNthCalledWith(2, 2); | ||
expect(subscriber.next).toHaveBeenNthCalledWith(3, 3); | ||
}); | ||
|
||
test('emit some values and subscribe a couple of times', () => { | ||
const observable$ = createObservable((next) => { | ||
next(1); | ||
next(2); | ||
next(3); | ||
}); | ||
const subscriber1 = { | ||
next: jest.fn() | ||
}; | ||
const subscriber2 = { | ||
next: jest.fn() | ||
}; | ||
const subscriber3 = { | ||
next: jest.fn() | ||
}; | ||
|
||
observable$.subscribe(subscriber1); | ||
observable$.subscribe(subscriber2); | ||
observable$.subscribe(subscriber3); | ||
|
||
expect(subscriber1.next).toHaveBeenCalledTimes(3); | ||
expect(subscriber1.next).toHaveBeenNthCalledWith(1, 1); | ||
expect(subscriber1.next).toHaveBeenNthCalledWith(2, 2); | ||
expect(subscriber1.next).toHaveBeenNthCalledWith(3, 3); | ||
|
||
expect(subscriber2.next).toHaveBeenCalledTimes(3); | ||
expect(subscriber2.next).toHaveBeenNthCalledWith(1, 1); | ||
expect(subscriber2.next).toHaveBeenNthCalledWith(2, 2); | ||
expect(subscriber2.next).toHaveBeenNthCalledWith(3, 3); | ||
|
||
expect(subscriber3.next).toHaveBeenCalledTimes(3); | ||
expect(subscriber3.next).toHaveBeenNthCalledWith(1, 1); | ||
expect(subscriber3.next).toHaveBeenNthCalledWith(2, 2); | ||
expect(subscriber3.next).toHaveBeenNthCalledWith(3, 3); | ||
}); | ||
|
||
test('emit no values, subscribe and unsubscribe', () => { | ||
const unsubscribe = jest.fn(); | ||
const observable$ = createObservable(() => { | ||
return unsubscribe; | ||
}); | ||
const subscriber = { | ||
next: jest.fn() | ||
}; | ||
|
||
const subscription = observable$.subscribe(subscriber); | ||
subscription.unsubscribe(); | ||
|
||
expect(subscriber.next).toHaveBeenCalledTimes(0); | ||
expect(unsubscribe).toHaveBeenCalledTimes(1); | ||
}); | ||
|
||
test('emit no values, subscribe and unsubscribe with void observer', () => { | ||
const observable$ = createObservable(() => {}); | ||
const subscriber = { | ||
next: jest.fn() | ||
}; | ||
|
||
const subscription = observable$.subscribe(subscriber); | ||
|
||
expect(() => subscription.unsubscribe()).not.toThrow(); | ||
expect(subscriber.next).toHaveBeenCalledTimes(0); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
/* | ||
* This file is part of the Neos.Neos.Ui package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
import type {Subscriber} from './Subscriber'; | ||
import type {Subscription} from './Subscription'; | ||
import type {Observer} from './Observer'; | ||
|
||
/** | ||
* An Observable emits values over time. You can attach a subscriber to it | ||
* using the Observable's `subscribe` method, or you can perform operations | ||
* producing new Observables via its `pipe` method. | ||
*/ | ||
export interface Observable<V> { | ||
subscribe: (subscriber: Subscriber<V>) => Subscription; | ||
} | ||
|
||
/** | ||
* An ObservablePipeOperation is a function that takes an observable and | ||
* returns a new observable. It can be passed to any Observable's `pipe` | ||
* method. | ||
*/ | ||
export interface ObservablePipeOperation<I, O> { | ||
(observable: Observable<I>): Observable<O>; | ||
} | ||
|
||
/** | ||
* Creates an Observable from the given Observer. | ||
*/ | ||
export function createObservable<V>(observer: Observer<V>): Observable<V> { | ||
const observable: Observable<V> = { | ||
subscribe(subscriber) { | ||
return Object.freeze({ | ||
unsubscribe: observer( | ||
subscriber.next, | ||
subscriber.error ?? noop | ||
) ?? noop | ||
}); | ||
} | ||
}; | ||
|
||
return Object.freeze(observable); | ||
} | ||
|
||
function noop() { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
* This file is part of the Neos.Neos.Ui package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
|
||
/** | ||
* An Observer is a function that emits values via its `next` callback. It can | ||
* return a function that handles all logic that must be performed when a | ||
* Subscription is cancelled (e.g. clearTimeout or similar cancellation | ||
* effects). | ||
*/ | ||
export interface Observer<V> { | ||
(next: (value: V) => void, fail: (error: any) => void): void | (() => void); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
/* | ||
* This file is part of the Neos.Neos.Ui package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
import {createState} from './State'; | ||
|
||
describe('State', () => { | ||
test('get current value', () => { | ||
const state$ = createState(0); | ||
|
||
expect(state$.current).toBe(0); | ||
|
||
state$.update((value) => value + 1); | ||
expect(state$.current).toBe(1); | ||
|
||
state$.update((value) => value + 1); | ||
expect(state$.current).toBe(2); | ||
|
||
state$.update((value) => value + 1); | ||
expect(state$.current).toBe(3); | ||
}); | ||
|
||
test('subscribe to state updates: subscriber receives current value immediately', () => { | ||
const state$ = createState(0); | ||
const subscriber1 = { | ||
next: jest.fn() | ||
}; | ||
const subscriber2 = { | ||
next: jest.fn() | ||
}; | ||
|
||
state$.subscribe(subscriber1); | ||
expect(subscriber1.next).toHaveBeenCalledTimes(1); | ||
expect(subscriber1.next).toHaveBeenNthCalledWith(1, 0); | ||
|
||
state$.update((value) => value + 1); | ||
state$.update((value) => value + 1); | ||
state$.update((value) => value + 1); | ||
|
||
state$.subscribe(subscriber2); | ||
expect(subscriber2.next).toHaveBeenCalledTimes(1); | ||
expect(subscriber2.next).toHaveBeenNthCalledWith(1, 3); | ||
}); | ||
|
||
test('subscribe to state updates: subscriber receives all updates', () => { | ||
const state$ = createState(0); | ||
const subscriber = { | ||
next: jest.fn() | ||
}; | ||
|
||
state$.subscribe(subscriber); | ||
state$.update((value) => value + 1); | ||
state$.update((value) => value + 1); | ||
state$.update((value) => value + 1); | ||
|
||
expect(subscriber.next).toHaveBeenCalledTimes(4); | ||
expect(subscriber.next).toHaveBeenNthCalledWith(1, 0); | ||
expect(subscriber.next).toHaveBeenNthCalledWith(2, 1); | ||
expect(subscriber.next).toHaveBeenNthCalledWith(3, 2); | ||
expect(subscriber.next).toHaveBeenNthCalledWith(4, 3); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/* | ||
* This file is part of the Neos.Neos.Ui package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
import {createObservable, Observable} from './Observable'; | ||
|
||
/** | ||
* A State is a special kind of Observable that keeps track of a value over | ||
* time. | ||
* | ||
* It has a public readonly `current` property that allows you to ask for | ||
* its current value at any point in time. A new subscriber to the State | ||
* Observable will also immediately receive the current value at the time of | ||
* subscription. | ||
* | ||
* Via the `update` method, a State's value can be modified. When called, | ||
* Subscribers to the state are immediately informed about the new value. | ||
*/ | ||
export interface State<V> extends Observable<V> { | ||
readonly current: V; | ||
update: (updateFn: (current: V) => V) => void; | ||
} | ||
|
||
/** | ||
* Creates a new State with the given initial value. | ||
*/ | ||
export function createState<V>(initialValue: V): State<V> { | ||
let currentState = initialValue; | ||
const listeners = new Set<(value: V) => void>(); | ||
const state: State<V> = { | ||
...createObservable((next) => { | ||
listeners.add(next); | ||
next(currentState); | ||
|
||
return () => listeners.delete(next); | ||
}), | ||
|
||
get current() { | ||
return currentState; | ||
}, | ||
|
||
update(updateFn) { | ||
const nextState = updateFn(currentState); | ||
|
||
if (currentState !== nextState) { | ||
currentState = nextState; | ||
|
||
for (const next of listeners) { | ||
next(currentState); | ||
} | ||
} | ||
} | ||
}; | ||
|
||
return Object.freeze(state); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
* This file is part of the Neos.Neos.Ui package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
|
||
/** | ||
* A Subscriber can be attached to an Observable. It receives values from the | ||
* Observable in its `next` callback function. It may also provide an optional | ||
* `error` callback, that will only be called if the Observable emits an Error. | ||
*/ | ||
export interface Subscriber<V> { | ||
next: (value: V) => void; | ||
error?: (error: Error) => void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
* This file is part of the Neos.Neos.Ui package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
|
||
/** | ||
* When attaching a Subscriber to an Observable, a Subscription is returned. | ||
* The `unsubscribe` method of the Subscription allows you to detach the | ||
* Subscriber from the Observable again, after which the Subscriber no longer | ||
* receives any values emitted from the Observable. | ||
*/ | ||
export interface Subscription { | ||
unsubscribe: () => void; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
/* | ||
* This file is part of the Neos.Neos.Ui package. | ||
* | ||
* (c) Contributors of the Neos Project - www.neos.io | ||
* | ||
* This package is Open Source Software. For the full copyright and license | ||
* information, please view the LICENSE file which was distributed with this | ||
* source code. | ||
*/ | ||
export type {Observable} from './Observable'; | ||
export {createObservable} from './Observable'; | ||
|
||
export type {State} from './State'; | ||
export {createState} from './State'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters