Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@rx-signals/store

Package Overview
Dependencies
Maintainers
1
Versions
94
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@rx-signals/store - npm Package Compare versions

Comparing version 3.0.0-rc33 to 3.0.0-rc34

70

dist/cjs/effect-signals-factory.js

@@ -20,2 +20,3 @@ "use strict";

exports.isCombinedEffectResultInSuccessState = isCombinedEffectResultInSuccessState;
const isCompletedSuccess = (value) => value.completed;
const getInputSignalIds = (nameExtension) => ({

@@ -32,8 +33,12 @@ input: (0, store_utils_1.getDerivedId)(`${nameExtension !== null && nameExtension !== void 0 ? nameExtension : ''}_input`),

successes: (0, store_utils_1.getEventId)(`${nameExtension !== null && nameExtension !== void 0 ? nameExtension : ''}_successes`),
completedSuccesses: (0, store_utils_1.getEventId)(`${nameExtension !== null && nameExtension !== void 0 ? nameExtension : ''}_completedSuccesses`),
});
const NO_VALUE_TRIGGERED_INPUT = '$INTERNAL_NV_TI$';
const getIsNewInput = (effectInputEquals) => ([input, resultState, token]) => token !== resultState.resultToken ||
(0, store_utils_1.isNoValueType)(resultState.resultInput) ||
!effectInputEquals(input, resultState.resultInput);
const getEffectBuilder = (config) => {
var _a;
const effectId = (0, store_utils_1.getEffectId)();
const internalResultEffect = (input, store, previousInput, previousResult) => store.getEffect(effectId).pipe((0, rxjs_1.take)(1), (0, rxjs_1.switchMap)(effect => {
const wrappedResultEffect = (input, store, previousInput, previousResult) => store.getEffect(effectId).pipe((0, rxjs_1.take)(1), (0, rxjs_1.switchMap)(effect => {
try {

@@ -43,3 +48,3 @@ const wrappedEffect = config.wrappedEffectGetter

: effect;
return wrappedEffect(input, store, previousInput, previousResult).pipe((0, rxjs_1.take)(1));
return wrappedEffect(input, store, previousInput, previousResult);
}

@@ -50,3 +55,34 @@ catch (error) {

}));
const internalResultEffect = (input, store, previousInput, previousResult) => new rxjs_1.Observable(subscriber => {
let currentResult = store_utils_1.NO_VALUE;
const subscription = wrappedResultEffect(input, store, previousInput, previousResult).subscribe({
next: result => {
currentResult = result;
subscriber.next({
result,
completed: false,
});
},
complete: () => {
if ((0, store_utils_1.isNotNoValueType)(currentResult)) {
subscriber.next({
result: currentResult,
completed: true,
});
}
subscriber.complete();
currentResult = store_utils_1.NO_VALUE;
},
error: e => {
subscriber.error(e);
currentResult = store_utils_1.NO_VALUE;
},
});
return () => {
subscription.unsubscribe();
currentResult = store_utils_1.NO_VALUE;
};
});
const effectInputEquals = (_a = config.effectInputEquals) !== null && _a !== void 0 ? _a : ((a, b) => a === b);
const isNewInput = getIsNewInput(effectInputEquals);
const inIds = getInputSignalIds(config.nameExtension);

@@ -66,3 +102,5 @@ const outIds = getOutputSignalIds(config.nameExtension);

store.addDerivedState(resultBehavior, store.getEventStream(resultEvent), {
result: initialResult,
result: (0, store_utils_1.isNotNoValueType)(initialResult)
? { result: initialResult, completed: true }
: store_utils_1.NO_VALUE,
resultInput: store_utils_1.NO_VALUE,

@@ -74,2 +112,3 @@ resultToken: null,

store.addDerivedState(triggeredInputBehavior, store.getEventStream(triggeredInputEvent), NO_VALUE_TRIGGERED_INPUT);
store.addEventSource(outIds.completedSuccesses, store.getEventStream(outIds.successes).pipe((0, rxjs_1.filter)(isCompletedSuccess)));
// It is important to setup the combined observable as behavior,

@@ -88,5 +127,3 @@ // because a simple shareReplay (even with refCount) could create a memory leak!!!

: combined.pipe((0, rxjs_1.debounceTime)(config.effectDebounceTime));
store.add4TypedEventSource(resultEvent, triggeredInputEvent, outIds.errors, outIds.successes, eventSourceInput.pipe((0, rxjs_1.filter)(([input, resultState, token]) => token !== resultState.resultToken ||
resultState.resultInput === store_utils_1.NO_VALUE ||
!effectInputEquals(input, resultState.resultInput)), (0, rxjs_1.switchMap)(([input, resultState, token, triggeredInput]) => config.withTrigger && input !== triggeredInput
store.add4TypedEventSource(resultEvent, triggeredInputEvent, outIds.errors, outIds.successes, eventSourceInput.pipe((0, rxjs_1.filter)(isNewInput), (0, rxjs_1.switchMap)(([input, resultState, token, triggeredInput]) => config.withTrigger && input !== triggeredInput
? store.getEventStream(inIds.trigger).pipe((0, rxjs_1.map)(() => ({

@@ -96,5 +133,6 @@ type: triggeredInputEvent,

})))
: internalResultEffect(input, store, resultState.resultInput, resultState.result).pipe((0, rxjs_1.switchMap)(result => (0, rxjs_1.of)({
: internalResultEffect(input, store, resultState.resultInput, (0, store_utils_1.isNotNoValueType)(resultState.result) ? resultState.result.result : store_utils_1.NO_VALUE).pipe((0, rxjs_1.switchMap)((result) => (0, rxjs_1.of)({
type: resultEvent,
event: {
// InternalResultType<IT, InternalEffectResult<RT>>
result,

@@ -107,6 +145,10 @@ resultInput: input,

event: {
result,
// EffectSuccess<IT, RT>
result: result.result,
resultInput: input,
previousInput: resultState.resultInput,
previousResult: resultState.result,
previousResult: (0, store_utils_1.isNotNoValueType)(resultState.result)
? resultState.result.result
: store_utils_1.NO_VALUE,
completed: result.completed,
},

@@ -132,14 +174,16 @@ })), (0, rxjs_1.catchError)(error => (0, rxjs_1.of)({

resultState.resultInput === store_utils_1.NO_VALUE ||
!effectInputEquals(input, resultState.resultInput))
!effectInputEquals(input, resultState.resultInput) ||
((0, store_utils_1.isNotNoValueType)(resultState.result) && !resultState.result.completed))
: ([input, resultState, token]) => token !== resultState.resultToken ||
resultState.resultInput === store_utils_1.NO_VALUE ||
!effectInputEquals(input, resultState.resultInput);
!effectInputEquals(input, resultState.resultInput) ||
((0, store_utils_1.isNotNoValueType)(resultState.result) && !resultState.result.completed);
store.addDerivedState(outIds.combined, combined.pipe((0, rxjs_1.map)(([input, resultState, token, triggeredInput]) => getIsPending([input, resultState, token, triggeredInput])
? {
currentInput: input,
result: resultState.result,
result: (0, store_utils_1.isNotNoValueType)(resultState.result) ? resultState.result.result : store_utils_1.NO_VALUE,
resultInput: resultState.resultInput,
resultPending: true,
}
: Object.assign({ currentInput: input, result: resultState.result, resultInput: resultState.resultInput, resultError: resultState.resultError, resultPending: false }, (resultState.resultError ? { resultError: resultState.resultError } : {})))), config.initialResultGetter
: Object.assign({ currentInput: input, result: (0, store_utils_1.isNotNoValueType)(resultState.result) ? resultState.result.result : store_utils_1.NO_VALUE, resultInput: resultState.resultInput, resultError: resultState.resultError, resultPending: false }, (resultState.resultError ? { resultError: resultState.resultError } : {})))), config.initialResultGetter
? {

@@ -146,0 +190,0 @@ currentInput: store_utils_1.NO_VALUE,

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getEntityEditSignalsFactory = void 0;
exports.getEntityEditSignalsFactory = exports.shallowEquals = void 0;
const rxjs_1 = require("rxjs");

@@ -10,5 +10,32 @@ const effect_signals_factory_1 = require("./effect-signals-factory");

const validated_input_with_result_signals_factory_1 = require("./validated-input-with-result-signals-factory");
const isStringRecord = (value) => value && typeof value === 'object' && !Array.isArray(value);
/**
* A shallow equals function that performs shallow equals on records or arrays
* and else falls back to strict equals.
*/
const shallowEquals = (a, b) => {
var _a;
if (a === b) {
return true;
}
if (isStringRecord(a) && isStringRecord(b)) {
return ((_a = Object.entries(a).find(([k, v]) => v !== b[k])) !== null && _a !== void 0 ? _a : null) === null;
}
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
return a === b;
};
exports.shallowEquals = shallowEquals;
/**
* Generic function to create a specific {@link EntityEditFactory}.
* CAVE: This factory is work in progress and unit tests, as well as documentation is still missing!
* CAVE: This factory is work in progress and unit tests are still missing!
*/

@@ -35,3 +62,4 @@ const getEntityEditSignalsFactory = () => (0, effect_signals_factory_1.getEffectSignalsFactory)() // model-fetch (fetching the edit entity)

},
onSaveSuccessEvent: config.onSaveSuccessEvent,
onSaveCompletedEvent: config.onSaveCompletedEvent,
entityEquals: config.entityEquals,
}))

@@ -47,11 +75,13 @@ .extendSetup((store, _, output, config) => {

edit,
model: modelWithDefault.model,
entity: modelWithDefault.model,
validation: (0, store_utils_1.isNotNoValueType)(edit.validationResult) ? edit.validationResult : null,
loading: pending || edit.resultPending,
disabled: pending || edit.resultPending || edit.validationPending || !edit.isValid,
changed: modelWithDefault.model !== modelWithDefault.default,
changed: config.entityEquals
? config.entityEquals(modelWithDefault.model, modelWithDefault.default)
: (0, exports.shallowEquals)(modelWithDefault.model, modelWithDefault.default),
}))));
if (config.onSaveSuccessEvent) {
if (config.onSaveCompletedEvent) {
console.log('connecting');
store.connectObservable(store.getEventStream(output.resultSuccesses).pipe((0, rxjs_1.map)(() => undefined)), config.onSaveSuccessEvent);
store.connectObservable(store.getEventStream(output.resultCompletedSuccesses).pipe((0, rxjs_1.map)(() => undefined)), config.onSaveCompletedEvent);
}

@@ -75,2 +105,3 @@ })

successes: ids.successes,
completedSuccesses: ids.completedSuccesses,
errors: ids.errors,

@@ -82,4 +113,6 @@ },

validationSuccesses: ids.validationSuccesses,
validationCompletedSuccesses: ids.validationCompletedSuccesses,
validationErrors: ids.validationErrors,
resultSuccesses: ids.resultSuccesses,
resultCompletedSuccesses: ids.resultCompletedSuccesses,
resultErrors: ids.resultErrors,

@@ -92,5 +125,5 @@ },

validation: ids.validation,
result: ids.result,
save: ids.result,
}));
exports.getEntityEditSignalsFactory = getEntityEditSignalsFactory;
//# sourceMappingURL=entity-edit-signals-factory.js.map

9

dist/cjs/validated-input-with-result-signals-factory.js

@@ -7,5 +7,8 @@ "use strict";

const store_utils_1 = require("./store-utils");
const isResultInputGetterInput = (c) => c.resultInput !== store_utils_1.NO_VALUE && c.result !== store_utils_1.NO_VALUE && c.currentInput === c.resultInput;
const isResultInputGetterInput = (c) => !c.resultPending &&
c.resultInput !== store_utils_1.NO_VALUE &&
c.result !== store_utils_1.NO_VALUE &&
c.currentInput === c.resultInput;
const resultInputGetter = (store, validationBehaviorId, isValidationResultValid) => store.getBehavior(validationBehaviorId).pipe((0, rxjs_1.filter)(isResultInputGetterInput), (0, rxjs_1.filter)(c => isValidationResultValid(c.result)), (0, rxjs_1.map)(c => c.resultInput), (0, rxjs_1.distinctUntilChanged)());
const mapBehaviors = ([v, r], isValidationResultValid) => (Object.assign(Object.assign({ currentInput: v.currentInput, validationPending: v.resultPending, validatedInput: v.resultInput, validationResult: v.result, isValid: v.result !== store_utils_1.NO_VALUE ? isValidationResultValid(v.result) : false, resultPending: r.resultPending, resultInput: r.resultInput }, (r.resultError ? { resultError: r.resultError } : {})), { result: r.result }));
const mapBehaviors = ([v, r], isValidationResultValid) => (Object.assign(Object.assign({ currentInput: v.currentInput, validationPending: v.resultPending, validatedInput: v.resultInput, validationResult: v.result, isValid: !v.resultPending && v.result !== store_utils_1.NO_VALUE ? isValidationResultValid(v.result) : false, resultPending: r.resultPending, resultInput: r.resultInput }, (r.resultError ? { resultError: r.resultError } : {})), { result: r.result }));
const setupCombinedBehavior = (store, outIds, id, isValidationResultValid, initialResultGetter) => {

@@ -80,6 +83,8 @@ store.addDerivedState(id, (0, rxjs_1.combineLatest)([

validationSuccesses: output.conflicts1.successes,
validationCompletedSuccesses: output.conflicts1.completedSuccesses,
resultErrors: output.conflicts2.errors,
resultSuccesses: output.conflicts2.successes,
resultCompletedSuccesses: output.conflicts2.completedSuccesses,
}));
exports.getValidatedInputWithResultSignalsFactory = getValidatedInputWithResultSignalsFactory;
//# sourceMappingURL=validated-input-with-result-signals-factory.js.map

@@ -16,4 +16,6 @@ import { Signals, SignalsFactory } from './signals-factory';

currentInput: Input | NoValueType;
/** The current result,
* or NO_VALUE, if no result was received yet or input led to error
/**
* The current result,
* or NO_VALUE if no result was received yet,
* or the effect produced an error
*/

@@ -23,11 +25,16 @@ result: Result | NoValueType;

* The input that produced the current result,
* or NO_VALUE, initial result or no result received yet */
* or NO_VALUE, if initial result or no result received yet */
resultInput: Input | NoValueType;
/**
* In case the resultInput led to an error (result === NO_VALUE in that case) */
* In case the effect led to an error (result === NO_VALUE in that case), else undefined */
resultError?: any;
/**
* Indicates whether the effect is currently running.
* In case of a factory without trigger, this will be true whenever
* currentInput !== resultInput, or whenever an invalidation event has been sent
* In case of a factory without trigger, this will be true whenever one or multiple
* of the following conditions is met:
* currentInput !== resultInput,
* or an invalidation event has been sent,
* or the effect has sent a result, but has not yet completed.
* In case of a factory with result-trigger, in addition to the previous
* criteria, a trigegr event must have been received.
*/

@@ -81,2 +88,7 @@ resultPending: boolean;

* Value-type for success events produced by {@link EffectSignals}.
* In case the effect completes after one result, two success events will
* be dispatched, one with completed false and one with completed true.
* This is to handle cases where the effect might send multiple results
* before completing. Thus, if an effect never completes, all success event
* will have completed false.
*

@@ -91,8 +103,21 @@ * @template Input - specifies the input type for the effect

resultInput: Input;
/** the input of the previous result, or NO_VALUE */
/** the input of the previous completed result, or NO_VALUE */
previousInput: Input | NoValueType;
/** the previous result, or NO_VALUE */
/** the previous completed result, or NO_VALUE */
previousResult: Result | NoValueType;
/** has the effect for the given resultInput completed */
completed: boolean;
};
/**
* Value-type for completed success events produced by {@link EffectSignals}.
* In contrast to {@link EffectSuccess} events with this type are only dispatched,
* if the effect has completed, hence it will never be fired for effects that never complete.
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
*/
export type EffectCompletedSuccess<Input, Result> = Omit<EffectSuccess<Input, Result>, 'completed'> & {
completed: true;
};
/**
* Type specifying the input {@link EffectSignals} (the corresponding signal-sources are NOT added to the store

@@ -110,3 +135,6 @@ * by the EffectSignals-setup, but by whoever uses the signals, e.g. by extendSetup or fmap or just using dispatch).

invalidate: EventId<undefined>;
/** Event that can be dispatched to trigger the given effect. This event has only meaning, if withTrigger is configured (see EffectConfiguration) */
/**
* Event that can be dispatched to trigger the given effect.
* This event has only meaning, if withTrigger is configured (see EffectConfiguration),
* else dispatching it is a no-op. */
trigger: EventId<undefined>;

@@ -116,3 +144,4 @@ };

* Type specifying the output {@link EffectSignals} (signals produced by EffectSignals).
* The {@link EffectSignalsFactory} takes care that subscribing error- or success-events keeps the effect itself lazy (hence only subscribing the combined behavior will subscribe the effect itself).
* The {@link EffectSignalsFactory} takes care that subscribing error- or success-events keeps
* the effect itself lazy (hence only subscribing the combined behavior will subscribe the effect itself).
*

@@ -125,3 +154,3 @@ * @template Input - specifies the input type for the effect

combined: DerivedId<CombinedEffectResult<Input, Result>>;
/** Convenience behavior derived from combined-behavior, representing only success states */
/** Convenience behavior derived from combined-behavior, representing only completed success states */
result: DerivedId<CombinedEffectResultInSuccessState<Input, Result>>;

@@ -134,2 +163,4 @@ /** Convenience behavior derived from combined-behavior, representing only pending state */

successes: EventId<EffectSuccess<Input, Result>>;
/** Produced success events */
completedSuccesses: EventId<EffectCompletedSuccess<Input, Result>>;
};

@@ -154,6 +185,6 @@ /**

* errors event stream will NOT subscribe the result behavior (see requirement 1).
* 3.) In addition to the result behavior, also an event stream for EffectSuccess<Input, Result> is provided. This is important
* 3.) In addition to the result behavior, also an event-stream for EffectSuccess<Input, Result> is provided. This is important
* in cases where an effect success should be used to trigger something else (e.g. close a popup), but you cannot use the result
* behavior, because it would mean to always subscribe the result. In contrast, subscription of the success event stream will NOT
* subscribe the result behavior.
* subscribe the result behavior. (the same holds true for the completedSuccesses event-stream)
* ```

@@ -190,3 +221,3 @@ *

/**
* Type specifying the {@link SignalsBuild} function for {@link EffectSignals}, hence a function taking an {@link EffectConfiguration} and producing EffectSignals.
* Type specifying the {@link SignalsBuild} function for {@link EffectSignals}, hence a function taking a {@link EffectConfiguration} and producing EffectSignals.
*

@@ -193,0 +224,0 @@ * @template Input - specifies the input type for the effect

@@ -1,2 +0,2 @@

import { catchError, combineLatest, debounceTime, filter, map, of, switchMap, take, throwError, } from 'rxjs';
import { Observable, catchError, combineLatest, debounceTime, filter, map, of, switchMap, take, throwError, } from 'rxjs';
import { SignalsFactory } from './signals-factory';

@@ -15,2 +15,3 @@ import { NO_VALUE, getDerivedId, getEffectId, getEventId, getStateId, isNoValueType, isNotNoValueType, } from './store-utils';

export const isCombinedEffectResultInSuccessState = (cer) => !cer.resultError && !cer.resultPending && isNotNoValueType(cer.result);
const isCompletedSuccess = (value) => value.completed;
const getInputSignalIds = (nameExtension) => ({

@@ -27,8 +28,12 @@ input: getDerivedId(`${nameExtension !== null && nameExtension !== void 0 ? nameExtension : ''}_input`),

successes: getEventId(`${nameExtension !== null && nameExtension !== void 0 ? nameExtension : ''}_successes`),
completedSuccesses: getEventId(`${nameExtension !== null && nameExtension !== void 0 ? nameExtension : ''}_completedSuccesses`),
});
const NO_VALUE_TRIGGERED_INPUT = '$INTERNAL_NV_TI$';
const getIsNewInput = (effectInputEquals) => ([input, resultState, token]) => token !== resultState.resultToken ||
isNoValueType(resultState.resultInput) ||
!effectInputEquals(input, resultState.resultInput);
const getEffectBuilder = (config) => {
var _a;
const effectId = getEffectId();
const internalResultEffect = (input, store, previousInput, previousResult) => store.getEffect(effectId).pipe(take(1), switchMap(effect => {
const wrappedResultEffect = (input, store, previousInput, previousResult) => store.getEffect(effectId).pipe(take(1), switchMap(effect => {
try {

@@ -38,3 +43,3 @@ const wrappedEffect = config.wrappedEffectGetter

: effect;
return wrappedEffect(input, store, previousInput, previousResult).pipe(take(1));
return wrappedEffect(input, store, previousInput, previousResult);
}

@@ -45,3 +50,34 @@ catch (error) {

}));
const internalResultEffect = (input, store, previousInput, previousResult) => new Observable(subscriber => {
let currentResult = NO_VALUE;
const subscription = wrappedResultEffect(input, store, previousInput, previousResult).subscribe({
next: result => {
currentResult = result;
subscriber.next({
result,
completed: false,
});
},
complete: () => {
if (isNotNoValueType(currentResult)) {
subscriber.next({
result: currentResult,
completed: true,
});
}
subscriber.complete();
currentResult = NO_VALUE;
},
error: e => {
subscriber.error(e);
currentResult = NO_VALUE;
},
});
return () => {
subscription.unsubscribe();
currentResult = NO_VALUE;
};
});
const effectInputEquals = (_a = config.effectInputEquals) !== null && _a !== void 0 ? _a : ((a, b) => a === b);
const isNewInput = getIsNewInput(effectInputEquals);
const inIds = getInputSignalIds(config.nameExtension);

@@ -61,3 +97,5 @@ const outIds = getOutputSignalIds(config.nameExtension);

store.addDerivedState(resultBehavior, store.getEventStream(resultEvent), {
result: initialResult,
result: isNotNoValueType(initialResult)
? { result: initialResult, completed: true }
: NO_VALUE,
resultInput: NO_VALUE,

@@ -69,2 +107,3 @@ resultToken: null,

store.addDerivedState(triggeredInputBehavior, store.getEventStream(triggeredInputEvent), NO_VALUE_TRIGGERED_INPUT);
store.addEventSource(outIds.completedSuccesses, store.getEventStream(outIds.successes).pipe(filter(isCompletedSuccess)));
// It is important to setup the combined observable as behavior,

@@ -83,5 +122,3 @@ // because a simple shareReplay (even with refCount) could create a memory leak!!!

: combined.pipe(debounceTime(config.effectDebounceTime));
store.add4TypedEventSource(resultEvent, triggeredInputEvent, outIds.errors, outIds.successes, eventSourceInput.pipe(filter(([input, resultState, token]) => token !== resultState.resultToken ||
resultState.resultInput === NO_VALUE ||
!effectInputEquals(input, resultState.resultInput)), switchMap(([input, resultState, token, triggeredInput]) => config.withTrigger && input !== triggeredInput
store.add4TypedEventSource(resultEvent, triggeredInputEvent, outIds.errors, outIds.successes, eventSourceInput.pipe(filter(isNewInput), switchMap(([input, resultState, token, triggeredInput]) => config.withTrigger && input !== triggeredInput
? store.getEventStream(inIds.trigger).pipe(map(() => ({

@@ -91,5 +128,6 @@ type: triggeredInputEvent,

})))
: internalResultEffect(input, store, resultState.resultInput, resultState.result).pipe(switchMap(result => of({
: internalResultEffect(input, store, resultState.resultInput, isNotNoValueType(resultState.result) ? resultState.result.result : NO_VALUE).pipe(switchMap((result) => of({
type: resultEvent,
event: {
// InternalResultType<IT, InternalEffectResult<RT>>
result,

@@ -102,6 +140,10 @@ resultInput: input,

event: {
result,
// EffectSuccess<IT, RT>
result: result.result,
resultInput: input,
previousInput: resultState.resultInput,
previousResult: resultState.result,
previousResult: isNotNoValueType(resultState.result)
? resultState.result.result
: NO_VALUE,
completed: result.completed,
},

@@ -127,14 +169,16 @@ })), catchError(error => of({

resultState.resultInput === NO_VALUE ||
!effectInputEquals(input, resultState.resultInput))
!effectInputEquals(input, resultState.resultInput) ||
(isNotNoValueType(resultState.result) && !resultState.result.completed))
: ([input, resultState, token]) => token !== resultState.resultToken ||
resultState.resultInput === NO_VALUE ||
!effectInputEquals(input, resultState.resultInput);
!effectInputEquals(input, resultState.resultInput) ||
(isNotNoValueType(resultState.result) && !resultState.result.completed);
store.addDerivedState(outIds.combined, combined.pipe(map(([input, resultState, token, triggeredInput]) => getIsPending([input, resultState, token, triggeredInput])
? {
currentInput: input,
result: resultState.result,
result: isNotNoValueType(resultState.result) ? resultState.result.result : NO_VALUE,
resultInput: resultState.resultInput,
resultPending: true,
}
: Object.assign({ currentInput: input, result: resultState.result, resultInput: resultState.resultInput, resultError: resultState.resultError, resultPending: false }, (resultState.resultError ? { resultError: resultState.resultError } : {})))), config.initialResultGetter
: Object.assign({ currentInput: input, result: isNotNoValueType(resultState.result) ? resultState.result.result : NO_VALUE, resultInput: resultState.resultInput, resultError: resultState.resultError, resultPending: false }, (resultState.resultError ? { resultError: resultState.resultError } : {})))), config.initialResultGetter
? {

@@ -141,0 +185,0 @@ currentInput: NO_VALUE,

@@ -7,35 +7,114 @@ import { CombinedEffectResult, EffectOutputSignals } from './effect-signals-factory';

import { ValidatedInputWithResult, ValidatedInputWithResultOutput } from './validated-input-with-result-signals-factory';
/**
* A shallow equals function that performs shallow equals on records or arrays
* and else falls back to strict equals.
*/
export declare const shallowEquals: <T>(a: T, b: T) => boolean;
/**
* Value-type for the derived model behavior produced by {@link EntityEditFactory} Signals.
*
* @template Entity - specifies the entity type
* @template IdType - specifies the entity-id-type
* @template ValidationErrorType - specifies the error-type for failed validations
*/
export type EntityEditModel<Entity, IdType, ValidationErrorType> = {
/**
* The {@link CombinedEffectResult} for the load effect
*/
load: CombinedEffectResult<IdType | null, Entity>;
/**
* The {@link ValidatedInputWithResult} for validation and result effects
*/
edit: ValidatedInputWithResult<Entity, ModelValidationResult<Entity, ValidationErrorType>, IdType>;
model: Entity | undefined;
/**
* The current Entity state
*/
entity: Entity;
/**
* Convenience property for the current {@link ModelValidationResult}, taken from the edit property.
*/
validation: ModelValidationResult<Entity, ValidationErrorType>;
/**
* true, if either:
* the load effect is pending, or
* the result effect is pending
*/
loading: boolean;
/**
* true, if either:
* the load effect is pending, or
* the result effect is pending, or
* the validation effect is pending, or
* edit.isValid is false (the current validation result represents invalid entity state)
*/
disabled: boolean;
/**
* true, if the entity does not equal the default entity, with respect to configured
* equals function (defaults to {@link shallowEquals}).
*/
changed: boolean;
};
/**
* Type specifying the input signals for entity edit signals, hence a combination of
* {@link ModelInputSignals}, an id-behavior (load) and a save event.
*/
export type EntityEditInput<Entity, IdType> = ModelInputSignals<Entity> & {
/** input for the load effect */
load: DerivedId<IdType | null>;
/** input for the save effect */
save: EventId<undefined>;
};
/**
* Type specifying the output signals for entity edit signals,
*/
export type EntityEditOutput<Entity, IdType, ValidationErrorType> = {
/** {@link EffectOutputSignals} for the load effect */
load: EffectOutputSignals<IdType | null, Entity>;
/** {@link ValidatedInputWithResultOutput} for the validation and result effects */
edit: ValidatedInputWithResultOutput<Entity, ModelValidationResult<Entity, ValidationErrorType>, IdType>;
/** derived bahavior for the {@link EntityEditModel} */
model: DerivedId<EntityEditModel<Entity, IdType, ValidationErrorType>>;
};
/**
* Type specifying the configuration for {@link EntityEditFactory},
*/
export type EntityEditConfiguration<Entity> = {
/** the initial default entity, hence the initial entity state before the load effect. */
defaultEntity: Entity;
onSaveSuccessEvent?: EventId<undefined>;
/**
* Used to determine if the current entity state differs from default.
* Note that the default might differ from configured defaultEntity!
* A successful load effect will set a new default, as well as dispatching
* a setAsDefault event.
* This funtion defaults to {@link shallowEquals}, if not specified.
*/
entityEquals?: (a: Entity, b: Entity) => boolean;
/** optional event id for an event that should be dispatched on all save completed events */
onSaveCompletedEvent?: EventId<undefined>;
/** specifies whether the load behavior should be subscribed eagerly (defaults to false) */
eagerLoadSubscription?: boolean;
};
/**
* Type specifying the effects for {@link EntityEditFactory},
*/
export type EntityEditEffects<Entity, IdType, ValidationErrorType> = {
/** effect that takes entity id or null and returns a corresponding entity (which sets the default model) */
load: EffectId<IdType | null, Entity>;
/** effect that takes an entity and returns the corresponding {@link ModelValidationResult} */
validation: EffectId<Entity, ModelValidationResult<Entity, ValidationErrorType>>;
result: EffectId<Entity, IdType>;
/** effect that takes an entity and returns the id of the persisted entity */
save: EffectId<Entity, IdType>;
};
/**
* This type specifies a {@link SignalsFactory} producing signals to load, edit and persist an entity.
*
* @template Entity - specifies the entity type
* @template IdType - specifies the entity-id-type
* @template ValidationErrorType - specifies the error-type for failed validations
*/
export type EntityEditFactory<Entity, IdType, ValidationErrorType> = SignalsFactory<EntityEditInput<Entity, IdType>, EntityEditOutput<Entity, IdType, ValidationErrorType>, EntityEditConfiguration<Entity>, EntityEditEffects<Entity, IdType, ValidationErrorType>>;
/**
* Generic function to create a specific {@link EntityEditFactory}.
* CAVE: This factory is work in progress and unit tests, as well as documentation is still missing!
* CAVE: This factory is work in progress and unit tests are still missing!
*/
export declare const getEntityEditSignalsFactory: <Entity, IdType = number, ValidationErrorType = string>() => EntityEditFactory<Entity, IdType, ValidationErrorType>;

@@ -7,5 +7,31 @@ import { combineLatest, map } from 'rxjs';

import { getValidatedInputWithResultSignalsFactory, } from './validated-input-with-result-signals-factory';
const isStringRecord = (value) => value && typeof value === 'object' && !Array.isArray(value);
/**
* A shallow equals function that performs shallow equals on records or arrays
* and else falls back to strict equals.
*/
export const shallowEquals = (a, b) => {
var _a;
if (a === b) {
return true;
}
if (isStringRecord(a) && isStringRecord(b)) {
return ((_a = Object.entries(a).find(([k, v]) => v !== b[k])) !== null && _a !== void 0 ? _a : null) === null;
}
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
return a === b;
};
/**
* Generic function to create a specific {@link EntityEditFactory}.
* CAVE: This factory is work in progress and unit tests, as well as documentation is still missing!
* CAVE: This factory is work in progress and unit tests are still missing!
*/

@@ -32,3 +58,4 @@ export const getEntityEditSignalsFactory = () => getEffectSignalsFactory() // model-fetch (fetching the edit entity)

},
onSaveSuccessEvent: config.onSaveSuccessEvent,
onSaveCompletedEvent: config.onSaveCompletedEvent,
entityEquals: config.entityEquals,
}))

@@ -44,11 +71,13 @@ .extendSetup((store, _, output, config) => {

edit,
model: modelWithDefault.model,
entity: modelWithDefault.model,
validation: isNotNoValueType(edit.validationResult) ? edit.validationResult : null,
loading: pending || edit.resultPending,
disabled: pending || edit.resultPending || edit.validationPending || !edit.isValid,
changed: modelWithDefault.model !== modelWithDefault.default,
changed: config.entityEquals
? config.entityEquals(modelWithDefault.model, modelWithDefault.default)
: shallowEquals(modelWithDefault.model, modelWithDefault.default),
}))));
if (config.onSaveSuccessEvent) {
if (config.onSaveCompletedEvent) {
console.log('connecting');
store.connectObservable(store.getEventStream(output.resultSuccesses).pipe(map(() => undefined)), config.onSaveSuccessEvent);
store.connectObservable(store.getEventStream(output.resultCompletedSuccesses).pipe(map(() => undefined)), config.onSaveCompletedEvent);
}

@@ -72,2 +101,3 @@ })

successes: ids.successes,
completedSuccesses: ids.completedSuccesses,
errors: ids.errors,

@@ -79,4 +109,6 @@ },

validationSuccesses: ids.validationSuccesses,
validationCompletedSuccesses: ids.validationCompletedSuccesses,
validationErrors: ids.validationErrors,
resultSuccesses: ids.resultSuccesses,
resultCompletedSuccesses: ids.resultCompletedSuccesses,
resultErrors: ids.resultErrors,

@@ -89,4 +121,4 @@ },

validation: ids.validation,
result: ids.result,
save: ids.result,
}));
//# sourceMappingURL=entity-edit-signals-factory.js.map

@@ -1,2 +0,2 @@

import { CombinedEffectResultInSuccessState, EffectError, EffectSuccess } from './effect-signals-factory';
import { CombinedEffectResultInSuccessState, EffectCompletedSuccess, EffectError, EffectSuccess } from './effect-signals-factory';
import { SignalsFactory } from './signals-factory';

@@ -20,3 +20,3 @@ import { DerivedId, EffectId, EventId, NoValueType } from './store-utils';

validationResult: ValidationResult | NoValueType;
/** whether the current validationResult represents a valid state (false, if current validationResult is NO_VALUE) */
/** only true if validationResult represents a valid state AND validationPending is false */
isValid: boolean;

@@ -48,4 +48,6 @@ /** indicates whether the result-effect is currently running */

validationSuccesses: EventId<EffectSuccess<Input, ValidationResult>>;
validationCompletedSuccesses: EventId<EffectCompletedSuccess<Input, ValidationResult>>;
resultErrors: EventId<EffectError<Input>>;
resultSuccesses: EventId<EffectSuccess<Input, Result>>;
resultCompletedSuccesses: EventId<EffectCompletedSuccess<Input, Result>>;
};

@@ -52,0 +54,0 @@ /**

import { combineLatest, distinctUntilChanged, filter, map, startWith } from 'rxjs';
import { getEffectSignalsFactory, } from './effect-signals-factory';
import { NO_VALUE, getDerivedId, } from './store-utils';
const isResultInputGetterInput = (c) => c.resultInput !== NO_VALUE && c.result !== NO_VALUE && c.currentInput === c.resultInput;
const isResultInputGetterInput = (c) => !c.resultPending &&
c.resultInput !== NO_VALUE &&
c.result !== NO_VALUE &&
c.currentInput === c.resultInput;
const resultInputGetter = (store, validationBehaviorId, isValidationResultValid) => store.getBehavior(validationBehaviorId).pipe(filter(isResultInputGetterInput), filter(c => isValidationResultValid(c.result)), map(c => c.resultInput), distinctUntilChanged());
const mapBehaviors = ([v, r], isValidationResultValid) => (Object.assign(Object.assign({ currentInput: v.currentInput, validationPending: v.resultPending, validatedInput: v.resultInput, validationResult: v.result, isValid: v.result !== NO_VALUE ? isValidationResultValid(v.result) : false, resultPending: r.resultPending, resultInput: r.resultInput }, (r.resultError ? { resultError: r.resultError } : {})), { result: r.result }));
const mapBehaviors = ([v, r], isValidationResultValid) => (Object.assign(Object.assign({ currentInput: v.currentInput, validationPending: v.resultPending, validatedInput: v.resultInput, validationResult: v.result, isValid: !v.resultPending && v.result !== NO_VALUE ? isValidationResultValid(v.result) : false, resultPending: r.resultPending, resultInput: r.resultInput }, (r.resultError ? { resultError: r.resultError } : {})), { result: r.result }));
const setupCombinedBehavior = (store, outIds, id, isValidationResultValid, initialResultGetter) => {

@@ -76,5 +79,7 @@ store.addDerivedState(id, combineLatest([

validationSuccesses: output.conflicts1.successes,
validationCompletedSuccesses: output.conflicts1.completedSuccesses,
resultErrors: output.conflicts2.errors,
resultSuccesses: output.conflicts2.successes,
resultCompletedSuccesses: output.conflicts2.completedSuccesses,
}));
//# sourceMappingURL=validated-input-with-result-signals-factory.js.map
{
"name": "@rx-signals/store",
"version": "3.0.0-rc33",
"version": "3.0.0-rc34",
"description": "Reactive state- and effects-management with behaviors and event streams",

@@ -5,0 +5,0 @@ "author": "Gerd Neudert",

@@ -6,6 +6,6 @@ # _@rx-signals/store_

:warning: This documentation is work in progress for the upcoming 3.0.0 version.
There is however no good reason to use 2.x over 3.0.0-rc33, so please start with the rc-version (3.0.0 will be the first version I'm going to advertise publicly, so it's more like a 1.0 in reality.).
There is however no good reason to use 2.x over 3.0.0-rc34, so please start with the rc-version (3.0.0 will be the first version I'm going to advertise publicly, so it's more like a 1.0 in reality.).
2.x is deprecated and will NOT be maintained in any way.
3.0.0-rc33 is better than 2.x in any aspect and the implementation is production-ready.
3.0.0-rc34 is better than 2.x in any aspect and the implementation is production-ready.
It's mainly documentation that needs improvement for a final realease.

@@ -16,3 +16,3 @@ It is however possible that I will introduce minor breaking changes until 3.0.0 is finally released.

**`npm install --save @rx-signals/store@3.0.0-rc33`**
**`npm install --save @rx-signals/store@3.0.0-rc34`**

@@ -19,0 +19,0 @@ ## Dependencies

import {
Observable,
catchError,

@@ -41,4 +42,6 @@ combineLatest,

/** The current result,
* or NO_VALUE, if no result was received yet or input led to error
/**
* The current result,
* or NO_VALUE if no result was received yet,
* or the effect produced an error
*/

@@ -49,7 +52,7 @@ result: Result | NoValueType;

* The input that produced the current result,
* or NO_VALUE, initial result or no result received yet */
* or NO_VALUE, if initial result or no result received yet */
resultInput: Input | NoValueType;
/**
* In case the resultInput led to an error (result === NO_VALUE in that case) */
* In case the effect led to an error (result === NO_VALUE in that case), else undefined */
resultError?: any;

@@ -59,4 +62,9 @@

* Indicates whether the effect is currently running.
* In case of a factory without trigger, this will be true whenever
* currentInput !== resultInput, or whenever an invalidation event has been sent
* In case of a factory without trigger, this will be true whenever one or multiple
* of the following conditions is met:
* currentInput !== resultInput,
* or an invalidation event has been sent,
* or the effect has sent a result, but has not yet completed.
* In case of a factory with result-trigger, in addition to the previous
* criteria, a trigegr event must have been received.
*/

@@ -126,2 +134,7 @@ resultPending: boolean;

* Value-type for success events produced by {@link EffectSignals}.
* In case the effect completes after one result, two success events will
* be dispatched, one with completed false and one with completed true.
* This is to handle cases where the effect might send multiple results
* before completing. Thus, if an effect never completes, all success event
* will have completed false.
*

@@ -138,10 +151,30 @@ * @template Input - specifies the input type for the effect

/** the input of the previous result, or NO_VALUE */
/** the input of the previous completed result, or NO_VALUE */
previousInput: Input | NoValueType;
/** the previous result, or NO_VALUE */
/** the previous completed result, or NO_VALUE */
previousResult: Result | NoValueType;
/** has the effect for the given resultInput completed */
completed: boolean;
};
/**
* Value-type for completed success events produced by {@link EffectSignals}.
* In contrast to {@link EffectSuccess} events with this type are only dispatched,
* if the effect has completed, hence it will never be fired for effects that never complete.
*
* @template Input - specifies the input type for the effect
* @template Result - specifies the result type of the effect
*/
export type EffectCompletedSuccess<Input, Result> = Omit<
EffectSuccess<Input, Result>,
'completed'
> & { completed: true };
const isCompletedSuccess = <Input, Result>(
value: EffectCompletedSuccess<Input, Result> | EffectSuccess<Input, Result>,
): value is EffectCompletedSuccess<Input, Result> => value.completed;
/**
* Type specifying the input {@link EffectSignals} (the corresponding signal-sources are NOT added to the store

@@ -161,3 +194,6 @@ * by the EffectSignals-setup, but by whoever uses the signals, e.g. by extendSetup or fmap or just using dispatch).

/** Event that can be dispatched to trigger the given effect. This event has only meaning, if withTrigger is configured (see EffectConfiguration) */
/**
* Event that can be dispatched to trigger the given effect.
* This event has only meaning, if withTrigger is configured (see EffectConfiguration),
* else dispatching it is a no-op. */
trigger: EventId<undefined>;

@@ -168,3 +204,4 @@ };

* Type specifying the output {@link EffectSignals} (signals produced by EffectSignals).
* The {@link EffectSignalsFactory} takes care that subscribing error- or success-events keeps the effect itself lazy (hence only subscribing the combined behavior will subscribe the effect itself).
* The {@link EffectSignalsFactory} takes care that subscribing error- or success-events keeps
* the effect itself lazy (hence only subscribing the combined behavior will subscribe the effect itself).
*

@@ -178,3 +215,3 @@ * @template Input - specifies the input type for the effect

/** Convenience behavior derived from combined-behavior, representing only success states */
/** Convenience behavior derived from combined-behavior, representing only completed success states */
result: DerivedId<CombinedEffectResultInSuccessState<Input, Result>>;

@@ -190,2 +227,5 @@

successes: EventId<EffectSuccess<Input, Result>>;
/** Produced success events */
completedSuccesses: EventId<EffectCompletedSuccess<Input, Result>>;
};

@@ -212,6 +252,6 @@

* errors event stream will NOT subscribe the result behavior (see requirement 1).
* 3.) In addition to the result behavior, also an event stream for EffectSuccess<Input, Result> is provided. This is important
* 3.) In addition to the result behavior, also an event-stream for EffectSuccess<Input, Result> is provided. This is important
* in cases where an effect success should be used to trigger something else (e.g. close a popup), but you cannot use the result
* behavior, because it would mean to always subscribe the result. In contrast, subscription of the success event stream will NOT
* subscribe the result behavior.
* subscribe the result behavior. (the same holds true for the completedSuccesses event-stream)
* ```

@@ -260,3 +300,3 @@ *

/**
* Type specifying the {@link SignalsBuild} function for {@link EffectSignals}, hence a function taking an {@link EffectConfiguration} and producing EffectSignals.
* Type specifying the {@link SignalsBuild} function for {@link EffectSignals}, hence a function taking a {@link EffectConfiguration} and producing EffectSignals.
*

@@ -287,2 +327,5 @@ * @template Input - specifies the input type for the effect

successes: getEventId<EffectSuccess<Input, Result>>(`${nameExtension ?? ''}_successes`),
completedSuccesses: getEventId<EffectCompletedSuccess<Input, Result>>(
`${nameExtension ?? ''}_completedSuccesses`,
),
});

@@ -293,2 +336,7 @@

type InternalEffectResult<RT> = {
result: RT;
completed: boolean;
};
type InternalResultType<Input, Result> = {

@@ -301,2 +349,14 @@ result: Result | NoValueType;

const getIsNewInput =
<Input, Result>(effectInputEquals: (a: Input, b: Input) => boolean) =>
([input, resultState, token]: [
Input,
InternalResultType<Input, InternalEffectResult<Result>>,
object | null,
Input | NoValueTriggeredInput,
]): boolean =>
token !== resultState.resultToken ||
isNoValueType(resultState.resultInput) ||
!effectInputEquals(input, resultState.resultInput);
const getEffectBuilder: EffectSignalsBuild = <IT, RT>(

@@ -306,3 +366,3 @@ config: EffectConfiguration<IT, RT>,

const effectId = getEffectId<IT, RT>();
const internalResultEffect = (
const wrappedResultEffect = (
input: IT,

@@ -320,3 +380,3 @@ store: Store,

: effect;
return wrappedEffect(input, store, previousInput, previousResult).pipe(take(1));
return wrappedEffect(input, store, previousInput, previousResult);
} catch (error) {

@@ -327,4 +387,46 @@ return throwError(() => error);

);
const internalResultEffect = (
input: IT,
store: Store,
previousInput: IT | NoValueType,
previousResult: RT | NoValueType,
): Observable<InternalEffectResult<RT>> =>
new Observable<InternalEffectResult<RT>>(subscriber => {
let currentResult: RT | NoValueType = NO_VALUE;
const subscription = wrappedResultEffect(
input,
store,
previousInput,
previousResult,
).subscribe({
next: result => {
currentResult = result;
subscriber.next({
result,
completed: false,
});
},
complete: () => {
if (isNotNoValueType(currentResult)) {
subscriber.next({
result: currentResult,
completed: true,
});
}
subscriber.complete();
currentResult = NO_VALUE;
},
error: e => {
subscriber.error(e);
currentResult = NO_VALUE;
},
});
return () => {
subscription.unsubscribe();
currentResult = NO_VALUE;
};
});
const effectInputEquals = config.effectInputEquals ?? ((a, b) => a === b);
const isNewInput = getIsNewInput<IT, RT>(effectInputEquals);

@@ -343,7 +445,9 @@ const inIds = getInputSignalIds<IT>(config.nameExtension);

const resultEvent = getEventId<InternalResultType<IT, RT>>();
const resultBehavior = getDerivedId<InternalResultType<IT, RT>>();
const resultEvent = getEventId<InternalResultType<IT, InternalEffectResult<RT>>>();
const resultBehavior = getDerivedId<InternalResultType<IT, InternalEffectResult<RT>>>();
const initialResult = config.initialResultGetter ? config.initialResultGetter() : NO_VALUE;
store.addDerivedState(resultBehavior, store.getEventStream(resultEvent), {
result: initialResult,
result: isNotNoValueType(initialResult)
? { result: initialResult, completed: true }
: NO_VALUE,
resultInput: NO_VALUE,

@@ -361,6 +465,18 @@ resultToken: null,

store.addEventSource(
outIds.completedSuccesses,
store.getEventStream(outIds.successes).pipe(filter(isCompletedSuccess)),
);
// It is important to setup the combined observable as behavior,
// because a simple shareReplay (even with refCount) could create a memory leak!!!
const combinedId =
getDerivedId<[IT, InternalResultType<IT, RT>, object | null, IT | NoValueTriggeredInput]>();
getDerivedId<
[
IT,
InternalResultType<IT, InternalEffectResult<RT>>,
object | null,
IT | NoValueTriggeredInput,
]
>();
store.addDerivedState(

@@ -388,59 +504,70 @@ combinedId,

eventSourceInput.pipe(
filter(
([input, resultState, token]) =>
token !== resultState.resultToken ||
resultState.resultInput === NO_VALUE ||
!effectInputEquals(input, resultState.resultInput),
),
switchMap(([input, resultState, token, triggeredInput]) =>
config.withTrigger && input !== triggeredInput
? store.getEventStream(inIds.trigger).pipe(
map(() => ({
type: triggeredInputEvent,
event: input,
})),
)
: internalResultEffect(input, store, resultState.resultInput, resultState.result).pipe(
switchMap(result =>
of(
{
type: resultEvent,
event: {
result,
resultInput: input,
resultToken: token,
filter(isNewInput),
switchMap(
([input, resultState, token, triggeredInput]: [
IT,
InternalResultType<IT, InternalEffectResult<RT>>,
object | null,
IT | NoValueTriggeredInput,
]) =>
config.withTrigger && input !== triggeredInput
? store.getEventStream(inIds.trigger).pipe(
map(() => ({
type: triggeredInputEvent,
event: input,
})),
)
: internalResultEffect(
input,
store,
resultState.resultInput,
isNotNoValueType(resultState.result) ? resultState.result.result : NO_VALUE,
).pipe(
switchMap((result: InternalEffectResult<RT>) =>
of(
{
type: resultEvent,
event: {
// InternalResultType<IT, InternalEffectResult<RT>>
result,
resultInput: input,
resultToken: token,
},
},
},
{
type: outIds.successes,
event: {
result,
resultInput: input,
previousInput: resultState.resultInput,
previousResult: resultState.result,
{
type: outIds.successes,
event: {
// EffectSuccess<IT, RT>
result: result.result,
resultInput: input,
previousInput: resultState.resultInput,
previousResult: isNotNoValueType(resultState.result)
? resultState.result.result
: NO_VALUE,
completed: result.completed,
},
},
},
),
),
),
catchError(error =>
of(
{
type: outIds.errors,
event: {
error,
errorInput: input,
catchError(error =>
of(
{
type: outIds.errors,
event: {
error,
errorInput: input,
},
},
},
{
type: resultEvent,
event: {
result: NO_VALUE,
resultInput: input,
resultError: error,
resultToken: token,
{
type: resultEvent,
event: {
result: NO_VALUE,
resultInput: input,
resultError: error,
resultToken: token,
},
},
},
),
),
),
),
),

@@ -454,3 +581,3 @@ ),

IT,
InternalResultType<IT, RT>,
InternalResultType<IT, InternalEffectResult<RT>>,
object | null,

@@ -462,6 +589,7 @@ IT | NoValueTriggeredInput,

resultState.resultInput === NO_VALUE ||
!effectInputEquals(input, resultState.resultInput))
!effectInputEquals(input, resultState.resultInput) ||
(isNotNoValueType(resultState.result) && !resultState.result.completed))
: ([input, resultState, token]: [
IT,
InternalResultType<IT, RT>,
InternalResultType<IT, InternalEffectResult<RT>>,
object | null,

@@ -472,3 +600,4 @@ IT | NoValueTriggeredInput,

resultState.resultInput === NO_VALUE ||
!effectInputEquals(input, resultState.resultInput);
!effectInputEquals(input, resultState.resultInput) ||
(isNotNoValueType(resultState.result) && !resultState.result.completed);

@@ -482,3 +611,3 @@ store.addDerivedState(

currentInput: input,
result: resultState.result,
result: isNotNoValueType(resultState.result) ? resultState.result.result : NO_VALUE,
resultInput: resultState.resultInput,

@@ -489,3 +618,3 @@ resultPending: true,

currentInput: input,
result: resultState.result,
result: isNotNoValueType(resultState.result) ? resultState.result.result : NO_VALUE,
resultInput: resultState.resultInput,

@@ -492,0 +621,0 @@ resultError: resultState.resultError,

@@ -17,4 +17,46 @@ import { combineLatest, map } from 'rxjs';

const isStringRecord = (value: any): value is Record<string, any> =>
value && typeof value === 'object' && !Array.isArray(value);
/**
* A shallow equals function that performs shallow equals on records or arrays
* and else falls back to strict equals.
*/
export const shallowEquals = <T>(a: T, b: T): boolean => {
if (a === b) {
return true;
}
if (isStringRecord(a) && isStringRecord(b)) {
return (Object.entries(a).find(([k, v]) => v !== b[k]) ?? null) === null;
}
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) {
return false;
}
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
return a === b;
};
/**
* Value-type for the derived model behavior produced by {@link EntityEditFactory} Signals.
*
* @template Entity - specifies the entity type
* @template IdType - specifies the entity-id-type
* @template ValidationErrorType - specifies the error-type for failed validations
*/
export type EntityEditModel<Entity, IdType, ValidationErrorType> = {
/**
* The {@link CombinedEffectResult} for the load effect
*/
load: CombinedEffectResult<IdType | null, Entity>;
/**
* The {@link ValidatedInputWithResult} for validation and result effects
*/
edit: ValidatedInputWithResult<

@@ -25,16 +67,56 @@ Entity,

>;
model: Entity | undefined;
/**
* The current Entity state
*/
entity: Entity;
/**
* Convenience property for the current {@link ModelValidationResult}, taken from the edit property.
*/
validation: ModelValidationResult<Entity, ValidationErrorType>;
/**
* true, if either:
* the load effect is pending, or
* the result effect is pending
*/
loading: boolean;
/**
* true, if either:
* the load effect is pending, or
* the result effect is pending, or
* the validation effect is pending, or
* edit.isValid is false (the current validation result represents invalid entity state)
*/
disabled: boolean;
/**
* true, if the entity does not equal the default entity, with respect to configured
* equals function (defaults to {@link shallowEquals}).
*/
changed: boolean;
};
/**
* Type specifying the input signals for entity edit signals, hence a combination of
* {@link ModelInputSignals}, an id-behavior (load) and a save event.
*/
export type EntityEditInput<Entity, IdType> = ModelInputSignals<Entity> & {
/** input for the load effect */
load: DerivedId<IdType | null>;
/** input for the save effect */
save: EventId<undefined>;
};
/**
* Type specifying the output signals for entity edit signals,
*/
export type EntityEditOutput<Entity, IdType, ValidationErrorType> = {
/** {@link EffectOutputSignals} for the load effect */
load: EffectOutputSignals<IdType | null, Entity>;
/** {@link ValidatedInputWithResultOutput} for the validation and result effects */
edit: ValidatedInputWithResultOutput<

@@ -45,17 +127,51 @@ Entity,

>;
/** derived bahavior for the {@link EntityEditModel} */
model: DerivedId<EntityEditModel<Entity, IdType, ValidationErrorType>>;
};
/**
* Type specifying the configuration for {@link EntityEditFactory},
*/
export type EntityEditConfiguration<Entity> = {
/** the initial default entity, hence the initial entity state before the load effect. */
defaultEntity: Entity;
onSaveSuccessEvent?: EventId<undefined>;
/**
* Used to determine if the current entity state differs from default.
* Note that the default might differ from configured defaultEntity!
* A successful load effect will set a new default, as well as dispatching
* a setAsDefault event.
* This funtion defaults to {@link shallowEquals}, if not specified.
*/
entityEquals?: (a: Entity, b: Entity) => boolean;
/** optional event id for an event that should be dispatched on all save completed events */
onSaveCompletedEvent?: EventId<undefined>;
/** specifies whether the load behavior should be subscribed eagerly (defaults to false) */
eagerLoadSubscription?: boolean;
};
/**
* Type specifying the effects for {@link EntityEditFactory},
*/
export type EntityEditEffects<Entity, IdType, ValidationErrorType> = {
/** effect that takes entity id or null and returns a corresponding entity (which sets the default model) */
load: EffectId<IdType | null, Entity>;
/** effect that takes an entity and returns the corresponding {@link ModelValidationResult} */
validation: EffectId<Entity, ModelValidationResult<Entity, ValidationErrorType>>;
result: EffectId<Entity, IdType>;
/** effect that takes an entity and returns the id of the persisted entity */
save: EffectId<Entity, IdType>;
};
/**
* This type specifies a {@link SignalsFactory} producing signals to load, edit and persist an entity.
*
* @template Entity - specifies the entity type
* @template IdType - specifies the entity-id-type
* @template ValidationErrorType - specifies the error-type for failed validations
*/
export type EntityEditFactory<Entity, IdType, ValidationErrorType> = SignalsFactory<

@@ -70,3 +186,3 @@ EntityEditInput<Entity, IdType>,

* Generic function to create a specific {@link EntityEditFactory}.
* CAVE: This factory is work in progress and unit tests, as well as documentation is still missing!
* CAVE: This factory is work in progress and unit tests are still missing!
*/

@@ -110,3 +226,4 @@ export const getEntityEditSignalsFactory = <

},
onSaveSuccessEvent: config.onSaveSuccessEvent,
onSaveCompletedEvent: config.onSaveCompletedEvent,
entityEquals: config.entityEquals,
}))

@@ -125,15 +242,17 @@ .extendSetup((store, _, output, config) => {

edit,
model: modelWithDefault.model,
entity: modelWithDefault.model,
validation: isNotNoValueType(edit.validationResult) ? edit.validationResult : null,
loading: pending || edit.resultPending,
disabled: pending || edit.resultPending || edit.validationPending || !edit.isValid,
changed: modelWithDefault.model !== modelWithDefault.default,
changed: config.entityEquals
? config.entityEquals(modelWithDefault.model, modelWithDefault.default)
: shallowEquals(modelWithDefault.model, modelWithDefault.default),
})),
),
);
if (config.onSaveSuccessEvent) {
if (config.onSaveCompletedEvent) {
console.log('connecting');
store.connectObservable(
store.getEventStream(output.resultSuccesses).pipe(map(() => undefined)),
config.onSaveSuccessEvent,
store.getEventStream(output.resultCompletedSuccesses).pipe(map(() => undefined)),
config.onSaveCompletedEvent,
);

@@ -161,2 +280,3 @@ }

successes: ids.successes,
completedSuccesses: ids.completedSuccesses,
errors: ids.errors,

@@ -168,4 +288,6 @@ },

validationSuccesses: ids.validationSuccesses,
validationCompletedSuccesses: ids.validationCompletedSuccesses,
validationErrors: ids.validationErrors,
resultSuccesses: ids.resultSuccesses,
resultCompletedSuccesses: ids.resultCompletedSuccesses,
resultErrors: ids.resultErrors,

@@ -180,4 +302,4 @@ },

validation: ids.validation,
result: ids.result,
save: ids.result,
}),
);

@@ -5,2 +5,3 @@ import { Observable, combineLatest, distinctUntilChanged, filter, map, startWith } from 'rxjs';

CombinedEffectResultInSuccessState,
EffectCompletedSuccess,
EffectError,

@@ -44,3 +45,3 @@ EffectOutputSignals,

/** whether the current validationResult represents a valid state (false, if current validationResult is NO_VALUE) */
/** only true if validationResult represents a valid state AND validationPending is false */
isValid: boolean;

@@ -79,4 +80,6 @@

validationSuccesses: EventId<EffectSuccess<Input, ValidationResult>>;
validationCompletedSuccesses: EventId<EffectCompletedSuccess<Input, ValidationResult>>;
resultErrors: EventId<EffectError<Input>>;
resultSuccesses: EventId<EffectSuccess<Input, Result>>;
resultCompletedSuccesses: EventId<EffectCompletedSuccess<Input, Result>>;
};

@@ -126,5 +129,8 @@

const isResultInputGetterInput = <Input, ValidationResult>(
c: CombinedEffectResult<Input, ValidationResult>,
c: CombinedEffectResult<Input, ValidationResult>, // from the validation effect
): c is ResultInputGetterInput<Input, ValidationResult> =>
c.resultInput !== NO_VALUE && c.result !== NO_VALUE && c.currentInput === c.resultInput;
!c.resultPending &&
c.resultInput !== NO_VALUE &&
c.result !== NO_VALUE &&
c.currentInput === c.resultInput;

@@ -151,3 +157,3 @@ const resultInputGetter = <Input, ValidationResult>(

validationResult: v.result,
isValid: v.result !== NO_VALUE ? isValidationResultValid(v.result) : false,
isValid: !v.resultPending && v.result !== NO_VALUE ? isValidationResultValid(v.result) : false,
resultPending: r.resultPending,

@@ -264,4 +270,6 @@ resultInput: r.resultInput,

validationSuccesses: output.conflicts1.successes,
validationCompletedSuccesses: output.conflicts1.completedSuccesses,
resultErrors: output.conflicts2.errors,
resultSuccesses: output.conflicts2.successes,
resultCompletedSuccesses: output.conflicts2.completedSuccesses,
}));

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc