Skip to content

Commit

Permalink
feat(signal): context prover and consumer
Browse files Browse the repository at this point in the history
  • Loading branch information
AliMD committed Jan 28, 2023
1 parent c2e0771 commit 081a51a
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 257 deletions.
221 changes: 0 additions & 221 deletions core/signal/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,224 +3,3 @@
Elegant powerful event system for handle global signals and states written in tiny TypeScript module.

Every signal has own value and can be used as a advance **state management** like redux and recoil without the complexities and unnecessary facilities of those libraries.

## Example usage

### Add your signal name to global `AlwatrSignals` type helper

```ts
declare global {
/**
* Global signals value type registry.
*/
interface AlwatrSignals {
readonly 'content-change': Record<string, number>;
}

/**
* Global request signal parameters types.
*/
interface AlwatrRequestSignals {
readonly 'content-change': number;
}
}
```

### Dispatch signal with value

```ts
import {SignalInterface} from 'https://esm.run/@alwatr/signal';

const contentChangeSignal1 = new SignalInterface('content-change');

contentChangeSignal1.dispatch({a: 1, b: 2});

contentChangeSignal1.dispatch({a: 2, b: 3});

// Multiple dispatch debounced and last value dispatched after an animation frame.
contentChangeSignal1.dispatch({a: 3, b: 4});
```

### Receive the signal value

```ts
import {SignalInterface} from 'https://esm.run/@alwatr/signal';

const contentChangeSignal2 = new SignalInterface('content-change'); // Same share signal as contentChangeSignal1

contentChangeSignal2.addListener((content) => {
console.log(content); // {a:3, b:4}
});
```

## API

### Prepare

#### `new SignalInterface(signal-name)`

```ts
import {SignalInterface} from 'https://esm.run/@alwatr/signal';

const contentChangeSignal = new SignalInterface('content-change');
```

### `name`

Get signal name.

Example:

```ts
console.log(contentChangeSignal.name); // 'content-change'
```

### `value`

Get last dispatched signal value or undefined.

Example:

```ts
if (contentChangeSignal.dispatched) {
const content = contentChangeSignal.value!;
...
}
```

### `dispatched`

Check signal dispatched before or not!

Example

```ts
if (contentChangeSignal.dispatched) {
// contentChangeSignal.value exist.
}
```

### `disabled`

Disable signal, all dispatch's ignored (just value updated) and no more listeners will be called.

Example:

```ts
contentChangeSignal.disabled = true;
```

### `expire()`

Expire the signal by clear last dispatched value.

dispatched and receivePrevious etc not work until new signal.

Example:

```ts
contentChangeSignal.dispatched; // true
contentChangeSignal.expire();
contentChangeSignal.value; // undefined
contentChangeSignal.dispatched; // false
```

### `setProvider(provider)`

Defines the provider of the signal that will be called when the signal requested (addRequestSignalListener).

Example:

```ts
contentChangeSignal.setProvider(async (requestParam) => {
const content = await fetchNewContent(requestParam);
if (content != null) {
return content; // Dispatch signal 'content-change' with content.
} else {
// dispatch new signal: 'content-not-found'
}
});
```

### `request(requestParam)`

Dispatch request signal and wait for answer (wait for new signal dispatched).

Resolved with signal value when new signal received (getNextSignalValue).

Example:

```ts
// dispatch request signal and wait for answer (wait for NEW signal).
const newContent = await contentChangeSignal.request({foo: 'bar'});
```

### `getNextSignalValue()`

Resolved with signal value when new signal received.

Example:

```ts
// Wait for NEW signal received.
const newContent = await contentChangeSignal.getNextSignalValue();
```

### `getSignalValue()`

Resolved with signal value when signal is ready.

Get signal value from last dispatched signal (if any) or wait for new signal received.

Example:

```ts
// get signal value from last dispatched signal (if any) or wait for a new signal to receive
const content = await contentChangeSignal.getSignalValue();
```

### `dispatch(signalValue)`

Dispatch signal to all listeners.

Example:

```ts
contentChangeSignal.dispatch(content);
```

### `addListener(listener)`

Adds a new listener to the signal.

Example:

```ts
const listener = contentChangeSignal.addListener((content) => console.log(content));
```

### Listener API Interface

#### `listener.disabled`

Disable the listener, not called anymore.

Example:

```ts
const listener = contentChangeSignal.addListener((content) => console.log(content));
...
listener.disabled = true;
```

#### `listener.remove()`

Removes a listener from the signal.

Example:

```ts
const listener = contentChangeSignal.addListener((content) => console.log(content));
...
listener.remove();
```
34 changes: 34 additions & 0 deletions core/signal/src/context-consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {_addSignalListener, _getSignalDetail, _removeSignalListener, _requestSignal, _untilNextSignal} from './core.js';

import type {SignalInterface, BoundSignalInterface} from './signal-interface-type.js';

export type AllowContextConsumerMethods = 'getValue' | 'untilNext' | 'subscribe' | 'unsubscribe' | 'request';
export type BoundContextConsumerInterface<T extends Record<string, unknown>> = Pick<
BoundSignalInterface<T>,
AllowContextConsumerMethods & 'id'
>;
export interface ContextConsumerInterface extends Pick<SignalInterface, AllowContextConsumerMethods> {
/**
* Bind signal consumer to special signal id.
*/
readonly bind: <T extends Record<string, unknown>>(signalId: string) => BoundContextConsumerInterface<T>;
}

/**
* Context consumer (signal consumer for context).
*/
export const contextConsumer: ContextConsumerInterface = {
getValue: _getSignalDetail,
untilNext: _untilNextSignal,
subscribe: _addSignalListener,
unsubscribe: _removeSignalListener,
request: _requestSignal,
bind: <T extends Record<string, unknown>>(signalId: string) => <BoundContextConsumerInterface<T>>{
id: signalId,
getValue: _getSignalDetail.bind(null, signalId),
untilNext: _untilNextSignal.bind(null, signalId),
subscribe: _addSignalListener.bind(null, signalId),
unsubscribe: _removeSignalListener,
request: _requestSignal.bind(null, signalId),
},
};
30 changes: 30 additions & 0 deletions core/signal/src/context-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {_dispatchSignal, _getSignalDetail, _setSignalProvider} from './core.js';

import type {SignalInterface, BoundSignalInterface} from './signal-interface-type.js';

export type AllowContextProviderMethods = 'getValue' | 'setValue' | 'setProvider';
export type BoundContextProviderInterface<T extends Record<string, unknown>> = Pick<
BoundSignalInterface<T>,
AllowContextProviderMethods & 'id'
>;
export interface ContextProviderInterface extends Pick<SignalInterface, AllowContextProviderMethods> {
/**
* Bind signal provider to special signal id.
*/
readonly bind: <T extends Record<string, unknown>>(signalId: string) => BoundContextProviderInterface<T>;
}

/**
* Context provider (signal provider for context).
*/
export const contextProvider: ContextProviderInterface = {
getValue: _getSignalDetail,
setValue: _dispatchSignal,
setProvider: _setSignalProvider,
bind: <T extends Record<string, unknown>>(signalId: string) => <BoundContextProviderInterface<T>>{
id: signalId,
getValue: _getSignalDetail.bind(null, signalId),
setValue: _dispatchSignal.bind(null, signalId),
setProvider: _setSignalProvider.bind(null, signalId),
},
};
3 changes: 2 additions & 1 deletion core/signal/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './signal-manager.js';
export {signalConsumer} from './signal-consumer.js';
export {signalProvider} from './signal-provider.js';

/*
TODO:
Expand Down
6 changes: 3 additions & 3 deletions core/signal/src/signal-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import {_addSignalListener, _getSignalDetail, _removeSignalListener, _untilNextS

import type {SignalInterface, BoundSignalInterface} from './signal-interface-type.js';

type AllowMethods = 'addListener' | 'getDetail' | 'untilNext' | 'removeListener';
export type AllowSignalConsumerMethods = 'addListener' | 'getDetail' | 'untilNext' | 'removeListener';
export type BoundSignalConsumerInterface<T extends Record<string, unknown>> = Pick<
BoundSignalInterface<T>,
AllowMethods & 'id'
AllowSignalConsumerMethods & 'id'
>;
export interface SignalConsumerInterface extends Pick<SignalInterface, Exclude<AllowMethods, 'id'>> {
export interface SignalConsumerInterface extends Pick<SignalInterface, Exclude<AllowSignalConsumerMethods, 'id'>> {
/**
* Bind signal consumer to special signal id.
*/
Expand Down
46 changes: 46 additions & 0 deletions core/signal/src/signal-interface-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ export interface SignalInterface<> {
*/
readonly getDetail: typeof _getSignalDetail,

/**
* Get current context value.
*
* alias for `signalConsumer.getDetail`.
*
* return undefined if context not provided before or expired.
*
* Example:
*
* ```ts
* const currentContent = await contextConsumer.getValue<ContentType>('content');
* if (currentContent == null) {
* // signal not dispatched yet
* }
* ```
*/
readonly getValue: typeof _getSignalDetail,

/**
* Resolved with signal detail when new signal received.
*
Expand All @@ -65,6 +83,19 @@ export interface SignalInterface<> {
*/
readonly addListener: typeof _addSignalListener,

/**
* Subscribe to a context changes.
*
* alias for `signalConsumer.addListener`.
*
* Example:
*
* ```ts
* const listener = contextConsumer.subscribe<ContentType>('content', (content) => console.log(content));
* ```
*/
readonly subscribe: typeof _addSignalListener,

/**
* Removes a listener from a signal.
*
Expand All @@ -78,6 +109,21 @@ export interface SignalInterface<> {
*/
readonly removeListener: typeof _removeSignalListener,

/**
* Unsubscribe from a context changes.
*
* alias for `signalConsumer.removeListener`.
*
* Example:
*
* ```ts
* const listener = contextConsumer.unsubscribe<ContentType>('content', (content) => console.log(content));
* ...
* signals.removeListener(listener);
* ```
*/
readonly unsubscribe: typeof _removeSignalListener,

/**
* Dispatch (send) signal to all listeners.
*
Expand Down
Loading

0 comments on commit 081a51a

Please sign in to comment.