Comparing version 0.0.10 to 0.0.11
# Changelog | ||
## 0.0.11 / 2024-03-22 | ||
**New** | ||
- Add support for multiple update operations in single call. | ||
**Fixes** | ||
- Restore clone mode default to "copy". | ||
## 0.0.10 / 2024-03-04 | ||
@@ -4,0 +11,0 @@ **New** |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Selector = exports.Store = exports.createStore = void 0; | ||
exports.Store = exports.createStore = void 0; | ||
const mingo_1 = require("mingo"); | ||
@@ -10,4 +10,4 @@ const core_1 = require("mingo/core"); | ||
const util_1 = require("mingo/util"); | ||
const selector_1 = require("./selector"); | ||
const util_2 = require("./util"); | ||
const NONE = Symbol(); | ||
const EMPTY_QUERY = new mingo_1.Query({}); | ||
@@ -43,4 +43,2 @@ /** helper to create query object. */ | ||
this.selectors = new Map(); | ||
// signals for notifying selectors of changes. | ||
this.signals = new Map(); | ||
// flag for checking modifications to the entire state. | ||
@@ -52,6 +50,6 @@ this.modified = true; | ||
useStrictMode: false })); | ||
this.mutate = (0, updater_1.createUpdater)(Object.assign({ cloneMode: "none" }, options)); | ||
this.mutate = (0, updater_1.createUpdater)(Object.assign(Object.assign({ cloneMode: "copy" }, options), { queryOptions: this.queryOptions })); | ||
} | ||
/** | ||
* Returns the current state as a frozen object subject to the given criteria. | ||
* Returns the current state as a frozen object subject to the given criteria.s | ||
* When no options are specified, returns the full state. | ||
@@ -104,3 +102,3 @@ * | ||
if (this.selectors.has(hash)) { | ||
return this.selectors.get(hash); | ||
return this.selectors.get(hash).selector; | ||
} | ||
@@ -116,3 +114,3 @@ // get expected paths to monitor for changes. | ||
// create and add a new selector | ||
const selector = new Selector(this, mkQuery(condition, this.queryOptions), projection); | ||
const selector = new selector_1.Selector(this.getState.bind(this), mkQuery(condition, this.queryOptions), projection); | ||
// if no field is specified, select everything. | ||
@@ -132,5 +130,3 @@ const pred = !expected.size | ||
}; | ||
// this.selectors.add(selector); | ||
this.signals.set(selector, signal); | ||
this.selectors.set(hash, selector); | ||
this.selectors.set(hash, { selector, signal }); | ||
return selector; | ||
@@ -141,3 +137,3 @@ } | ||
* | ||
* @param {UpdateExpression} expr Update expression as a MongoDB update query. | ||
* @param {UpdateExpression | UpdateExpression[]} expr Update expression as a MongoDB update query. | ||
* @param {Array<RawObject>} arrayFilters Array filter expressions to filter elements to update. | ||
@@ -148,3 +144,6 @@ * @param {RawObject} condition Condition to check before applying update. | ||
update(expr, arrayFilters = [], condition = {}) { | ||
const fields = this.mutate(this.state, expr, arrayFilters, condition); | ||
const query = mkQuery(condition, this.queryOptions); | ||
const fields = Array.from(new Set( | ||
// apply mutations | ||
(0, util_1.ensureArray)(expr).flatMap(e => this.mutate(this.state, e, arrayFilters, query)))); | ||
// return if state is unchanged | ||
@@ -154,2 +153,4 @@ if (!fields.length) { | ||
} | ||
// maintain stability. | ||
fields.sort(); | ||
// set modified flag | ||
@@ -159,4 +160,3 @@ this.modified = true; | ||
let notifyCount = 0; | ||
this.selectors.forEach(selector => { | ||
const signal = this.signals.get(selector); | ||
for (const { selector, signal } of this.selectors.values()) { | ||
// record the number of listeners before notifying the selector. | ||
@@ -167,3 +167,3 @@ // upon notification a listener will be removed from the selector if it throws or is configured to run once. | ||
notifyCount += size; | ||
}); | ||
} | ||
return { modified: true, fields, notifyCount }; | ||
@@ -173,120 +173,1 @@ } | ||
exports.Store = Store; | ||
/** | ||
* Provides an observable interface for selecting customized views of the state. | ||
* Listeners can subscribe to be notified of changes in the view repeatedely or once. | ||
*/ | ||
class Selector { | ||
/** | ||
* Construct a new selector | ||
* @param store Reference to the store object. | ||
* @param query Query object for checking conditions based on MongoDB filter query. | ||
* @param projection View of the state to select expressed as MongoDB projection query. | ||
*/ | ||
constructor(store, query, projection) { | ||
this.store = store; | ||
this.query = query; | ||
this.projection = projection; | ||
// iteration happens in insertion order. | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set | ||
this.listeners = new Set(); | ||
// listeners to be run once only also included in the main listener set. | ||
this.onceOnly = new Set(); | ||
// flag used to control when to use cached value. | ||
this.cached = false; | ||
} | ||
/** Returns the number of subscribers to this selector. */ | ||
get size() { | ||
return this.listeners.size; | ||
} | ||
/** | ||
* Returns the current state view subject to the selector criteria. | ||
* The value is only recomputed when the depedent fields in the criteria change. | ||
* | ||
* @returns {T | undefined} | ||
*/ | ||
getState() { | ||
// return cached if value has not changed since | ||
if (this.cached) | ||
return this.value; | ||
// update cached status | ||
this.cached = true; | ||
// project fields and freeze final value if query passes | ||
return (this.value = this.store.getState(this.projection, this.query)); | ||
} | ||
/** | ||
* Notify all listeners with the current value of the selector if different from the previous value. | ||
* If a listener throws an exception when notified, it is removed and does not receive future notifications. | ||
*/ | ||
notifyAll() { | ||
// only recompute if there are active listeners. | ||
if (!this.listeners.size) | ||
return; | ||
const prev = this.cached ? this.getState() : NONE; | ||
// reset the cache when notifyAll() is called. | ||
this.cached = false; | ||
// compute new value. | ||
const val = this.getState(); | ||
// No change so skip notifications. If a new subscriber was added after the last notification, it will be skipped here as well. | ||
// This is becuase the state has still not changed after it was added. For new subscribers to receive current state on subcsription, | ||
// they should be registered with {runImmediately: true}. | ||
if ((0, util_1.isEqual)(prev, val)) | ||
return; | ||
for (const f of this.listeners) { | ||
/*eslint-disable*/ | ||
try { | ||
f(val); | ||
} | ||
catch (_a) { | ||
// on error unsubscribe listener | ||
this.listeners.delete(f); | ||
} | ||
finally { | ||
// if runOnce, cleanup afterwards | ||
if (this.onceOnly.delete(f)) { | ||
this.listeners.delete(f); | ||
} | ||
} | ||
/*eslint-disable-enable*/ | ||
} | ||
} | ||
/** | ||
* Subscribe a listener to be notified about state updates. | ||
* | ||
* @param listener The function to receive new data on update. | ||
* @returns {Unsubscribe} Function to unsubscribe listener. | ||
*/ | ||
subscribe(listener, options) { | ||
// check if we are reregistering the same observer | ||
if (this.listeners.has(listener)) { | ||
throw new Error("Listener already subscribed."); | ||
} | ||
// setup to throw after first run. | ||
if (options && options.runOnce) { | ||
this.onceOnly.add(listener); | ||
} | ||
this.listeners.add(listener); | ||
const unsub = () => { | ||
this.onceOnly.delete(listener); | ||
this.listeners.delete(listener); | ||
}; | ||
if (options && options.runImmediately) { | ||
// immediately invoke | ||
const val = this.getState(); | ||
if (val !== undefined) { | ||
try { | ||
listener(val); | ||
} | ||
catch (e) { | ||
unsub(); | ||
throw e; | ||
} | ||
finally { | ||
if (this.onceOnly.has(listener)) | ||
unsub(); | ||
} | ||
} | ||
} | ||
return unsub; | ||
} | ||
} | ||
exports.Selector = Selector; |
@@ -17,2 +17,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
__exportStar(require("./_internal/selector"), exports); | ||
__exportStar(require("./_internal/store"), exports); |
@@ -6,5 +6,5 @@ import { Query } from "mingo"; | ||
import { createUpdater } from "mingo/updater"; | ||
import { assert, cloneDeep, isEqual, normalize, stringify } from "mingo/util"; | ||
import { assert, cloneDeep, ensureArray, isEqual, normalize, stringify } from "mingo/util"; | ||
import { Selector } from "./selector"; | ||
import { cloneFrozen, getDependentPaths, isProjectExpression, sameAncestor } from "./util"; | ||
const NONE = Symbol(); | ||
const EMPTY_QUERY = new Query({}); | ||
@@ -39,4 +39,2 @@ /** helper to create query object. */ | ||
this.selectors = new Map(); | ||
// signals for notifying selectors of changes. | ||
this.signals = new Map(); | ||
// flag for checking modifications to the entire state. | ||
@@ -48,6 +46,6 @@ this.modified = true; | ||
useStrictMode: false })); | ||
this.mutate = createUpdater(Object.assign({ cloneMode: "none" }, options)); | ||
this.mutate = createUpdater(Object.assign(Object.assign({ cloneMode: "copy" }, options), { queryOptions: this.queryOptions })); | ||
} | ||
/** | ||
* Returns the current state as a frozen object subject to the given criteria. | ||
* Returns the current state as a frozen object subject to the given criteria.s | ||
* When no options are specified, returns the full state. | ||
@@ -100,3 +98,3 @@ * | ||
if (this.selectors.has(hash)) { | ||
return this.selectors.get(hash); | ||
return this.selectors.get(hash).selector; | ||
} | ||
@@ -112,3 +110,3 @@ // get expected paths to monitor for changes. | ||
// create and add a new selector | ||
const selector = new Selector(this, mkQuery(condition, this.queryOptions), projection); | ||
const selector = new Selector(this.getState.bind(this), mkQuery(condition, this.queryOptions), projection); | ||
// if no field is specified, select everything. | ||
@@ -128,5 +126,3 @@ const pred = !expected.size | ||
}; | ||
// this.selectors.add(selector); | ||
this.signals.set(selector, signal); | ||
this.selectors.set(hash, selector); | ||
this.selectors.set(hash, { selector, signal }); | ||
return selector; | ||
@@ -137,3 +133,3 @@ } | ||
* | ||
* @param {UpdateExpression} expr Update expression as a MongoDB update query. | ||
* @param {UpdateExpression | UpdateExpression[]} expr Update expression as a MongoDB update query. | ||
* @param {Array<RawObject>} arrayFilters Array filter expressions to filter elements to update. | ||
@@ -144,3 +140,6 @@ * @param {RawObject} condition Condition to check before applying update. | ||
update(expr, arrayFilters = [], condition = {}) { | ||
const fields = this.mutate(this.state, expr, arrayFilters, condition); | ||
const query = mkQuery(condition, this.queryOptions); | ||
const fields = Array.from(new Set( | ||
// apply mutations | ||
ensureArray(expr).flatMap(e => this.mutate(this.state, e, arrayFilters, query)))); | ||
// return if state is unchanged | ||
@@ -150,2 +149,4 @@ if (!fields.length) { | ||
} | ||
// maintain stability. | ||
fields.sort(); | ||
// set modified flag | ||
@@ -155,4 +156,3 @@ this.modified = true; | ||
let notifyCount = 0; | ||
this.selectors.forEach(selector => { | ||
const signal = this.signals.get(selector); | ||
for (const { selector, signal } of this.selectors.values()) { | ||
// record the number of listeners before notifying the selector. | ||
@@ -163,123 +163,5 @@ // upon notification a listener will be removed from the selector if it throws or is configured to run once. | ||
notifyCount += size; | ||
}); | ||
} | ||
return { modified: true, fields, notifyCount }; | ||
} | ||
} | ||
/** | ||
* Provides an observable interface for selecting customized views of the state. | ||
* Listeners can subscribe to be notified of changes in the view repeatedely or once. | ||
*/ | ||
export class Selector { | ||
/** | ||
* Construct a new selector | ||
* @param store Reference to the store object. | ||
* @param query Query object for checking conditions based on MongoDB filter query. | ||
* @param projection View of the state to select expressed as MongoDB projection query. | ||
*/ | ||
constructor(store, query, projection) { | ||
this.store = store; | ||
this.query = query; | ||
this.projection = projection; | ||
// iteration happens in insertion order. | ||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set | ||
this.listeners = new Set(); | ||
// listeners to be run once only also included in the main listener set. | ||
this.onceOnly = new Set(); | ||
// flag used to control when to use cached value. | ||
this.cached = false; | ||
} | ||
/** Returns the number of subscribers to this selector. */ | ||
get size() { | ||
return this.listeners.size; | ||
} | ||
/** | ||
* Returns the current state view subject to the selector criteria. | ||
* The value is only recomputed when the depedent fields in the criteria change. | ||
* | ||
* @returns {T | undefined} | ||
*/ | ||
getState() { | ||
// return cached if value has not changed since | ||
if (this.cached) | ||
return this.value; | ||
// update cached status | ||
this.cached = true; | ||
// project fields and freeze final value if query passes | ||
return (this.value = this.store.getState(this.projection, this.query)); | ||
} | ||
/** | ||
* Notify all listeners with the current value of the selector if different from the previous value. | ||
* If a listener throws an exception when notified, it is removed and does not receive future notifications. | ||
*/ | ||
notifyAll() { | ||
// only recompute if there are active listeners. | ||
if (!this.listeners.size) | ||
return; | ||
const prev = this.cached ? this.getState() : NONE; | ||
// reset the cache when notifyAll() is called. | ||
this.cached = false; | ||
// compute new value. | ||
const val = this.getState(); | ||
// No change so skip notifications. If a new subscriber was added after the last notification, it will be skipped here as well. | ||
// This is becuase the state has still not changed after it was added. For new subscribers to receive current state on subcsription, | ||
// they should be registered with {runImmediately: true}. | ||
if (isEqual(prev, val)) | ||
return; | ||
for (const f of this.listeners) { | ||
/*eslint-disable*/ | ||
try { | ||
f(val); | ||
} | ||
catch (_a) { | ||
// on error unsubscribe listener | ||
this.listeners.delete(f); | ||
} | ||
finally { | ||
// if runOnce, cleanup afterwards | ||
if (this.onceOnly.delete(f)) { | ||
this.listeners.delete(f); | ||
} | ||
} | ||
/*eslint-disable-enable*/ | ||
} | ||
} | ||
/** | ||
* Subscribe a listener to be notified about state updates. | ||
* | ||
* @param listener The function to receive new data on update. | ||
* @returns {Unsubscribe} Function to unsubscribe listener. | ||
*/ | ||
subscribe(listener, options) { | ||
// check if we are reregistering the same observer | ||
if (this.listeners.has(listener)) { | ||
throw new Error("Listener already subscribed."); | ||
} | ||
// setup to throw after first run. | ||
if (options && options.runOnce) { | ||
this.onceOnly.add(listener); | ||
} | ||
this.listeners.add(listener); | ||
const unsub = () => { | ||
this.onceOnly.delete(listener); | ||
this.listeners.delete(listener); | ||
}; | ||
if (options && options.runImmediately) { | ||
// immediately invoke | ||
const val = this.getState(); | ||
if (val !== undefined) { | ||
try { | ||
listener(val); | ||
} | ||
catch (e) { | ||
unsub(); | ||
throw e; | ||
} | ||
finally { | ||
if (this.onceOnly.has(listener)) | ||
unsub(); | ||
} | ||
} | ||
} | ||
return unsub; | ||
} | ||
} |
@@ -0,1 +1,2 @@ | ||
export * from "./_internal/selector"; | ||
export * from "./_internal/store"; |
@@ -5,13 +5,3 @@ import { Query } from "mingo"; | ||
import { UpdateExpression } from "mingo/updater"; | ||
/** Observes a selector for changes in store and optionally return updates to apply. */ | ||
export type Listener<T> = (data: T) => void; | ||
/** Unsbuscribe from receiving further notifications */ | ||
export type Unsubscribe = () => void; | ||
/** Options to pass on subscription. */ | ||
export interface SubscribeOptions { | ||
/** Immediately run the listener when register. Any error will bubble up immediately. */ | ||
readonly runImmediately?: boolean; | ||
/** Run only once. */ | ||
readonly runOnce?: boolean; | ||
} | ||
import { Selector } from "./selector"; | ||
/** Result from update operation which returns useful details. */ | ||
@@ -22,3 +12,3 @@ export interface UpdateResult { | ||
/** The fields in the state object that were modified. */ | ||
readonly fields?: string[]; | ||
readonly fields?: Readonly<string[]>; | ||
/** The number of listeners notified. */ | ||
@@ -41,6 +31,5 @@ readonly notifyCount?: number; | ||
*/ | ||
export declare class Store<T extends RawObject = RawObject> { | ||
export declare class Store<S extends RawObject = RawObject> { | ||
private readonly state; | ||
private readonly selectors; | ||
private readonly signals; | ||
private readonly queryOptions; | ||
@@ -50,5 +39,5 @@ private readonly mutate; | ||
private prevState; | ||
constructor(initialState: T, options?: UpdateOptions); | ||
constructor(initialState: S, options?: UpdateOptions); | ||
/** | ||
* Returns the current state as a frozen object subject to the given criteria. | ||
* Returns the current state as a frozen object subject to the given criteria.s | ||
* When no options are specified, returns the full state. | ||
@@ -60,3 +49,3 @@ * | ||
*/ | ||
getState<P extends T>(projection?: Record<keyof P, AnyVal> | RawObject, condition?: RawObject | Query): P | undefined; | ||
getState<P extends RawObject & S>(projection?: Record<keyof P, AnyVal> | RawObject, condition?: RawObject | Query): P | undefined; | ||
/** | ||
@@ -69,7 +58,7 @@ * Creates a new observable for a view of the state. | ||
*/ | ||
select<P extends RawObject>(projection: Record<keyof P, AnyVal> | RawObject, condition?: RawObject): Selector<P>; | ||
select<P extends RawObject = S>(projection: Record<keyof P, AnyVal> | RawObject, condition?: RawObject): Selector<P>; | ||
/** | ||
* Dispatches an update expression to mutate the state. Triggers a notification to relevant selectors only. | ||
* | ||
* @param {UpdateExpression} expr Update expression as a MongoDB update query. | ||
* @param {UpdateExpression | UpdateExpression[]} expr Update expression as a MongoDB update query. | ||
* @param {Array<RawObject>} arrayFilters Array filter expressions to filter elements to update. | ||
@@ -79,44 +68,3 @@ * @param {RawObject} condition Condition to check before applying update. | ||
*/ | ||
update(expr: UpdateExpression, arrayFilters?: RawObject[], condition?: RawObject): UpdateResult; | ||
update(expr: UpdateExpression | UpdateExpression[], arrayFilters?: RawObject[], condition?: RawObject): UpdateResult; | ||
} | ||
/** | ||
* Provides an observable interface for selecting customized views of the state. | ||
* Listeners can subscribe to be notified of changes in the view repeatedely or once. | ||
*/ | ||
export declare class Selector<T extends RawObject = RawObject> { | ||
private readonly store; | ||
private readonly query; | ||
private readonly projection; | ||
private readonly listeners; | ||
private readonly onceOnly; | ||
private value; | ||
private cached; | ||
/** | ||
* Construct a new selector | ||
* @param store Reference to the store object. | ||
* @param query Query object for checking conditions based on MongoDB filter query. | ||
* @param projection View of the state to select expressed as MongoDB projection query. | ||
*/ | ||
constructor(store: Store, query: Query, projection: Record<keyof T, AnyVal> | RawObject); | ||
/** Returns the number of subscribers to this selector. */ | ||
get size(): number; | ||
/** | ||
* Returns the current state view subject to the selector criteria. | ||
* The value is only recomputed when the depedent fields in the criteria change. | ||
* | ||
* @returns {T | undefined} | ||
*/ | ||
getState(): T | undefined; | ||
/** | ||
* Notify all listeners with the current value of the selector if different from the previous value. | ||
* If a listener throws an exception when notified, it is removed and does not receive future notifications. | ||
*/ | ||
notifyAll(): void; | ||
/** | ||
* Subscribe a listener to be notified about state updates. | ||
* | ||
* @param listener The function to receive new data on update. | ||
* @returns {Unsubscribe} Function to unsubscribe listener. | ||
*/ | ||
subscribe(listener: Listener<T>, options?: SubscribeOptions): Unsubscribe; | ||
} |
@@ -0,1 +1,2 @@ | ||
export * from "./_internal/selector"; | ||
export * from "./_internal/store"; |
{ | ||
"name": "adaka", | ||
"version": "0.0.10", | ||
"version": "0.0.11", | ||
"description": "High-precision state management using MongoDB query language.", | ||
@@ -22,3 +22,3 @@ "main": "./dist/cjs/index.js", | ||
"dependencies": { | ||
"mingo": "^6.4.12" | ||
"mingo": "^6.4.13" | ||
}, | ||
@@ -25,0 +25,0 @@ "devDependencies": {}, |
56653
16
1065
Updatedmingo@^6.4.13