Skip to content

Commit

Permalink
wip: adding stream providers
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-jonathan committed Jun 2, 2024
1 parent dc2d6e4 commit 0b58a16
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 69 deletions.
8 changes: 4 additions & 4 deletions src/Aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,11 @@ import {
} from '@/Entity'

import {
EventTopics,
EventObservable,
} from '@/Event'
MessageTopics,
MessageObservable,
} from '@/Message'

export abstract class Aggregate<E extends Entity, T extends EventTopics = EventTopics> extends EventObservable<T> {
export abstract class Aggregate<E extends Entity, T extends MessageTopics = MessageTopics> extends MessageObservable<T> {
protected root: E

constructor(root: E) {
Expand Down
76 changes: 13 additions & 63 deletions src/Event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,16 @@

import {
guard,
FoundationError,
} from '@cosmicmind/foundationjs'

import {
Observable,
ObservableTopics,
} from '@cosmicmind/patternjs'
Message,
MessageError,
MessageLifecycle,
MessagePropertyKey,
MessagePropertyLifecycle,
MessagePropertyLifecycleMap,
} from '@/Message'

/**
* Represents an Event.
Expand All @@ -53,70 +56,17 @@ import {
* age: 25
* }
*/
export type Event = Record<string, unknown>
export type Event = Message

/**
* Represents a collection of event topics.
*
* @extends {ObservableTopics}
*
* @property {Event} [K] - The event topic.
*/
export type EventTopics = ObservableTopics & {
readonly [K: string]: Event
}

/**
* An observable class for handling events of specific types.
*
* @template T The event topic type.
*/
export class EventObservable<T extends EventTopics> extends Observable<T> {}

/**
* Represents a type that is a valid property key of a given event type.
*
* @template K - The event type.
* @typeparam K - A type is a valid property key.
*
* @returns - A valid property key of the event type, or `never` if the key is not a valid property key.
*/
export type EventPropertyKey<K> = keyof K extends string | symbol ? keyof K : never
export type EventPropertyKey<K> = MessagePropertyKey<K>

/**
* Represents the lifecycle hooks for an event property.
*
* @template E - The type of the event.
* @template V - The type of the property value.
*/
export type EventPropertyLifecycle<E extends Event, V> = {
required?: boolean
validator?(value: V, event: E): boolean | never
updated?(newValue: V, oldValue: V, event: E): void
}
export type EventPropertyLifecycle<E extends Event, V> = MessagePropertyLifecycle<E, V>

/**
* Represents a map that defines the lifecycle of event properties.
*
* @template E - The type of the event.
*/
export type EventPropertyLifecycleMap<E extends Event> = {
[K in keyof E]?: EventPropertyLifecycle<E, E[K]>
}
export type EventPropertyLifecycleMap<E extends Event> = MessagePropertyLifecycleMap<E>

export class EventError extends FoundationError {}
export class EventError extends MessageError {}

/**
* Represents the lifecycle methods for an event.
*
* @template E - The type of event.
*/
export type EventLifecycle<E extends Event> = {
created?(event: E): void
trace?(event: E): void
error?(error: EventError): void
properties?: EventPropertyLifecycleMap<E>
}
export type EventLifecycle<E extends Event> = MessageLifecycle<E>

/**
* Defines an event with an optional event lifecycle handler.
Expand Down
215 changes: 215 additions & 0 deletions src/Message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/**
* BSD 3-Clause License
*
* Copyright © 2023, Daniel Jonathan <daniel at cosmicmind dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES LOSS OF USE, DATA, OR PROFITS OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/**
* @module Message
*/

import {
guard,
FoundationError,
} from '@cosmicmind/foundationjs'

import {
Observable,
ObservableTopics,
} from '@cosmicmind/patternjs'

/**
* Represents an Message.
*
* @example
* const message: Message = {
* name: 'John Doe',
* age: 25
* }
*/
export type Message = Record<string, unknown>

/**
* Represents a collection of message topics.
*
* @extends {ObservableTopics}
*
* @property {Message} [K] - The message topic.
*/
export type MessageTopics = ObservableTopics & {
readonly [K: string]: Message
}

/**
* An observable class for handling messages of specific types.
*
* @template T The message topic type.
*/
export class MessageObservable<T extends MessageTopics> extends Observable<T> {}

/**
* Represents a type that is a valid property key of a given message type.
*
* @template K - The message type.
* @typeparam K - A type is a valid property key.
*
* @returns - A valid property key of the message type, or `never` if the key is not a valid property key.
*/
export type MessagePropertyKey<K> = keyof K extends string | symbol ? keyof K : never

/**
* Represents the lifecycle hooks for an message property.
*
* @template E - The type of the message.
* @template V - The type of the property value.
*/
export type MessagePropertyLifecycle<E extends Message, V> = {
required?: boolean
validator?(value: V, message: E): boolean | never
updated?(newValue: V, oldValue: V, message: E): void
}

/**
* Represents a map that defines the lifecycle of message properties.
*
* @template E - The type of the message.
*/
export type MessagePropertyLifecycleMap<E extends Message> = {
[K in keyof E]?: MessagePropertyLifecycle<E, E[K]>
}

export class MessageError extends FoundationError {}

/**
* Represents the lifecycle methods for an message.
*
* @template E - The type of message.
*/
export type MessageLifecycle<E extends Message> = {
created?(message: E): void
trace?(message: E): void
error?(error: MessageError): void
properties?: MessagePropertyLifecycleMap<E>
}

/**
* Defines an message with an optional message lifecycle handler.
*
* @template E The type of the message.
* @param {MessageLifecycle<E>} [handler={}] The optional message lifecycle handler.
* @returns {(message: E) => E} A function that creates an message with the given lifecycle handler.
*/
export const defineMessage = <E extends Message>(handler: MessageLifecycle<E> = {}): (message: E) => E =>
(message: E): E => createMessage(message, handler)

/**
* Creates a ProxyHandler for Message instances with the given MessageLifecycle handler.
* The ProxyHandler intercepts property set operations, validates the new value against the associated property validator,
* triggers the updated callback for the property, updates the property value, and traces the change if trace is enabled.
*
* @typeparam E - The type of Message.
* @param handler - The MessageLifecycle handler.
* @returns A ProxyHandler for Message instances.
*/
function createMessageHandler<E extends Message>(handler: MessageLifecycle<E>): ProxyHandler<E> {
return {
set<A extends MessagePropertyKey<E>, V extends E[A]>(target: E, key: A, value: V): boolean | never {
const property = handler.properties?.[key]

if (false === property?.validator?.(value, target)) {
throwErrorAndTrace(`${JSON.stringify(target)} ${String(key)} is invalid`, handler)
}

property?.updated?.(value, target[key], target)

const result = Reflect.set(target, key, value)
handler.trace?.(target)

return result
},
}
}

/**
* Throws an MessageError with a specified message and invokes the error handler.
*
* @template E - The type of Message.
* @param {string} message - The error message.
* @param {MessageLifecycle<E>} handler - The message lifecycle handler.
* @throws {MessageError} - The MessageError instance.
* @return {never} - This method never returns.
*/
function throwErrorAndTrace<E extends Message>(message: string, handler: MessageLifecycle<E>): never {
const error = new MessageError(message)
handler.error?.(error)
throw error
}

/**
* Creates an message of type `E`.
*
* @template E - The type of the message to create.
* @param {E} target - The target object to create the message from.
* @param {MessageLifecycle<E>} [handler={}] - The lifecycle handler for the message.
* @returns {E} - The created message object.
* @throws {MessageError} - If the target object is invalid.
*/
function createMessage<E extends Message>(target: E, handler: MessageLifecycle<E> = {}): E | never {
if (guard<E>(target)) {
const properties = handler.properties

if (guard<MessagePropertyLifecycleMap<E>>(properties)) {
const message = new Proxy(target, createMessageHandler(handler))

for (const [ key, property ] of Object.entries(properties) as [string, MessagePropertyLifecycle<E, unknown>][]) {
if (property.required) {
if (!(key in target)) {
throwErrorAndTrace(`${JSON.stringify(target)} ${key} is required`, handler)
}

if (false === property.validator?.(target[key], message)) {
throwErrorAndTrace(`${JSON.stringify(target)} ${key} is invalid`, handler)
}
}
else if (key in target && 'undefined' !== typeof target[key]) {
if (guard(property, 'validator') && false === property.validator?.(target[key], message)) {
throwErrorAndTrace(`${JSON.stringify(target)} ${key} is invalid`, handler)
}
}
}

handler.created?.(message)
handler.trace?.(message)

return message
}
}

throw new MessageError(`${String(target)} is invalid`)
}
62 changes: 62 additions & 0 deletions src/Topic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* BSD 3-Clause License
*
* Copyright © 2023, Daniel Jonathan <daniel at cosmicmind dot com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES LOSS OF USE, DATA, OR PROFITS OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/**
* @module Topic
*/

import {
Observable,
ObservableTopics,
} from '@cosmicmind/patternjs'

import {
Message,
} from '@/Message'

/**
* Represents a collection of Message topics.
*
* @extends {ObservableTopics}
*
* @property {Message} [K] - A message topic.
*/
export type MessageTopics = ObservableTopics & {
readonly [K: string]: Message
}

/**
* An observable class for handling topics of specific types.
*
* @template T The message topic type.
*/
export class TopicObservable<T extends MessageTopics> extends Observable<T> {}
Loading

0 comments on commit 0b58a16

Please sign in to comment.