rxjs-supersets v2.2.0
rxjs-supersets
A collection of Typescript Maps and Sets that publish changes in their state (entries added, modified or deleted) using RxJS Observables.
For the latest changes and the current version see the Change log.
Table of contents
Introduction
This library was created to support subscribing to streams of updates to a Map
or Set
.
Subscribers will always get the current Map
or Set
state when they subscribe,
along with updates from that point onward.
rxjs-supersets
contains a number of Typescript Map
and Set
subclasses that have an RxJS Observable
property delta$
that you can subscribe to if you want to be informed of the changes taking place in the Set
or Map
. It keeps track of addition, modification and deletion of entries in the Set
or Map
.
rxjs-supersets
has only one dependency (tslib), one peer dependency (rxjs) and one optional peer dependency (immer).
The content changes are published in MapDelta
format:
export interface MapDelta<K, V> {
all: ReadonlyMap<K, V>;
added: ReadonlyMap<K, V>;
deleted: ReadonlyMap<K, V>;
modified: ReadonlyMap<K, V>;
}
The entries returned in a MapDelta
are defined as Readonly
in Typescript.
This is done as a precaution to prevent accidental in place updating of the entries.
The correct way is to create a copy of the entry when updating its properties. There are excellent libraries for this, e.g. Immer, immutable-js or lodash just to name a few.
A processDelta
rxjs operator is provided to help processing the resulting MapDelta
changes.
In detail
Maps and Sets
- DeltaMap, a basic observable map
- DeltaSet, a set of IdObjects, objects with an 'id' property
- SuperSet, a set of IdMemberObjects, 'IdObjects' with a
memberOf
property that makes them member of one or more subsets within theSuperSet
and provides automatic (sub)set operations and management. You can subscribe to thedelta$
of the individual subsets. - SimpleSuperSet, a simpler version of of the
SuperSet
that does not have subset subscription.
RxJS operators
- filterDelta filters all added and modified elements of a MapDelta.
- groupDelta moves MapDelta
IdObject
entries into groups and forwards the result as a MapDelta. - mapDelta creates a mapping over all added and modified elements of a MapDelta, this can result in another class of
IdObject
for the processed elements. - produceDelta creates a mapping over all added and modified elements of a MapDelta using the immer
produce
function, this always results in the same class ofIdObject
for the processed elements. - startDelta makes sure that a new subscription to an existing DeltaMap or DeltaSet always gets the full list of elements in the added property on the first delta$ subscription update.
- tapDelta can create side effects for all added, modified and deleted elements of a MapDelta.
- Deprecated: processDelta a combination of startDelta and tapDelta.
RxJS creators
- mergeDelta merges multiple
MapDelta
' Observables of the same type to a singleMapDelta
Observable.
Utility functions
- processElements allows easy processing of all elements in a
MapDelta
. - createDelta allows easy creation of dummy
MapDelta
structures for unit testing.
DataTypes and Interfaces
See types.ts for the type definitions of the types and interfaces used
Examples
This section contains example code demonstrating how the rxjs-supersets
maps and sets can be used.
DeltaMap example
DeltaMap is the basic class that provides the delta$
observable, the other sets in rxjs-supersets
are based on this class.
const deltaMap = new DeltaMap<string, Date>();
deltaMap.set('item1', new Date());
deltaMap.set('item2', new Date());
// deltaMap.delta$ is a Replaysubject(1), it only returns the last update
deltaMap.delta$.subscribe(delta => {
delta.all; // contains a map with both added entries
delta.added; // contains a map with only the latest addition (item2)
delta.modified; // contains a map with no entries (nothing modified)
delta.deleted; // contains a map with no entries (nothing deleted)
});
// modify a DeltaMap entry
deltaMap.set('item1', new Date());
// latest deltaMap.delta$ update now contains
deltaMap.delta$.subscribe(delta => {
delta.all; // a map with both entries
delta.added; // a map with no entries (nothing added)
delta.modified; // a map with item1
delta.deleted; // a map with no entries (nothing deleted)
});
// delete a DeltaMap entry
deltaMap.delete('item2');
// latest deltaMap.delta$ update now contains
deltaMap.delta$.subscribe(delta => {
delta.all; // a map with remaining entry (item1)
delta.added; // a map with no entries (nothing added)
delta.modified; // a map with no entries (nothing modified)
delta.deleted; // a map with item2
});
operator examples
RxJS operators have been added to make working with MapDelta's easier.
const deltaMap = new DeltaMap<string, Date>();
deltaMap.set('item1', new Date());
deltaMap.set('item2', new Date());
// if you want a new subscription to always start with all in the added property
// you can insert the startDelta() operator
deltaMap.delta$.pipe(startDelta()).subscribe(delta => {
delta.all; // contains a map with both added entries
delta.added; // contains a map with all entries on the first update
delta.modified; // contains a map with no entries on the first update
delta.deleted; // contains a map with no entries on the first update
});
// if you do not want to iterate through the updates yourself
// you can use the tapDelta operator for this
deltaMap.delta$.pipe(
startDelta(),
tapDelta({
before: () => initUpdate(), // call before update processing (optional)
add: entry => doAdd(entry), // processes both entries one at a time (optional)
modify: entry => doModify(entry), // ignored because there are no entries to process (optional)
delete: entry => doDelete(entry), // ignored because there are no entries to process (optional)
after: () => completeUpdate() // call after update processing (optional)
})
).subscribe();
// if you only want to process certain elements of a MapDelta
// you can use the filterDelta() operator
deltaMap.delta$.pipe(
startDelta(),
filterDelta(element => element < Date.now())
).subscribe(delta => {
delta.all; // contains a map with both added entries
delta.added; // contains a map with all entries on the first update
delta.modified; // contains a map with no entries on the first update
delta.deleted; // contains a map with no entries on the first update
});
DeltaSet example
DeltaSet
extends the DeltaMap
, it treats its contents more as a Set, where the id
property of its content uniquely identifies the entry in the Set.
// an IdObject class to demonstrate te DeltaSet
class IdContent implements IdObject {
constructor (
public id: string,
public content: string,
public extra?: string
) { }
}
const item1 = new IdContent('id1','content1');
const item2 = new IdContent('id2','content2');
const item3 = new IdContent('id3','content3');
const deltaSet = new DeltaSet<string, IdContent>();
deltaSet.addMultiple([item1, item2, item3]);
// deltaSet.delta$ is a Replaysubject(1), it only returns the last update
deltaSet.delta$.subscribe(delta => {
delta.all; // a map with all added entries
delta.added; // a map with only the latest addition (item3)
delta.modified; // a map with no entries (nothing modified)
delta.deleted; // a map with no entries (nothing deleted)
});
// update an existing entry
const item2b = new IdDate('id2','content2b');
deltaSet.add(item2b); // item2 is replaced with item2b
// a subscription would receve the following
deltaSet.delta$.subscribe(delta => {
delta.all; // a map with all current entries
delta.added; // a map with no entries (nothing modified)
delta.modified; // a map with item2b
delta.deleted; // a map with no entries (nothing deleted)
});
// you can change (map) the content of all MapDelta elements using the
// mapDelta() operator. It is best used along the 'immer' library
import { produce } from 'immer';
deltaSet.delta$.pipe(
mapDelta(element => produce(element, draft => {
draft.id += 'm'; // you can even change it's id
draft.extra = 'mapped!';
}))
).subscribe(delta => {
delta.all; // a map with all mapped entries
delta.added; // a map with new mapped entries
delta.modified; // a map with modified mapped entries
delta.deleted; // a map with deleted mapped entries
});
// if you return the same object type, you can simplify the above using the produceDelta() operator
deltaSet.delta$.pipe(
produceDelta(draft => {
draft.id += 'm'; // you can even change it's id
draft.extra = 'mapped!';
})
).subscribe();
settings example
All rxjs-supersets
Maps and Sets can have settings added to modify their behaviour.
// an IdObject class to demonstrate te DeltaSet
class IdContent implements IdObject {
constructor (
public id: string,
public content: string
) { }
}
const item1 = new IdDate('id1','content1');
const item2 = new IdDate('id2','content2');
const item3 = new IdDate('id3','content3');
const deltaSet = new DeltaSet<string, IdContent>({
isUpdated: (newItem, existingItem) => newItem.content === existingItem.content,
publishEmpty: true
});
// normally a delta$ subscription only starts receiving updates if the set is not empty.
// if 'publishEmpty' is set to true, initially empty sets also publish updates
deltaSet.delta$.subscribe(delta => {
delta.all; // a map with no entries (nothing present)
delta.added; // a map with no entries (nothing added)
delta.modified; // a map with no entries (nothing modified)
delta.deleted; // a map with no entries (nothing deleted)
});
deltaSet.addMultiple([item1, item2, item3]);
// entries added and updates sent to delta$ subscriptions
const item2b = new IdDate('id2','content2');
deltaSet.add(item2b);
// item2b will not replace item2 because 'isUpdated' returns false
// delta$ subscriptions will not receive an update because nothing was changed
SuperSet and SimpleSuperSet example
The SuperSet
is a collection of entries that each are member of one or more of its subsets.
It extends the DeltaSet
. The difference between SuperSet
and SimpleSuperSet
is that you can
subscribe to the subset delta$
to receive updates whereas with the SimpleSuperSet
you cannot do that.
// a MemberObject class to demonstrate te SuperSet
class MemberContent implements MemberObject {
public memberOf: Set<string>
constructor (
public id: string,
memberOf: string[],
public content: string
) {
this.memberOf = new Set(memberOf);
}
}
const item1 = new MemberContent('id1', ['subset1'], 'content1');
const item2 = new MemberContent('id2', ['subset1'], 'content2');
const item3 = new MemberContent('id3', ['subset1'], 'content3');
const item4 = new MemberContent('id4', ['subset1', 'subset2'], 'content4');
const item5 = new MemberContent('id5', ['subset2'], 'content5');
const item6 = new MemberContent('id6', ['subset1', 'subset3'], 'content6');
const superSet = new SuperSet<string, MemberContent>();
const supersetSubscription = superSet.delta$.subscribe();
// subscribing to an emty subset creates it
const subset2Subscription = superSet.subsets.get('subset2').delta$.subscribe();
superSet.addMultiple([item1, item2, item3, item4, item5, item6]);
// 'subset2Subscription' receives a delta that tells that MemberContent entries with
// id4 and id5 have been added.
// 'superSetSubscription' receives a delta that tells that MemberContent entries with
// id1, id2, id3, id4, id5 and id6 have been added.
// Subscribing to an exisiting subset returns its members directly after the subscription.
const subset1Subscription = superSet.subsets.get('subset1').delta$.subscribe();
// 'subset1Subscription' receives a delta that tells that MemberContent entries with
// id1, id2, id3, id4 and id6 are present (in `all` property).
superset.deleteSubsetItems('subset3');
// All entries in subset3 are removed from the superset and also
// removed from all other subsets they are member of.
// Both 'superSetSubscription' and 'subset1Subscription' receive a delta that tells
// that MemberContent entries with id6 was deleted.
superset.subsets.empty('subset2');
// All entries in subset 2 are removed from the subset,
// entries that are no longer in a subset are also removed from the SuperSet.
// 'subset2Subscription' receives a delta that tells that MemberContent entries with
// id4 and id5 were deleted.
// 'superSetSubscription' receives a delta that tells that MemberContent entries with
// id5 was deleted.
superset.subsets.delete('subset1');
// All entries in subset 1 are removed from the subset, its `delta` observable is closed,
// the subset is removed from the SuperSet.
// 'subset1Subscription' receives a delta that tells that MemberContent entries with
// id1, id2, id3 and id4 were deleted, after that the subscription is closed.
// 'superSetSubscription' receives a delta that tells that MemberContent entries with
// id1, id2, id3 and id4 were deleted.