meteor-sdk
Version:
The aim of this library is to simplify the process of working with meteor server over DDP protocol using external JS environments
397 lines (356 loc) • 13.3 kB
text/typescript
import { ddpReducer } from './ddpReducer.js';
import { ddpReactiveDocument } from './ddpReactiveDocument.js';
import { ddpOnChange } from './ddpOnChange.js';
import { ddpCollection } from "./ddpCollection";
/**
* A reactive collection class.
* @constructor
* @param {ddpCollection} ddpCollection - Instance of @see ddpCollection class.
* @param {Object} [settings={skip:0,limit:Infinity,sort:false}] - Object for declarative reactive collection slicing.
* @param {Function} [filter=undefined] - Filter function.
*/
export class ddpReactiveCollection<T> {
private _skip: number;
private _limit: number;
private _sort: false | ((a: any, b: any) => number);
private _length: { result: number } = { result: 0 };
private _data: any[] = [];
private _rawData: any[] = [];
private _reducers: ddpReducer<any, any, any, T>[] = [];
private _tickers: any[] = [];
private _ones: any[] = [];
private _first = {}
private _syncFunc: (skip: number | undefined, limit: number | undefined, sort: ((a: any, b: any) => number) | false) => any;
private _changeHandler;
private started: boolean;
constructor(ddpCollectionInstance: ddpCollection<T>, settings?: { skip?: number; limit?: number; sort?: false | ((a: T, b: T) => number) }, filter?: boolean | ((value: T, index: number, array: T[]) => any)) {
this._skip = settings && typeof settings.skip === 'number' ? settings.skip : 0;
this._limit = settings && typeof settings.limit === 'number' ? settings.limit : Infinity;
this._sort = settings && typeof settings.sort === 'function' ? settings.sort : false;
this._syncFunc = function (skip: number | undefined, limit: number | undefined, sort: ((a: any, b: any) => number) | boolean) {
const options: {
skip?: number,
limit?: number,
sort?: ((a: any, b: any) => number) | boolean
} = {};
if (typeof skip === 'number') options.skip = skip;
if (typeof limit === 'number') options.limit = limit;
if (sort) {
options.sort = sort;
}
return ddpCollectionInstance.fetch.call(ddpCollectionInstance, options);
};
// @ts-ignore
// @ts-ignore
this._changeHandler = ddpCollectionInstance.onChange(({ prev, next, predicatePassed }) => {
if (prev && next) {
if (predicatePassed[0] == 0 && predicatePassed[1] == 1) {
// prev falling, next passing filter, adding new element with sort
this._smartUpdate(next);
} else if (predicatePassed[0] == 1 && predicatePassed[1] == 0) {
// prev passing, next falling filter, removing old element
let i = this._rawData.findIndex((obj) => {
return obj._id == prev._id;
});
this._removeItem(i);
} else if (predicatePassed[0] == 1 && predicatePassed[1] == 1) {
// both passing, should delete previous and add new
let i = this._rawData.findIndex((obj) => {
return obj._id == prev._id;
});
this._smartUpdate(next, i);
}
} else if (!prev && next) {
// element was added and is passing the filter
// adding new element with sort
this._smartUpdate(next);
} else if (prev && !next) {
// element was removed and is passing the filter, so it was in newCollection
// removing old element
let i = this._rawData.findIndex((obj) => {
return obj._id == prev._id;
});
this._removeItem(i);
}
this._length.result = this._data.length;
this._reducers.forEach((reducer) => {
reducer.doReduce();
});
if (this._data[0] !== this._first) {
this._updateReactiveObjects();
}
this._first = this._data[0];
this._tickers.forEach((ticker) => {
ticker(this.data());
});
//@ts-ignore
}, filter ? filter : (_) => 1);
this.started = false;
this.start();
}
/**
* Removes document from the local collection copies.
* @private
* @param {number} i - Document index in this._rawData array.
*/
_removeItem(i: number) {
this._rawData.splice(i, 1);
if (i >= this._skip && i < this._skip + this._limit) {
this._data.splice(i - this._skip, 1);
if (this._rawData.length >= this._skip + this._limit) {
this._data.push(this._rawData[this._skip + this._limit - 1]);
}
} else if (i < this._skip) {
this._data.shift();
if (this._rawData.length >= this._skip + this._limit) {
this._data.push(this._rawData[this._skip + this._limit - 1]);
}
}
}
/**
* Adds document to local the collection this._rawData according to used sorting if specified.
* @private
* @param {Object} newEl - Document to be added to the local collection.
* @param j
* @return {boolean} - The first element in the collection was changed
*/
_smartUpdate(newEl: {}, j?: number) {
let placement;
if (!this._rawData.length) {
placement = this._rawData.push(newEl) - 1;
if (placement >= this._skip && placement < this._skip + this._limit) {
this._data.push(newEl);
}
return;
}
if (this._sort) {
for (let i = 0; i < this._rawData.length; i++) {
if (this._sort(newEl, this._rawData[i]) < 1) {
placement = i;
if (i == j) {
// new position is the the same
this._rawData[i] = newEl;
if (j >= this._skip && j < this._skip + this._limit) {
this._data[j - this._skip] = newEl;
}
} else {
// new position is different
// removing old element and adding new
this._removeItem(j as number);
this._rawData.splice(i, 0, newEl);
if (i >= this._skip && i < this._skip + this._limit) {
this._data.splice(i - this._skip, 0, newEl);
this._data.splice(this._limit);
}
}
break;
}
if (i == this._rawData.length - 1) {
placement = this._rawData.push(newEl) - 1;
if (placement >= this._skip && placement < this._skip + this._limit) {
this._data.push(newEl);
}
break;
}
}
} else {
// no sorting, trying to change existing
if (typeof j === 'number') {
placement = j;
this._rawData[j] = newEl;
if (j >= this._skip && j < this._skip + this._limit) {
this._data[j - this._skip] = newEl;
}
} else {
placement = this._rawData.push(newEl) - 1;
if (placement >= this._skip && placement < this._skip + this._limit) {
this._data.push(newEl);
}
}
}
}
/**
* Adds reducer.
* @private
* @param {ddpReducer} reducer - A ddpReducer object that needs to be updated on changes.
*/
_activateReducer<RArgs extends [any[], any, number, any[]], RResult, RInit>(reducer: ddpReducer<RArgs, RResult, RInit, T>) {
this._reducers.push(reducer);
}
/**
* Adds reactive object.
* @private
* @param {ddpReactiveDocument} o - A ddpReactiveDocument object that needs to be updated on changes.
*/
_activateReactiveObject(o: any) {
this._ones.push(o);
}
/**
* Removes reducer.
* @private
* @param {ddpReducer} reducer - A ddpReducer object that does not need to be updated on changes.
*/
_deactivateReducer<RArgs extends [any[], any, number, any[]], RResult, RInit>(reducer: ddpReducer<RArgs, RResult, RInit, T>) {
let i = this._reducers.indexOf(reducer);
if (i > -1) {
this._reducers.splice(i, 1);
}
}
/**
* Removes reactive object.
* @private
* @param {ddpReactiveDocument} o - A ddpReducer object that does not need to be updated on changes.
*/
_deactivateReactiveObject<RArgs extends [any[], any, number, any[]], RResult, RInit>(o: ddpReducer<RArgs, RResult, RInit, T>) {
let i = this._ones.indexOf(o);
if (i > -1) {
this._ones.splice(i, 1);
}
}
/**
* Sends new object state for every associated reactive object.
* @public
*/
_updateReactiveObjects() {
this._ones.forEach((ro) => {
ro._update(this.data()[0]);
});
}
/**
* Updates ddpReactiveCollection settings.
* @public
* @param {Object} [settings={skip:0,limit:Infinity,sort:false}] - Object for declarative reactive collection slicing.
* @return {this}
*/
settings(settings: { skip?: number; limit?: typeof Infinity; sort?: false | ((a: T, b: T) => number); }) {
let skip, limit, sort;
if (settings) {
skip = settings.skip;
limit = settings.limit;
sort = settings.sort;
}
this._skip = skip !== undefined ? skip : this._skip;
this._limit = limit !== undefined ? limit : this._limit;
this._sort = sort !== undefined ? sort : this._sort;
this._data.splice(0, this._data.length, ...this._syncFunc(this._skip, this._limit, this._sort));
this._updateReactiveObjects();
return this;
}
/**
* Updates the skip parameter only.
* @public
* @param {number} n - A number of documents to skip.
* @return {this}
*/
skip(n: number) {
return this.settings({ skip: n });
}
/**
* Updates the limit parameter only.
* @public
* @param {number} n - A number of documents to observe.
* @return {this}
*/
limit(n: number) {
return this.settings({ limit: n });
}
/**
* Stops reactivity. Also stops associated reactive objects.
* @public
*/
stop() {
if (this.started) {
this._changeHandler.stop();
this.started = false;
}
}
/**
* Starts reactivity. This method is being called on instance creation.
* Also starts every associated reactive object.
* @public
*/
start() {
if (!this.started) {
this._rawData.splice(0, this._rawData.length, ...this._syncFunc(0, 0, this._sort));
this._data.splice(0, this._data.length, ...this._syncFunc(this._skip, this._limit, this._sort));
this._updateReactiveObjects();
this._changeHandler.start();
this.started = true;
}
}
/**
* Sorts local collection according to specified function.
* Specified function form {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/sort}.
* @public
* @param {Function} f - A function used for sorting.
* @return {this}
*/
sort(f: false | ((a: T, b: T) => number)) {
this._sort = f;
if (this._sort) {
this._rawData.splice(0, this._rawData.length, ...this._syncFunc(0, 0, this._sort));
this._data.splice(0, this._data.length, ...this._syncFunc(this._skip, this._limit, this._sort));
this._updateReactiveObjects();
}
return this;
}
/**
* Returns reactive local collection with applied sorting, skip and limit.
* This returned array is being mutated within this class instance.
* @public
* @return {Array} - Local collection with applied sorting, skip and limit.
*/
data() {
return this._data;
}
/**
* Runs a function every time a change occurs.
* @param {Function} f - Function which recieves new collection at each change.
* @public
*/
onChange(f: {}) {
const self = this as unknown as { [x: string]: any[] };
return ddpOnChange(f, self, '_tickers');
}
/**
* Maps reactive local collection to another reactive array.
* Specified function form {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/map}.
* @public
* @param {Function} f - Function that produces an element of the new Array.
* @return {ddpReducer} - Object that allows to get reactive mapped data @see ddpReducer.
*/
// @ts-ignore
map<Arr extends any[]>(f: (arg0: T, arg1: number, arg2: any[]) => Arr) {
return new ddpReducer(this, function (accumulator, el, i, a) {
return accumulator.concat(f(el, i, a));
}, []) as unknown as Arr
}
/**
* Reduces reactive local collection.
* Specified function form {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce}.
* @public
* @param {Function} f - Function to execute on each element in the array.
* @param {*} initialValue - Value to use as the first argument to the first call of the function.
* @return {ddpReducer} - Object that allows to get reactive object based on reduced reactive local collection @see ddpReducer.
*/
reduce(f: (args_0: any[], args_1: any, args_2: number, args_3: any[]) => unknown, initialValue: any) {
return new ddpReducer(this, f, initialValue);
}
/**
* Reactive length of the local collection.
* @public
* @return {Object} - Object with reactive length of the local collection. {result}
*/
count() {
return this._length;
}
/**
* Returns a reactive object which fields are always the same as the first object in the collection.
* @public
* @param {Object} [settings={preserve:false}] - Settings for reactive object. Use {preserve:true} if you want to keep object on remove.
* @return {ddpReactiveDocument} - Object that allows to get reactive object based on reduced reactive local collection @see ddpReactiveDocument.
*/
one(settings: { preserve: any; } | null) {
return new ddpReactiveDocument<T>(this, settings);
}
}