https://github.com/flatstadt/omens
A library to connect distributed logic
https://github.com/flatstadt/omens
angular messenger model observable
Last synced: 18 days ago
JSON representation
A library to connect distributed logic
- Host: GitHub
- URL: https://github.com/flatstadt/omens
- Owner: flatstadt
- License: mit
- Created: 2020-04-17T17:32:04.000Z (about 6 years ago)
- Default Branch: master
- Last Pushed: 2020-04-26T09:43:51.000Z (about 6 years ago)
- Last Synced: 2025-04-21T01:10:15.415Z (about 1 year ago)
- Topics: angular, messenger, model, observable
- Language: TypeScript
- Homepage:
- Size: 192 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
[]()
[](https://github.com/prettier/prettier)
[](#contributors-)
[](https://github.com/flatstadt/)
> Omens helps managing internal events
Frequently, we are in need to propagate changes and notify multiple listeners in a way that it doesn't create event loops. Omens helps to keep control over your event flow.
## Features
- ✅ Messenger
- ✅ Observable Model
- ✅ Observable Properties
## Table of Contents
- [Features](#features)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [NPM](#npm)
- [Yarn](#yarn)
- [Usage](#usage)
- [Messenger](#messenger)
- [Observable Model](#observable-model)
- [Observable Property Changed](#observable-property-changed)
## Installation
### NPM
`npm install @lapita/omens --save`
### Yarn
`yarn add @lapita/omens`
## Usage
Omens offers three different functionalities. A messenger to broadcast messages across components. An observable model that help to notify observers that their underlying model has changed and could react accordingly. Lastly, an observable property changed extension that creates events every time a property value changes.
### Messenger
The messenger allows to send a message to multiple listeners. There's a default messenger that can be accessed as `Messenger.default()`, but it also possible to create new instances by doing `new Messenger()`.
Each instance of `Messenger` keeps their messages separated. They work like separated delivery systems.
When instancing a new Messenger, it's possible to pass in options to tune they way it works.
```ts
export interface MessengerOptions {
buffer: number; // size of the internal buffer.
delay: number; // default delay of messages
ttl: number; // default expiration of the buffer
scheduler: SchedulerLike; // RXJS observable scheduler
}
```
The messenger uses an internal queue which is a hot observable. The events published before start listening are gone. The buffer size allows to always return the last n-number of messages.
Messages are custom classes that extends `Messages`. When sending a new message, a new instance of the Messages is created. A listener can subscribe to a Message type by passing in the class name.
```ts
import { Message, Messenger } from '@lapita/omens';
export class MessengerPlayground {
public static play() {
// Listening to CustomMessage sent through the default messenger
Messenger.default().listen(CustomMessage).subscribe(env => console.log('listener: msg received', env.read()));
console.log('Send message to only one listener');
Messenger.default().broadcast(new CustomMessage('broadcast message'));
}
}
class CustomMessage extends Message {
constructor(public text: string) {
super();
}
}
```
* `default()` - return the default `Messenger` instance.
* `listen(type): Observable` - listen to a type of Message
```ts
Messenger.default().listen(CustomMessage).subscribe();
```
The subscription returns a `Envelope`. An envelope contains the sent Message.
```ts
interface Envelope {
get deliveredAt(): number; // when the envelope was received
get opened(): boolean; // whether message was already read
get expired(): boolean; // whether message expired
read(answer?: any): T; // get the inside message. It's possible to send an answer to the sender.
}
```
* `listenToAll(): Observable` - listen to any type of Message
```ts
Messenger.default().listenToAll().subscribe();
```
* `sendToOne(msg: Message, cb?: RecipientAnswer): Receipt` - send a single message
This way of sending a message makes sure that only one listener receives the message even when there are multiple subscribers.
Optionally, it's possible to add a callback function `RecipientAnswer` to receive answers from recipients.
```ts
const answer = (answer) => {console.log('Answer received')}
const receipt = Messenger.default().sentToOne(new CustomMessage(), answer);
```
It returns a `Receipt`.
```ts
interface Receipt {
answer: RecipientAnswer; // returns a reference of the optional answer callback fcn.
requestCancellation(); // cancels the message delivery
}
```
* `broadcast(msg: Message, cb?: RecipientAnswer): Receipt` - broadcast a message to all listeners
### Observable Model
By using `ModelChanged`, any class becomes observable to any change. Observers can subscribe to changes and apply updates. Any change has attached a source which allows to break infinity update loops.
```ts
import { ModelChanged } from '@lapita/omens';
// Observable Model Container
export class AppService extends ModelChanged {
constructor() {
super(new AppModel());
}
}
// Model
class AppModel {
name = 'John';
lastname = 'Connor';
createdAt = 1000;
}
```
`ModelChanged` passes the needed functionality to the model container that allows to interact with it and observe changes.
```ts
export abstract class ModelChanged {
modelChanged(): Observable; // broadcast all changes of the underlying model
updating(): boolean; // model is being updated and no events are propagated
value(): U; // returns the model value
history(): ModelHistory[]; // returns history of changes
historyPointIndex(): number; // returns current history point being applied, which changes when undoing and redoing
historyLength(): number; // returns history length
undo(opts: Partial = {}); // undo last change
undoAll(opts: Partial = {}); // undo all changes
redo(opts: Partial = {}); // redo next change
redoAll(opts: Partial = {}); // redo all changes
getPropertyValue(key: K): any; // get a property value
getPartialValue(keys: K[]): Partial; // get a model chunk
update(value: Partial, opts: Partial = {}); // update the model
listen(observer: symbol, ownUpdates = false): Observable; // listen to changes filtered by source.
beginUpdate(); // start a update batch and hold update events
endUpdate(opts: Partial = {}); // end update and flush pending events
}
```
Listening events using `listen()` allows to filter events. So that if the observer match the source, the source is not notified unless `ownUpdates` is set `true`.
Many methods accept extra options to change the default behavior.
```ts
interface ModelActionOptions {
source: symbol; // source of action, if not set, internal source is used.
emitEvent: boolean; // whether to emit events as result of the action
}
```
```ts
export class ModelPlayground {
public static play() {
const listener = Symbol('listener');
const source = Symbol('source');
const service = new AppModelService();
service.listen(listener).subscribe(event => {
console.log('Model Changed', event);
});
// change model
service.update({ name: 'Mary' }, {source});
// sequence of changes
service.beginUpdate();
service.update({ name: 'Joseph'}, {source});
service.update({lastname: 'Bosch'}, {source});
service.update({createdAt: 4000}, {source});
service.endUpdate();
// get change history
console.log('Change history:');
console.table(service.history);
// undo change
service.undo();
// undo all
service.undoAll();
// redo all
service.redoAll();
console.log('final name', service.getPropertyValue('name'));
console.log('final partial value', service.getPartialValue(['name', 'lastname']));
}
}
```
### Observable Property Changed
Similarly to `ModelChanged`, `PropertyChanged` can be extended by a custom model container and make any change observable. The different lays in that `PropertyChanged` observes properties one by one, even properties inside child objects.
```ts
import { PropertyChanged } from '@lapita/omens';
export class AppService extends PropertyChanged {
constructor() {
super(new AppModel());
}
}
class AppModel {
name = 'John';
lastname = 'Connor';
createdAt = 1000;
address = {
street: 'Big way',
number: 3
};
}
```
The `constructor` of `PropertyChanged` accepts extra options to modify its default behavior.
```ts
interface PropertyOptions {
bufferTime: number; // pack update notification in bundle of n milliseconds
omitOlderUpdates: boolean; // only propagate the most recent updates in a given update bundle. e.g. [update(name = 'Tim') - update(name = 'Josh')], observer only receive update(name = 'Josh')] if this option is set true.
}
```
`PropertyChanged` passes the needed functionality to the model container that allows to interact with it and observe changes.
```ts
abstract class PropertyChanged {
propertyChanged(): Observable; // notify any change
value(): U; // returns the model value
updating(): boolean; // indicates whether the model is being updated
history(): PropertyHistory[]; // returns history of changes
historyPointIndex(): number; // returns current history point
historyLength(): number; // returns history length
properties(): FlattenProperty[]; // returns a list of properties
paths(): string[][]; // returns a list of the property paths
undo(opts: Partial = {}); // undo last change
undoAll(opts: Partial = {}); // undo all changes
redo(opts: Partial = {}); // redo last undo
redoAll(opts: Partial = {}); // redo all changes
getPropertyValue(path: string | string[]): any; // get a specific property value
setPropertyValue(path: string | string[], value: any, opts: Partial = {}); // change a property value
setPropertiesValue(data: Partial, opts: Partial = {}); // set the value of a collection of properties
listen(observer: symbol, ownUpdates = false): Observable; // listen to updates
beginUpdate(); // begin a batch of changes withholding events
endUpdate(opts: Partial = {}); // end current update batch and flush events
}
```
This example makes use of `PropertyChanged` to listen for any change and performing updates using two different source and listener identifiers.
```ts
import { AppPropertyService } from './app-property';
export class PropertyPlayground {
public static play() {
const listener = Symbol('listener');
const source = Symbol('source');
const service = new AppPropertyService();
service.listen(listener).subscribe(event => {
console.log('Changed', event);
});
// set a subset of properties
console.log('->Set Name and Lastname');
service.setPropertiesValue({name: 'Mary', lastname: 'Pills'}, {source});
console.log('->Set CreatedAt');
service.setPropertyValue('createdAt', 1200, {source});
// redo the
setTimeout(() => {
console.log('->Undo');
service.undo();
console.log('history->', service.history);
}, 20);
setTimeout(() => {
console.log('->Set Lastname');
service.setPropertyValue('lastname', 'Bosch');
console.log('history->', service.history);
}, 40);
setTimeout(() => {
console.log('->Undo all');
service.undoAll();
console.log('history->', service.history);
}, 60);
}
}
```