UNPKG

live-set

Version:

Class representing a changing and transformable collection of items.

385 lines (350 loc) 11.2 kB
/* @flow */ import Scheduler from './Scheduler'; import $$observable from 'symbol-observable'; export type LiveSetChangeRecord<+T> = {type: 'add', +value: T} | {type: 'remove', +value: T} | {type: 'end'}; export type LiveSetController<-T> = { closed: boolean; add(item: T): void; remove(item: T): void; error(err: any): void; end(): void; }; export type ListenHandler = { unsubscribe(): void; +pullChanges?: () => void; }; export type LiveSetInit<T> = { scheduler?: Scheduler; read(): Set<T>; listen( setValues: { (values: Set<T>): void }, controller: LiveSetController<T> ): void|ListenHandler|()=>void; }; export type LiveSetSubscriber<-T> = (changes: $ReadOnlyArray<LiveSetChangeRecord<T>>) => void; export type LiveSetSubscription = { closed: boolean; unsubscribe(): void; pullChanges(): void; }; export type LiveSetObserver<-T> = { +start?: ?(subscription: LiveSetSubscription) => void; +next?: ?LiveSetSubscriber<T>; +error?: ?(err: any) => void; +complete?: ?() => void; }; type LiveSetObserverRecord<T> = { ignore: number; observer: LiveSetObserver<T>; }; export default class LiveSet<T> { static defaultScheduler = new Scheduler(); _init: LiveSetInit<T>; _scheduler: Scheduler; _values: ?Set<T> = null; _mutableValues: boolean = false; // Whether we can mutate the _values Set. _active: ?{ controller: LiveSetController<T>; listenHandler: ListenHandler; } = null; _inSubscriptionStart = false; _ended: boolean = false; _endedWithError: boolean = false; _error: any = null; _queuedCall: boolean = false; _changeQueue: Array<LiveSetChangeRecord<T>> = []; _observers: Array<LiveSetObserverRecord<T>> = []; constructor(init: LiveSetInit<T>) { this._init = init; this._scheduler = init.scheduler || LiveSet.defaultScheduler; } static active<T>(initialValues: ?Set<T>, options: ?{scheduler?: Scheduler}): {liveSet: LiveSet<T>, controller: LiveSetController<T>} { const set = initialValues || new Set(); let controller; const liveSet = new LiveSet({ scheduler: options ? options.scheduler : undefined, read: () => set, listen: (setValues, _controller) => { setValues(set); controller = _controller; } }); liveSet.subscribe({}); return {liveSet, controller: (controller: any)}; } static constant<T>(values: Set<T>, options: ?{scheduler?: Scheduler}): LiveSet<T> { makeSetImmutable(values); const shouldNotHappen = () => { throw new Error('Should not happen'); }; const ls = new LiveSet({ scheduler: options ? options.scheduler : undefined, read: shouldNotHappen, listen: shouldNotHappen }); ls._ended = true; ls._values = values; ls._mutableValues = false; return ls; } _queueChange(record: ?LiveSetChangeRecord<T>) { if (record) { this._changeQueue.push(record); } if (!this._queuedCall) { this._queuedCall = true; this._scheduler.schedule(() => { this._queuedCall = false; const changes = this._changeQueue; this._changeQueue = []; let observersToCall; const ended = this._ended; if (ended) { observersToCall = this._observers; this._observers = []; } else { observersToCall = this._observers.slice(); } observersToCall.forEach(record => { const {observer, ignore} = record; const observerNext = observer.next; if (observerNext) { if (ignore === 0) { observerNext.call(observer, changes); } else { record.ignore = 0; const changesToDeliver = changes.slice(ignore); if (changesToDeliver.length) { observerNext.call(observer, changesToDeliver); } } } if (ended) { if (this._endedWithError) { if (observer.error) observer.error(this._error); } else { if (observer.complete) observer.complete(); } } }); }); } } _deactivate() { if (!this._active) throw new Error('already inactive'); const {listenHandler} = this._active; this._active = null; if (listenHandler) { listenHandler.unsubscribe(); } } values(): Set<T> { if (this._values) { if (this._active && !this._inSubscriptionStart) { const {listenHandler} = this._active; if (listenHandler.pullChanges) { listenHandler.pullChanges(); } } if (this._mutableValues) { this._mutableValues = false; makeSetImmutable(this._values); } /*:: if (!this._values) throw new Error(); */ return this._values; } else { if (this._active) { throw new Error('tried to call values() on liveset during subscription before setValues was called'); } const s = this._init.read(); makeSetImmutable(s); return s; } } isEnded(): boolean { return this._ended; } getScheduler(): Scheduler { return this._scheduler; } subscribe(observerOrOnNext: LiveSetObserver<T> | LiveSetSubscriber<T>, onError: ?(err: any) => void, onComplete: ?() => void): LiveSetSubscription { const liveSet = this; let observer; if (typeof observerOrOnNext === 'function') { observer = { next: observerOrOnNext, error: onError, complete: onComplete }; } else { observer = observerOrOnNext; } (observer: LiveSetObserver<T>); if (this._ended) { const subscription = { closed: false, unsubscribe: () => { subscription.closed = true; }, pullChanges: () => {} }; if (observer.start) { observer.start(subscription); } if (!subscription.closed) { if (this._endedWithError) { if (observer.error) { observer.error(this._error); } } else { if (observer.complete) { observer.complete(); } } } subscription.closed = true; return subscription; } const observerRecord = {observer, ignore: this._changeQueue.length}; let isStarting = true; let unsubscribedInStart = false; const subscription = { /*:: closed: false&&` */ get closed() { return !isStarting && liveSet._observers.indexOf(observerRecord) < 0; }/*:: ` */, unsubscribe: () => { if (isStarting) { unsubscribedInStart = true; return; } const ix = this._observers.indexOf(observerRecord); if (ix >= 0) { this._observers.splice(ix, 1); if (!this._ended && this._observers.length === 0) { this._values = null; this._deactivate(); } } }, pullChanges: () => { if (this._active && this._active.listenHandler && this._active.listenHandler.pullChanges) { this._active.listenHandler.pullChanges(); } const changeQueueLength = this._changeQueue.length; const originalNext = observer.next; if (changeQueueLength !== 0 && originalNext) { const changesToDeliver = this._changeQueue.slice(observerRecord.ignore); if (changesToDeliver.length !== 0) { observerRecord.ignore = changeQueueLength; originalNext.call(observer, changesToDeliver); } } } }; if (!this._active) { const controller: LiveSetController<T> = { // Flow doesn't support getters and setters yet /*:: closed: false&&` */ get closed() { return !liveSet._active || liveSet._active.controller !== this; }/*:: ` */, add: value => { let values = this._values; if (!values) throw new Error('setValue must be called before controller is used'); if (!this._ended && !values.has(value)) { if (!this._mutableValues) { this._values = values = new Set(values); this._mutableValues = true; } values.add(value); this._queueChange({type: 'add', value}); } }, remove: value => { let values = this._values; if (!values) throw new Error('setValue must be called before controller is used'); if (!this._ended && values.has(value)) { if (!this._mutableValues) { this._values = values = new Set(values); this._mutableValues = true; } values.delete(value); this._queueChange({type: 'remove', value}); } }, error: err => { if (this._ended) return; this._ended = true; this._endedWithError = true; this._error = err; this._queueChange(); this._deactivate(); }, end: () => { if (this._ended) return; this._ended = true; this._queueChange(); this._deactivate(); } }; const active = this._active = { controller, listenHandler: { unsubscribe: () => {} } }; const setValuesError: Function = () => { throw new Error('setValues must be called once during listen'); }; let setValues = values => { setValues = setValuesError; makeSetImmutable(values); this._values = values; this._mutableValues = false; }; const listenHandlerOrFunction = this._init.listen(values => setValues(values), controller); if (!this._values) { setValuesError(); } if (typeof listenHandlerOrFunction === 'function') { active.listenHandler = { unsubscribe: listenHandlerOrFunction }; } else if (listenHandlerOrFunction != null && typeof listenHandlerOrFunction.unsubscribe === 'function') { active.listenHandler = listenHandlerOrFunction; } else if (listenHandlerOrFunction != null) { throw new TypeError('listen must return object with unsubscribe method, a function, or null'); } if (controller.closed) { this._active = active; this._deactivate(); } } if (observer.start) { this._inSubscriptionStart = true; observer.start(subscription); this._inSubscriptionStart = false; } isStarting = false; observerRecord.ignore = this._changeQueue.length; if (!unsubscribedInStart) { this._observers.push(observerRecord); } return subscription; } } // Assign here because Flow doesn't support computed property keys on classes: // https://github.com/facebook/flow/issues/2286 (LiveSet:any).prototype[$$observable] = function() { return this; }; function makeSetImmutable(set: Set<any>) { if (process.env.NODE_ENV !== 'production') { (set:any).add = (set:any).delete = (set:any).clear = readOnly; } } function readOnly() { throw new Error('Do not modify Set passed to or from LiveSet: Set is read-only in development'); }