Socket
Socket
Sign inDemoInstall

@typescript-tea/core

Package Overview
Dependencies
0
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.1.0 to 0.2.0-next.1

dist/cmd.d.ts

12

CHANGELOG.md

@@ -1,5 +0,15 @@

## [0.1.0](https://github.com/dividab/graphql-norm/compare/v0.1.0...v0.1.0) - 2020-01-26
## [0.2.0](https://github.com/typescript-tea/core/compare/v0.1.0...v0.2.0) - 2020-02-26
### Changed
- Renamed runtime to Program.run()
- Renamed mapDispatch to Dispatch.map()
- Require render function to Program.run()
- Make effectManagers optional in Program.run()
- Remove dep on ts-exhaustive-check
## [0.1.0](https://github.com/typescript-tea/core/compare/v0.1.0...v0.1.0) - 2020-01-26
### Added
- Initial version.

37

package.json
{
"name": "@typescript-tea/core",
"version": "0.1.0",
"version": "0.2.0-next.1",
"description": "The Elm Architecture for typescript",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"main": "dist/core.js",
"module": "dist/core.m.js",
"types": "dist/core.d.ts",
"author": "Jonas Kello <jonas.kello@gmail.com>",
"license": "MIT",
"repository": "github:typescript-tea/core",
"homepage": "https://typescript-tea.github.io/core",
"publishConfig": {
"access": "public"
},
"keywords": [
"Elm",
"Architecture",
"The Elm Architecture",
"TEA"
],
"files": [
"/lib",
"/src",
"/dist",
"/src",
"package.json",

@@ -25,3 +33,2 @@ "CHANGELOG.md",

"@typescript-eslint/parser": "^2.17.0",
"codecov": "^3.6.2",
"confusing-browser-globals": "^1.0.9",

@@ -35,21 +42,20 @@ "eslint": "^6.8.0",

"lint-staged": "^10.0.2",
"microbundle": "^0.12.0-next.8",
"prettier": "^1.19.1",
"rimraf": "^3.0.0",
"rollup": "^1.29.1",
"ts-jest": "^25.0.0",
"typedoc": "^0.17.0-3",
"typescript": "^3.7.5"
},
"dependencies": {
"ts-exhaustive-check": "^1.0.0"
},
"scripts": {
"build": "tsc -b",
"clean": "tsc -b --clean && rimraf lib",
"clean": "tsc -b --clean && rimraf lib && rimraf dist",
"test": "jest",
"test-coverage": "jest --coverage",
"lint": "eslint './src/**/*.ts{,x}' --ext .js,.ts,.tsx -f visualstudio",
"dist": "yarn build && rimraf dist && rollup lib/index.js --file dist/umd.js --format umd --name TypescriptTeaCore",
"dist": "yarn build && rimraf dist && microbundle src/index.ts",
"verify": "yarn lint && yarn test-coverage && yarn dist",
"report-coverage": "codecov -f coverage/lcov.info",
"preversion": "yarn verify",
"docs": "typedoc && touch docs/.nojekyll",
"preversion": "yarn verify && yarn docs",
"postversion": "git push --tags && yarn publish --new-version $npm_package_version && git push --follow-tags && echo \"Successfully released version $npm_package_version!\""

@@ -68,3 +74,8 @@ },

}
},
"prettier": {
"printWidth": 120,
"trailingComma": "es5",
"arrowParens": "always"
}
}

@@ -11,2 +11,159 @@ # @typescript-tea/core

## Introduction
This is an implementation of The Elm Architecture (TEA) for typescript.
Note: TEA has managed effects, meaning that things like HTTP requests or writing to disk are all treated as data in TEA. When this data is given to an Effect Manager, it can do some "query optimization" before actually performing the effect. Your application should consist of pure functions only and all effects should be handled in Effect Managers outside your application.
TEA has two kinds of managed effects: commands and subscriptions.
## How to use
```
yarn add @typescript-tea/core
```
## Documentation
Please see the [documentation site](https://typescript-tea.github.io/core).
## Example
This is the usual counter app example using the react as view library. It is also available in [this repo](https://github.com/typescript-tea/simple-counter-example).
```ts
import React from "react";
import ReactDOM from "react-dom";
import { exhaustiveCheck } from "ts-exhaustive-check";
import { Dispatch, Program } from "@typescript-tea/core";
// -- STATE
type State = number;
const init = (): readonly [State] => [0];
// -- UPDATE
type Action = { type: "Increment" } | { type: "Decrement" };
function update(action: Action, state: State): readonly [State] {
switch (action.type) {
case "Increment":
return [state + 1];
case "Decrement":
return [state - 1];
default:
return exhaustiveCheck(action, true);
}
}
// -- VIEW
const view = ({ dispatch, state }: { readonly dispatch: Dispatch<Action>; readonly state: State }) => (
<div>
<button onClick={() => dispatch({ type: "Decrement" })}>-</button>
<div>{state}</div>
<button onClick={() => dispatch({ type: "Increment" })}>+</button>
</div>
);
// -- PROGRAM
const program: Program<State, Action, JSX.Element> = {
init,
update,
view,
};
// -- RUN
const app = document.getElementById("app");
const render = (view: JSX.Element) => ReactDOM.render(view, app);
Program.run(program, render);
```
## Differences from TEA in Elm
There are some naming differences from TEA in Elm:
- `Msg` was renamed to `Action`
- `Model` was renamed to `State`
Elm is a pure language with strict guarantees and the Effect Managers are part of kernel in Elm and you cannot (for good [reasons](https://groups.google.com/forum/#!msg/elm-dev/1JW6wknkDIo/H9ZnS71BCAAJ)) write your own Effect Managers in Elm. Typescript is an impure lanauge without any guarantees so it (probably) does not make sense to have this restriction. Therefore in typescript-tea it is possible to write your own Effect Manager to do whatever you want.
It does not have a built-in view library, instead it is possible to integrate with existing view libraries like React.
## How to import
### Whole module from the root
This package (and others in `@typescript-tea` organization) exports only `function`s and `type`s grouped into modules. You can import a module from the root of the package in the following way:
```ts
import { ModuleName1, ModuleName2 } from "@typescript-tea/package-name";
```
For example:
```ts
import { Result } from "@typescript-tea/core";
const result = Result.Ok("It is OK");
```
### Unprefixed named imports from the module file
If you don't want to prefix with `ModuleName` you can also use named imports directly from the module file:
```ts
import { function1, function2 } from "@typescript-tea/package-name/module-name";
```
For example:
```ts
import { Ok } from "@typescript-tea/core/result";
const result = Ok("It is OK");
```
### Modules that export a single type
A common pattern is to have a module that exports a single type with the same name as the module. For example the `Result` module does this, it exports the `Result` type, some constructor functions that create a `Result` type, and some utility funcitons that operate on or return a `Result` type. In these cases it can become annoying to prefix the type with the module name, like `Result.Result`. Consider the following example. Note that this is **not** how it is done for modules with single type exports in typescript-tea, it is just to illustrate how it would be done normally:
```ts
import { Result } from "@typescript-tea/core";
function itsOk(): Result.Result<string, string> {
const ok: Result.Result<string, string> = Result.Ok("It is OK");
const err: Result.Result<string, string> = Result.Ok("It is not OK");
return ok;
}
```
To avoid having to write `Result.Result` in these cases, the `Result` module uses a trick so that both the module name and the type can be named simply `Result`. So the code above will become this (notice use of `Result` for the type annotations instead of `Result.Result`):
```ts
import { Result } from "@typescript-tea/core";
function itsOk(): Result<string, string> {
const ok: Result<string, string> = Result.Ok("It is OK");
const err: Result<string, string> = Result.Ok("It is not OK");
return ok;
}
```
How can this work? Well, the index file in the package does this to make it work:
```ts
import * as ResultNs from "./result";
export const Result = ResultNs;
export type Result<TError, TValue> = ResultNs.Result<TError, TValue>;
```
I think it is somehow related to [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) in typescript :-).
Please note that this only work for modules that export a single type. If two types are exported it is not possible to use this shortcut because the exported `const` will not contain any types.
## How to develop

@@ -13,0 +170,0 @@

@@ -1,6 +0,4 @@

export type ActionMapper<ChildAction, ParentAction> = (childAction: ChildAction) => ParentAction;
// export type ActionMapper<ChildAction, ParentAction> = (childAction: ChildAction) => ParentAction;
export interface Dispatch<A> {
(action: A): void;
}
export type Dispatch<A> = (action: A) => void;

@@ -11,3 +9,3 @@ // eslint-disable-next-line functional/prefer-readonly-type

// eslint-disable-next-line functional/prefer-readonly-type
Map<ActionMapper<ChildAction, ParentAction>, Dispatch<ChildAction>>
Map<(childAction: ChildAction) => ParentAction, Dispatch<ChildAction>>
>;

@@ -22,6 +20,6 @@

* e.g. react becuase the dispach prop will not change like it
* would if a lambda like (a) => diaptch(mapper(a)) was used.
* would if a lambda like (a) => dispatch(mapper(a)) was used.
*/
export function mapDispatch<ChildAction, ParentAction>(
actionMapper: ActionMapper<ChildAction, ParentAction>,
export function map<ChildAction, ParentAction>(
actionMapper: (childAction: ChildAction) => ParentAction,
dispatch: Dispatch<ParentAction>

@@ -28,0 +26,0 @@ ): Dispatch<ChildAction> {

@@ -1,130 +0,54 @@

import { exhaustiveCheck } from "ts-exhaustive-check";
import { Dispatch, ActionMapper } from "./dispatch";
import { Dispatch } from "./dispatch";
import { LeafEffect, LeafEffectMapper } from "./effect";
export type Cmd<A> = Effect<A>;
export const batchCmds = batchEffects;
export const mapCmd = mapEffect;
/**
* A function that will be called by the runtime with the effects (commands and subscriptions)
* that was gathered for the effect manager.
*/
export type OnEffects<AppAction, SelfAction, State> = (
dispatchApp: Dispatch<AppAction>,
dispatchSelf: Dispatch<SelfAction>,
cmds: ReadonlyArray<LeafEffect<AppAction>>,
subs: ReadonlyArray<LeafEffect<AppAction>>,
state: State
) => State;
export type Sub<A> = Effect<A>;
export const batchSubs = batchEffects;
export const mapSub = mapEffect;
/**
* A function that will be called by the runtime with the actions that an effect manager
* dispatches to itself.
*/
export type OnSelfAction<AppAction, SelfAction, State> = (
dispatchApp: Dispatch<AppAction>,
dispatchSelf: Dispatch<SelfAction>,
action: SelfAction,
state: State
) => State;
export type Effect<A> = BatchedEffect<A> | MappedEffect<A, unknown> | LeafEffect<A>;
/**
* A type that describes an effect manager that can be used by the runtime.
*/
export type EffectManager<AppAction = unknown, SelfAction = unknown, State = unknown, THome = unknown> = {
readonly home: THome;
readonly mapCmd: LeafEffectMapper;
readonly mapSub: LeafEffectMapper;
readonly onEffects: OnEffects<AppAction, SelfAction, State>;
readonly onSelfAction: OnSelfAction<AppAction, SelfAction, State>;
};
const InternalHome = "__internal";
type InternalHome = typeof InternalHome;
export interface LeafEffect<_A> {
readonly home: string;
readonly type: string;
}
interface BatchedEffect<A> {
readonly home: InternalHome;
readonly type: "Batched";
readonly list: ReadonlyArray<Effect<A>>;
}
interface MappedEffect<A1, A2> {
readonly home: InternalHome;
readonly type: "Mapped";
readonly actionMapper: ActionMapper<A1, A2>;
readonly original: BatchedEffect<A1> | MappedEffect<A1, A2> | LeafEffect<A1>;
}
function batchEffects<A>(effects: ReadonlyArray<Effect<A> | undefined>): BatchedEffect<A> {
return {
home: InternalHome,
type: "Batched",
list: effects.filter((c) => c !== undefined) as ReadonlyArray<Effect<A>>,
/** @ignore */
export function createGetEffectManager(effectManagers: ReadonlyArray<EffectManager>): (home: string) => EffectManager {
type ManagersByHome = {
readonly [home: string]: EffectManager<unknown, unknown, unknown>;
};
}
function mapEffect<A1, A2>(
mapper: ActionMapper<A1, A2>,
c: BatchedEffect<A1> | MappedEffect<A1, A2> | LeafEffect<A1> | undefined
): MappedEffect<A1, A2> | undefined {
return c === undefined ? undefined : { home: InternalHome, type: "Mapped", actionMapper: mapper, original: c };
}
export type LeafEffectMapper<A1, A2> = (actionMapper: ActionMapper<A1, A2>, effect: Effect<A1>) => LeafEffect<A2>;
export interface EffectManager<AppAction = unknown, ManagerAction = unknown, ManagerState = unknown, THome = unknown> {
readonly home: THome;
readonly mapCmd: LeafEffectMapper<AppAction, ManagerAction>;
readonly mapSub: LeafEffectMapper<AppAction, ManagerAction>;
readonly onEffects: (
dispatchApp: Dispatch<AppAction>,
dispatchSelf: Dispatch<ManagerAction>,
cmds: ReadonlyArray<LeafEffect<AppAction>>,
subs: ReadonlyArray<LeafEffect<AppAction>>,
state: ManagerState
) => ManagerState;
readonly onSelfAction: (
dispatchApp: Dispatch<AppAction>,
dispatchSelf: Dispatch<ManagerAction>,
action: ManagerAction,
state: ManagerState
) => ManagerState;
}
export interface ManagersByHome {
readonly [home: string]: EffectManager<unknown, unknown, unknown>;
}
export function managersByHome(
effectManagers: ReadonlyArray<EffectManager<unknown, unknown, unknown>>
): ManagersByHome {
return Object.fromEntries(effectManagers.map((em) => [em.home, em]));
}
export function getEffectManager(home: string, managers: ManagersByHome): EffectManager<unknown> {
const managerModule = managers[home];
if (!managerModule) {
throw new Error(`Could not find effect manager '${home}'. Make sure it was passed to the runtime.`);
function managersByHome(effectManagers: ReadonlyArray<EffectManager>): ManagersByHome {
return Object.fromEntries(effectManagers.map((em) => [em.home, em]));
}
return managerModule;
}
export interface GatheredEffects<A> {
// This interface is mutable for efficency
// eslint-disable-next-line
[home: string]: { readonly cmds: Array<LeafEffect<A>>; readonly subs: Array<LeafEffect<A>> };
}
export function gatherEffects<A>(
managers: ManagersByHome,
gatheredEffects: GatheredEffects<A>,
isCmd: boolean,
effect: Effect<unknown>,
actionMapper: ActionMapper<unknown, unknown> | undefined = undefined
): void {
if (effect.home === InternalHome) {
const internalEffect = effect as BatchedEffect<unknown> | MappedEffect<unknown, unknown>;
switch (internalEffect.type) {
case "Batched": {
internalEffect.list.flatMap((c) => gatherEffects(managers, gatheredEffects, isCmd, c, actionMapper));
return;
}
case "Mapped":
gatherEffects(
managers,
gatheredEffects,
isCmd,
internalEffect.original,
actionMapper ? (a) => actionMapper(internalEffect.actionMapper(a)) : internalEffect.actionMapper
);
return;
default:
exhaustiveCheck(internalEffect, true);
const managers = managersByHome(effectManagers);
return function getEffectManager(home: string): EffectManager<unknown> {
const managerModule = managers[home];
if (!managerModule) {
throw new Error(`Could not find effect manager '${home}'. Make sure it was passed to the runtime.`);
}
} else {
const manager = getEffectManager(effect.home, managers);
if (!gatheredEffects[effect.home]) {
gatheredEffects[effect.home] = { cmds: [], subs: [] };
}
const list = isCmd ? gatheredEffects[effect.home].cmds : gatheredEffects[effect.home].subs;
const mapper = isCmd ? manager.mapCmd : manager.mapSub;
list.push(actionMapper ? mapper(actionMapper, effect) : effect);
}
return managerModule;
};
}

@@ -1,6 +0,29 @@

// Run-time
export * from "./program";
export * from "./runtime";
export * from "./dispatch";
/* eslint-disable import/first, import/newline-after-import, import/order */
// Program
import * as ProgramNs from "./program";
export const Program = ProgramNs;
export type Program<S, A, V> = ProgramNs.Program<S, A, V>;
// Dispatch
import * as DispatchNs from "./dispatch";
export const Dispatch = DispatchNs;
export type Dispatch<A> = DispatchNs.Dispatch<A>;
// Effect manager
export * from "./effect-manager";
// Cmd
import * as CmdNs from "./cmd";
export const Cmd = CmdNs;
export type Cmd<A> = CmdNs.Cmd<A>;
// Sub
import * as SubNs from "./sub";
export const Sub = SubNs;
export type Sub<A> = SubNs.Sub<A>;
// Result
import * as ResultNs from "./result";
export const Result = ResultNs;
export type Result<TError, TValue> = ResultNs.Result<TError, TValue>;

@@ -1,6 +0,12 @@

import { Sub, Cmd } from "./effect-manager";
import { Cmd } from "./cmd";
import { Sub } from "./sub";
import { Dispatch } from "./dispatch";
import { EffectManager, createGetEffectManager } from "./effect-manager";
import { GatheredEffects, gatherEffects } from "./effect";
export interface Program<S, A, V> {
readonly init: (url: string, key: Key) => readonly [S, Cmd<A>?];
/**
* A program represents the root of an application.
*/
export type Program<S, A, V> = {
readonly init: (url: string, key: () => void) => readonly [S, Cmd<A>?];
readonly update: (action: A, state: S) => readonly [S, Cmd<A>?];

@@ -10,4 +16,119 @@ readonly view: (props: { readonly state: S; readonly dispatch: Dispatch<A> }) => V;

readonly onUrlChange?: (url: string) => A;
};
/**
* This is the runtime that provides the main loop to run a Program.
* Given a Program and an array of EffectManagers it will start the program
* and progress the state each time the program calls update().
* You can use the returned function to terminate the program.
*/
export function run<S, A, V>(
program: Program<S, A, V>,
render: (view: V) => void,
effectManagers?: ReadonlyArray<EffectManager<unknown, unknown, unknown>>
): () => void {
const getEffectManager = createGetEffectManager(effectManagers || []);
const { update, view, subscriptions } = program;
let state: S;
const managerStates: { [home: string]: unknown } = {};
let isRunning = true;
let isProcessing = false;
const actionQueue: Array<{
dispatch: Dispatch<unknown>;
action: unknown;
}> = [];
function processActions(): void {
if (!isRunning || isProcessing) {
return;
}
isProcessing = true;
while (actionQueue.length > 0) {
const queuedAction = actionQueue.shift()!;
queuedAction.dispatch(queuedAction.action);
}
isProcessing = false;
}
const dispatchManager = (home: string) => (action: A): void => {
if (isRunning) {
const manager = getEffectManager(home);
const enqueueSelfAction = enqueueManagerAction(home);
managerStates[home] = manager.onSelfAction(enqueueAppAction, enqueueSelfAction, action, managerStates[home]);
}
};
function dispatchApp(action: A): void {
if (isRunning) {
change(update(action, state));
}
}
const enqueueManagerAction = (home: string) => (action: unknown): void => {
enqueueRaw(dispatchManager(home), action);
};
const enqueueAppAction = (action: A): void => {
enqueueRaw(dispatchApp, action);
};
function enqueueRaw(dispatch: Dispatch<A>, action: unknown): void {
if (isRunning) {
actionQueue.push({ dispatch, action });
processActions();
}
}
function change(change: readonly [S, Cmd<A>?]): void {
state = change[0];
const cmd = change[1];
const sub = subscriptions && subscriptions(state);
const gatheredEffects: GatheredEffects<A> = {};
cmd && gatherEffects(getEffectManager, gatheredEffects, true, cmd); // eslint-disable-line no-unused-expressions
sub && gatherEffects(getEffectManager, gatheredEffects, false, sub); // eslint-disable-line no-unused-expressions
for (const home of Object.keys(gatheredEffects)) {
const { cmds, subs } = gatheredEffects[home];
const manager = getEffectManager(home);
managerStates[home] = manager.onEffects(
enqueueAppAction,
enqueueManagerAction(home),
cmds,
subs,
managerStates[home]
);
}
render(view({ state, dispatch: enqueueAppAction }));
}
function setup(): void {
window.addEventListener("popstate", key);
// eslint-disable-next-line no-unused-expressions
window.navigator.userAgent.indexOf("Trident") < 0 || window.addEventListener("hashchange", key);
}
function teardown(): void {
window.removeEventListener("popstate", key);
}
function key(): void {
if (program.onUrlChange) {
enqueueAppAction(program.onUrlChange(getCurrentUrl()));
}
}
setup();
change(program.init(getCurrentUrl(), key));
return function end(): void {
if (isRunning) {
isRunning = false;
teardown();
}
};
}
export type Key = () => void;
function getCurrentUrl(): string {
// return window.location.href;
return window.location.pathname;
}
SocketSocket SOC 2 Logo

Product

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

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc