How to create a private custom event or how to handle/apply event-dispatching in the most protected way possible?

184 views Asked by At

I would like to create a private custom event, an event nobody else can dispatch or listen. I tried this, but it does not work:

new CustomEvent(Symbol("validate"));

Is it possible?

I know I can use a random string for the name of an event.

2

There are 2 answers

5
ceving On

This seems to be the maximum, which can be achieved.

class ObscureEvent {
  #name;
  #event;
  constructor (options) {
    this.#name = crypto.randomUUID();
    this.#event = new CustomEvent(this.#name, options); }
  listen(target, handler) {
    target.addEventListener(this.#name, handler); }
  dispatch(target) {
    target.dispatchEvent(this.#event); } }
      
const validate = new ObscureEvent({ bubbles: true });
0
Peter Seliger On

From some of the OP's and my comments ...

Of cause one could build a system which encloses/encapsulates everything except for e.g. an instance's de/register / un/subscribe methods where one could add/remove objects which each feature an own (event/topic/subject/channel specific) handler function. The dispatching instance does not have a public dispatchEvent method but would invoke each registered/subscribed object's specific handler function. Something similar to this ... – Peter Seliger

@PeterSeliger The problem with the event-strings is: they share one global name-space. Nothing prevents collisions. – ceving

Nope ... if one [privately] ties an own EventTarget instance to every instance of a type which is capable of dispatching events to selected (validated and registered) objects, no one except the secretly/privately dispatching instance itself is capable of accessing such dispatched events and its related data. This is due to just the dispatching instance itself can access the dispatching object. The instance itself controls the de/registering of objects which each have to be capable of handling such controlled dispatched (and otherwise not accessible) event-types. – Peter Seliger

And regarding especially ...

"The problem with the event-strings is: they share one global name-space. Nothing prevents collisions." – ceving

... the event-dispatching of EventTarget implements a type-safe Signals and Slot approach. Nobody can fake / intercept / spoof the event-object which gets created at the addEventListener's invocation time. In addition, at an event-target's dispatchEvent time only the event-type specific handler-functions (each assigned as an created event-listener's handleEvent method) which have been registered at this very event-target are going to be invoked.

Combining, like suggested (and later demonstrated), a pub-sub implementation via addSubscriber/s / removeSubscriber/s with the enclosed (privately held) functionality of an event-target, enables both, the privacy and the type-safety of event-dispatching.

const subscriber_1 = {
  uuid: crypto.randomUUID(),
  handleTypeA(evt) {
    console.log({
      eventType: evt.type,
      currentTarget: evt.currentTarget,
      targetConstructorName: evt.detail.target.constructor.name,
      subscriber: subscriber_1,
      payload: evt.detail.payload,
    });
  }
};
const subscriber_2 = {
  uuid: crypto.randomUUID(),
  handleTypeB(evt) {
    console.log({
      eventType: evt.type,
      currentTarget: evt.currentTarget,
      targetConstructorName: evt.detail.target.constructor.name,
      subscriber: subscriber_2,
      payload: evt.detail.payload,
    });
  }
};
const subscriber_3 = {
  uuid: crypto.randomUUID(),
  handleTypeB(evt) {
    console.log({
      eventType: evt.type,
      currentTarget: evt.currentTarget,
      targetConstructorName: evt.detail.target.constructor.name,
      subscriber: subscriber_3,
      payload: evt.detail.payload,
    });
  },
  handleTypeZ(evt) {
    console.log({
      eventType: evt.type,
      currentTarget: evt.currentTarget,
      targetConstructorName: evt.detail.target.constructor.name,
      subscriber: subscriber_3,
      payload: evt.detail.payload,
    });
  }
};
const sampleInstance = new PrivatlyDispatchingType;

console.log(
  'sampleInstance.addSubscribers(subscriber_1, subscriber_2, subscriber_3) ...\n',
  '... sampleInstance.aggregatesAndDispatchesSampleData() ...',
);
sampleInstance.addSubscribers(subscriber_1, subscriber_2, subscriber_3);
sampleInstance.aggregatesAndDispatchesSampleData();

console.log(
  'sampleInstance.removeSubscribers([subscriber_1, subscriber_3]) ...\n',
  '... sampleInstance.aggregatesAndDispatchesSampleData() ...',
);
sampleInstance.removeSubscribers([subscriber_1, subscriber_3]);
sampleInstance.aggregatesAndDispatchesSampleData();;

console.log(
  'sampleInstance.removeSubscribers(subscriber_2) ...\n',
  '... sampleInstance.aggregatesAndDispatchesSampleData() ...',
);
sampleInstance.removeSubscribers(subscriber_2);
sampleInstance.aggregatesAndDispatchesSampleData();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// scope of module ... `PrivatlyDispatchingType`

function isValidSubscriber(value) {
  // e.g. implement other custom type check functionality.
  return (
    (typeof value?.uuid === 'string') &&
    (Object
      .entries(value)
      .filter(([key, value]) =>
        key.startsWith('handle') && (typeof value === 'function')
      )
      .length >= 1)      
  );
}

function getValidSubscribers(...subscribers) {
  return subscribers.flat().filter(isValidSubscriber);
}

// a contract like lookup.
//
// - A subscriber has to feature an especially named method
//   (here the lookup-table's key) in order to get notified.
// - Thus, the type/name of any of the later from exclusively
//   intern/protected dispatched events does not even matter.
//
const eventTypeLookup = {
  handleTypeA: 'private-event-type-A',
  handleTypeB: 'private-event-type-B',
  handleTypeZ: 'private-event-type-Z',
}

class PrivatlyDispatchingType {
  #dispatcher = new EventTarget;
  #subscribers = new Map;

  /**
   *  type specific implementation
   */

  //  ----- ----- ----- ----- -----

  addSubscribers(...args) {
    getValidSubscribers(...args)

      .forEach(subscriber => {
        const { uuid } = subscriber;

        if (!this.#subscribers.has(uuid)) {
          this.#subscribers.set(uuid, subscriber);

          Object
            .entries(subscriber)
            .filter(([key, value]) =>
              key.startsWith('handle') && (typeof value === 'function')
            )
            .forEach(([handlerName, handlerFunction]) =>

              this.#dispatcher.addEventListener(
                eventTypeLookup[handlerName], handlerFunction
              )
            );
        }
      });
  }
  removeSubscribers(...args) {
    getValidSubscribers(...args)

      .forEach(subscriber => {
        const { uuid } = subscriber;

        if (this.#subscribers.has(uuid)) {
          this.#subscribers.delete(uuid);

          Object
            .entries(subscriber)
            .filter(([key, value]) =>
              key.startsWith('handle') && (typeof value === 'function')
            )
            .forEach(([handlerName, handlerFunction]) =>

              this.#dispatcher.removeEventListener(
                eventTypeLookup[handlerName], handlerFunction
              )
            );
        }
      });
  }

  aggregatesAndDispatchesSampleData() {
    this.#dispatcher.dispatchEvent(new CustomEvent(
      'private-event-type-A', {
        detail: {
          target: this,
          payload: {
            foo: 'Foo',
            bar: 'Bar',
          }
        }
      })
    );
    this.#dispatcher.dispatchEvent(new CustomEvent(
      'private-event-type-B', {
        detail: {
          target: this,
          payload: {
            baz: 'Baz',
            biz: 'Biz',
          }
        }
      })
    );
    this.#dispatcher.dispatchEvent(new CustomEvent(
      'private-event-type-Z', {
        detail: {
          target: this,
          payload: {
            quick: 'quick',
            brown: 'brown',
            fox: 'fox',
          }
        }
      })
    );
  }
}
</script>

Code re-use of "Exclusive Publishing Behavior"

The next provided example code demonstrates a possible way of code re-use, which still enables the exclusive publishing.

The implementation does achieve such behavior by utilizing a single WeakMap instance for covering the privacy part of the re-usable implemented ExclusivePublisher code. The latter class nevertheless can not be allowed to be directly instantiated by third party code that wants to acquire exclusive publishing behavior. Instead the "exclusive-publishing" module exposes a single useExclusivePublishing method which grands access to an ExclusivePublisher instance's publisher and dispatcher functionality.

Thus, any third party code which consumes the publisher and dispatcher return values of useExclusivePublishing first has to keep both references private and second needs to implement forwarding code for addSubscribers and removeSubscribers and in addition needs to implement its own publishing code built around the dispatcher's dispatchEvent method.

// import { useExclusivePublishing } from 'exclusive-publishing.js'

class MyExclusivePublishingType {
  #publisher;
  #dispatcher;

  constructor(eventTypes, isSubscriber) {

    const { publisher, dispatcher } =
      useExclusivePublishing(eventTypes, isSubscriber);

    // private property based aggregation.
    this.#publisher = publisher;
    this.#dispatcher = dispatcher;    
  }

  // forwarding via aggregated private properties.

  addSubscribers(...args) {
    return this.#publisher.addSubscribers(...args);
  }
  removeSubscribers(...args) {
    return this.#publisher.removeSubscribers(...args);
  }

  // type-specific prototypal implementation/s.

  publishSampleData() {
    this.#dispatcher.dispatchEvent(new CustomEvent(
      'private-event-type-A', {
        detail: {
          target: this,
          payload: {
            foo: 'Foo',
            bar: 'Bar',
          }
        }
      })
    );
    this.#dispatcher.dispatchEvent(new CustomEvent(
      'private-event-type-B', {
        detail: {
          target: this,
          payload: {
            baz: 'Baz',
            biz: 'Biz',
          }
        }
      })
    );
    this.#dispatcher.dispatchEvent(new CustomEvent(
      'private-event-type-Z', {
        detail: {
          target: this,
          payload: {
            quick: 'quick',
            brown: 'brown',
            fox: 'fox',
          }
        }
      })
    );
  }
}

const publishingType = new MyExclusivePublishingType({
  handleTypeA: 'private-event-type-A',
  handleTypeB: 'private-event-type-B',
  handleTypeZ: 'private-event-type-Z',
});

const subscriber_1 = {
  uuid: crypto.randomUUID(),
  handleTypeA(evt) {
    console.log({
      eventType: evt.type,
      currentTarget: evt.currentTarget,
      targetConstructorName: evt.detail.target.constructor.name,
      subscriber: subscriber_1,
      payload: evt.detail.payload,
    });
  }
};
const subscriber_2 = {
  uuid: crypto.randomUUID(),
  handleTypeB(evt) {
    console.log({
      eventType: evt.type,
      currentTarget: evt.currentTarget,
      targetConstructorName: evt.detail.target.constructor.name,
      subscriber: subscriber_2,
      payload: evt.detail.payload,
    });
  }
};
const subscriber_3 = {
  uuid: crypto.randomUUID(),
  handleTypeB(evt) {
    console.log({
      eventType: evt.type,
      currentTarget: evt.currentTarget,
      targetConstructorName: evt.detail.target.constructor.name,
      subscriber: subscriber_3,
      payload: evt.detail.payload,
    });
  },
  handleTypeZ(evt) {
    console.log({
      eventType: evt.type,
      currentTarget: evt.currentTarget,
      targetConstructorName: evt.detail.target.constructor.name,
      subscriber: subscriber_3,
      payload: evt.detail.payload,
    });
  }
};

console.log(
  'publishingType.addSubscribers(subscriber_1, subscriber_2, subscriber_3) ...\n',
  '... publishingType.publishSampleData() ...',
);
publishingType.addSubscribers(subscriber_1, subscriber_2, subscriber_3);
publishingType.publishSampleData();

console.log(
  'publishingType.removeSubscribers([subscriber_1, subscriber_3]) ...\n',
  '... publishingType.publishSampleData() ...',
);
publishingType.removeSubscribers([subscriber_1, subscriber_3]);
publishingType.publishSampleData();;

console.log(
  'publishingType.removeSubscribers(subscriber_2) ...\n',
  '... publishingType.publishSampleData() ...',
);
publishingType.removeSubscribers(subscriber_2);
publishingType.publishSampleData();
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// scope of module ... "exclusive-publishing.js"

const publisherRegistry = new WeakMap;

function isFunction(value) {
  return (
    (typeof value === 'function') &&
    (typeof value.call === 'function') &&
    (typeof value.apply === 'function')
  );
}

function isMinimumSubscriber(value) {
  return (
    (typeof value?.uuid === 'string') &&
    (Object
      .entries(value)
      .filter(([key, value]) =>
        key.startsWith('handle') && isFunction(value)
      )
      .length >= 1)      
  );
}

function getSubscribers(isSubscriber, ...subscribers) {
  return subscribers.flat().filter(isSubscriber);
}
function getHandlerEntries(subscriber) {
  return Object
    .entries(subscriber)
    .filter(([key, value]) =>
      key.startsWith('handle') && isFunction(value)
    );
}

function addSubscribers(publisher, ...args) {
  const {
    eventTypes, dispatcher, subscribers: subscriberRegistry, isSubscriber,
  } =
    publisherRegistry.get(publisher);

  getSubscribers(isSubscriber, ...args)
    .forEach(subscriber => {
      const { uuid } = subscriber;

      if (!subscriberRegistry.has(uuid)) {
        subscriberRegistry.set(uuid, subscriber);

        getHandlerEntries(subscriber)
          .forEach(([handlerName, handlerFunction]) =>

            dispatcher.addEventListener(
              eventTypes[handlerName], handlerFunction
            )
          );
      }
    });
}
function removeSubscribers(publisher, ...args) {
  const {
    eventTypes, dispatcher, subscribers: subscriberRegistry, isSubscriber,
  } =
    publisherRegistry.get(publisher);

  getSubscribers(isSubscriber, ...args)
    .forEach(subscriber => {
      const { uuid } = subscriber;

      if (subscriberRegistry.has(uuid)) {
        subscriberRegistry.delete(uuid);

        getHandlerEntries(subscriber)
          .forEach(([handlerName, handlerFunction]) =>

            dispatcher.removeEventListener(
              eventTypes[handlerName], handlerFunction
            )
          );
      }
    });
}

class ExclusivePublisher {
  constructor(eventTypes = {}, isSubscriber) {

    publisherRegistry
      .set(this, {
        eventTypes,
        dispatcher: new EventTarget,
        subscribers: new Map,
        isSubscriber: isFunction(isSubscriber)
          && (value => isMinimumSubscriber(value) && isSubscriber(value))
          || isMinimumSubscriber,
      });
  }

  // - minimum prototypal implementation/footprint because of fowarding
  //   which is possible due to the `WeakMap`-based privacy-approach.

  addSubscribers(...args) {
    return addSubscribers(this, ...args);
  }
  removeSubscribers(...args) {
    return removeSubscribers(this, ...args);
  }
}

/*export */function useExclusivePublishing(eventTypes, isSubscriber) {

  const publisher = new ExclusivePublisher(eventTypes, isSubscriber);
  const { dispatcher } =  publisherRegistry.get(publisher);

  return {
    publisher, dispatcher,
  };
}
</script>

The practicability, re-usability and the advantages of the "Exclusive Publishing Behavior" are going to be demonstrated once again by rewriting the example-code of one of my recently provided answers ... "With Node EventEmitter, how would one remove an object from a service's list while also un-subscribing from the service's event-dispatching?".

Regarding the last example code of the just referred answer ... instead of the Bakery class letting extend EventTarget which allows everyone with access to a bakery-instance to invoke all of its 3 inherited methods (add/removeEventListener / dispatchEvent), one would implement a Bakery class with the help of useExclusivePublishing of the further above introduced 'exclusive-publishing' module.

// import { Bakery } from './bakery.js';
// import { Customer } from './customer.js';

const klugesherz =
  new Bakery({ name: 'Pâtisserie Klugesherz', breadPrice: 1.5 });
const hanss =
  new Bakery({ name: 'Boulangerie Patisserie Hanss', breadPrice: 2.7 });

const johnRich = new Customer({
  name: 'John Rich',
  maxPrice: 5,
  moneyTotal: 20,
  handlePurchaseBread: function ({ detail: { bakery } }) {
    this.buyBread(bakery, 3);
  },
});
const martinPoor = new Customer({
  name: 'Martin Poor',
  maxPrice: 3,
  moneyTotal: 10,
  handlePurchaseBread: function ({ detail: { bakery } }) {
    const quantity = (
      ((bakery.name === 'Boulangerie Patisserie Hanss') && 1) ||
      ((bakery.name === 'Pâtisserie Klugesherz') && 2) || 0
    );
    this.buyBread(bakery, quantity);
  },
});

klugesherz.addCustomers(johnRich, martinPoor);
hanss.addCustomers(johnRich, martinPoor);

console.log({
  bakeries: {
    klugesherz: klugesherz.valueOf(),
    hanss: hanss.valueOf(),
  },
  customers: {
    johnRich: johnRich.valueOf(),
    martinPoor: martinPoor.valueOf(),
  },
});

console.log('\n+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);

console.log('\n+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);

console.log('\n+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);

console.log('\n+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);

console.log('\n... remove John Rich from the customer list of Patisserie Hanss.\n\n');
hanss.removeCustomers(johnRich);

console.log('\n+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);

console.log('\n+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);

console.log('\n... remove Martin Poor from the customer list of Patisserie Hanss.');
hanss.removeCustomers(martinPoor);

console.log('... remove John Rich and Martin Poor from the customer list of Pâtisserie Klugesherz.\n\n');
klugesherz.removeCustomers(johnRich, martinPoor);

console.log('+++ klugesherz.bakeBread(4) +++');
klugesherz.bakeBread(4);

console.log('+++ hanss.bakeBread(5) +++');
hanss.bakeBread(5);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// scope of module ... "exclusive-publishing.js"

const publisherRegistry = new WeakMap;

function isFunction(value) {
  return (
    (typeof value === 'function') &&
    (typeof value.call === 'function') &&
    (typeof value.apply === 'function')
  );
}

function isMinimumSubscriber(subscriber) {
  return (
    (typeof subscriber?.uuid === 'string') &&
    (Object
      .entries(subscriber.valueOf?.() ?? subscriber)
      .filter(([key, value]) =>
        key.startsWith('handle') && isFunction(value)
      )
      .length >= 1)      
  );
}

function getSubscribers(isSubscriber, ...subscribers) {
  return subscribers.flat().filter(isSubscriber);
}
function getHandlerEntries(subscriber) {
  return Object
    .entries(subscriber.valueOf?.() ?? subscriber)
    .filter(([key, value]) =>
      key.startsWith('handle') && isFunction(value)
    );
}

function addSubscribers(publisher, ...args) {
  const {
    eventTypes, dispatcher, subscribers: subscriberRegistry, isSubscriber,
  } =
    publisherRegistry.get(publisher);

  getSubscribers(isSubscriber, ...args)
    .forEach(subscriber => {
      const { uuid } = subscriber;

      if (!subscriberRegistry.has(uuid)) {
        subscriberRegistry.set(uuid, subscriber);

        getHandlerEntries(subscriber)
          .forEach(([handlerName, handlerFunction]) =>

            dispatcher.addEventListener(
              eventTypes[handlerName], handlerFunction
            )
          );
      }
    });
}
function removeSubscribers(publisher, ...args) {
  const {
    eventTypes, dispatcher, subscribers: subscriberRegistry, isSubscriber,
  } =
    publisherRegistry.get(publisher);

  getSubscribers(isSubscriber, ...args)
    .forEach(subscriber => {
      const { uuid } = subscriber;

      if (subscriberRegistry.has(uuid)) {
        subscriberRegistry.delete(uuid);

        getHandlerEntries(subscriber)
          .forEach(([handlerName, handlerFunction]) =>

            dispatcher.removeEventListener(
              eventTypes[handlerName], handlerFunction
            )
          );
      }
    });
}

class ExclusivePublisher {
  constructor(eventTypes = {}, isSubscriber) {

    publisherRegistry
      .set(this, {
        eventTypes,
        dispatcher: new EventTarget,
        subscribers: new Map,
        isSubscriber: isFunction(isSubscriber)
          && (value => isMinimumSubscriber(value) && isSubscriber(value))
          || isMinimumSubscriber,
      });
  }

  // - minimum prototypal implementation/footprint because of fowarding
  //   which is possible due to the `WeakMap`-based privacy-approach.

  addSubscribers(...args) {
    return addSubscribers(this, ...args);
  }
  removeSubscribers(...args) {
    return removeSubscribers(this, ...args);
  }
}

/*export */function useExclusivePublishing(eventTypes, isSubscriber) {

  const publisher = new ExclusivePublisher(eventTypes, isSubscriber);
  const { dispatcher, subscribers } =  publisherRegistry.get(publisher);

  return {
    publisher, dispatcher, subscribers,
  };
}
</script>

<script>
// scope of module ... "customer.js"

/*export */class Customer {
  #uuid;
  #name;
  #maxPrice;
  #moneyTotal;
  #handlePurchaseBread;

  constructor({ name, maxPrice, moneyTotal, handlePurchaseBread }) {
    this.#uuid = crypto.randomUUID();
    this.#name = name;
    this.#maxPrice = maxPrice;
    this.#moneyTotal = moneyTotal;
    this.#handlePurchaseBread = (typeof handlePurchaseBread === 'function')
      && handlePurchaseBread.bind(this)
      || (({ currentTarget: bakery }) => { this.buyBread(bakery, 1); });
  }
  get uuid() {
    return this.#uuid;
  }
  get name() {
    return this.#name;
  }
  get maxPrice() {
    return this.#maxPrice;
  }
  get moneyTotal() {
    return this.#moneyTotal;
  }
  get handlePurchaseBread() {
    return this.#handlePurchaseBread;
  }

  buyBread(bakery, quantity = 1) {
    const { approved, reason } = bakery.sellBread(this, quantity);

    if (approved === true) {

      this.#moneyTotal = this.moneyTotal - (bakery.breadPrice * quantity);

      console.log(
        `Customer ${ this.name } bought ${ quantity } piece/s of bread for a total of ${ (bakery.breadPrice * quantity).toFixed(2) } at ${ bakery.name }.`
      );
    } else if (typeof reason === 'string') {

      console.log('Buying a bread did fail, due to ...', reason);
    }
  }

  valueOf() {
    const { uuid, name, maxPrice, moneyTotal, handlePurchaseBread } = this;
    return { uuid, name, maxPrice, moneyTotal, handlePurchaseBread };
  }
}

/*export */function isCustomer(value) {
  return ((value instanceof Customer) || (
    Object
      .keys(value.valueOf())
      .sort()
      .join('_') === 'handlePurchaseBread_maxPrice_moneyTotal_name_uuid'
  ));
}
</script>

<script>
// scope of module ... "bakery.js"

// import { useExclusivePublishing } from './exclusive-publishing.js';
// import { isCustomer } from './customer.js';

/*export */class Bakery {
  #name;

  #breadPrice;
  #breadCount;
  #moneyTotal;

  #customers;

  #publisher;
  #dispatcher;

  constructor({ name, breadPrice }) {
    this.#name = name;

    this.#breadPrice = breadPrice;
    this.#breadCount = 0;
    this.#moneyTotal = 0;

    const { publisher, dispatcher, subscribers: customers } =
      useExclusivePublishing({ handlePurchaseBread: 'bread-baked' }, isCustomer);

    this.#customers = customers;

    this.#publisher = publisher;
    this.#dispatcher = dispatcher;
  }

  get name() {
    return this.#name;
  }
  get breadPrice() {
    return this.#breadPrice;
  }
  get breadCount() {
    return this.#breadCount;
  }
  get moneyTotal() {
    return this.#moneyTotal;
  }
  get customers() {
    return [...this.#customers.values()];
  }

  addCustomers(...args) {
    return this.#publisher.addSubscribers(...args);
  }
  removeCustomers(...args) {
    return this.#publisher.removeSubscribers(...args);
  }

  bakeBread(quantity = 10) {
    this.#breadCount = this.#breadCount + quantity;

    this.#dispatcher.dispatchEvent(
      new CustomEvent('bread-baked', { detail: { bakery: this, quantity } })
    );
  }

  sellBread(customer, quantity = 1) {
    const transaction = { approved: false };

    if (quantity >= 1) {
      if (this.breadCount >= quantity) {

        const isWithinPriceLimit = this.breadPrice <= customer.maxPrice;
        const canEffortPurchase = (this.breadPrice * quantity) <= customer.moneyTotal;

        if (isWithinPriceLimit) {
          if (canEffortPurchase) {

            this.#breadCount = this.breadCount - quantity;

            transaction.approved = true;
          } else {
            transaction.reason =
              `Customer ${ customer.name } doesn't have enough money for buying a bread at ${ this.name }.`;
          }
        } else {
          transaction.reason =
            `Customer ${ customer.name } does have a price limit which just did exceed at ${ this.name }.`;
        }
      } else {
        transaction.reason =
          `The ${ this.name } bakery is too low on bread stock in order to fulfill ${ customer.name }'s order.`;
      }
    } else {
      transaction.reason =
        `Customer ${ customer.name } did not provide a valid quantity for purchasing bread at ${ this.name }.`;
    }
    return transaction;
  }

  valueOf() {
    const { name, breadPrice, breadCount, moneyTotal, customers } = this;
    return {
      name, breadPrice, breadCount, moneyTotal,
      customers: customers.map(customer => customer.valueOf()),
    };
  }
}
</script>