diff --git a/README.md b/README.md index dde30df6b2..6a85712b72 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,13 @@ difficulties, many combining functionality from several packages) in the | Projects | Version | | |----|----|----| | [`@thi.ng/api`](./packages/api) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/api.svg)](https://www.npmjs.com/package/@thi.ng/api) | [changelog](./packages/api/CHANGELOG.md) | +| [`@thi.ng/associative`](./packages/associative) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/associative.svg)](https://www.npmjs.com/package/@thi.ng/associative) | [changelog](./packages/associative/CHANGELOG.md) | | [`@thi.ng/atom`](./packages/atom) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/atom.svg)](https://www.npmjs.com/package/@thi.ng/atom) | [changelog](./packages/atom/CHANGELOG.md) | | [`@thi.ng/bitstream`](./packages/bitstream) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/bitstream.svg)](https://www.npmjs.com/package/@thi.ng/bitstream) | [changelog](./packages/bitstream/CHANGELOG.md) | | [`@thi.ng/checks`](./packages/checks) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/checks.svg)](https://www.npmjs.com/package/@thi.ng/checks) | [changelog](./packages/checks/CHANGELOG.md) | | [`@thi.ng/csp`](./packages/csp) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/csp.svg)](https://www.npmjs.com/package/@thi.ng/csp) | [changelog](./packages/csp/CHANGELOG.md) | | [`@thi.ng/dcons`](./packages/dcons) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/dcons.svg)](https://www.npmjs.com/package/@thi.ng/dcons) | [changelog](./packages/dcons/CHANGELOG.md) | +| [`@thi.ng/dgraph`](./packages/dgraph) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/dgraph.svg)](https://www.npmjs.com/package/@thi.ng/dgraph) | [changelog](./packages/dgraph/CHANGELOG.md) | | [`@thi.ng/diff`](./packages/diff) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/diff.svg)](https://www.npmjs.com/package/@thi.ng/diff) | [changelog](./packages/diff/CHANGELOG.md) | | [`@thi.ng/hdom`](./packages/hdom) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/hdom.svg)](https://www.npmjs.com/package/@thi.ng/hdom) | [changelog](./packages/hdom/CHANGELOG.md) | | [`@thi.ng/hdom-components`](./packages/hdom-components) | [![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/hdom-components.svg)](https://www.npmjs.com/package/@thi.ng/hdom-components) | [changelog](./packages/hdom-components/CHANGELOG.md) | diff --git a/assets/deps.png b/assets/deps.png index e29db166cf..b0628abce2 100644 Binary files a/assets/deps.png and b/assets/deps.png differ diff --git a/examples/router-basics/src/app.ts b/examples/router-basics/src/app.ts index 009a66e56d..138121309d 100644 --- a/examples/router-basics/src/app.ts +++ b/examples/router-basics/src/app.ts @@ -8,6 +8,8 @@ import { EVENT_ROUTE_CHANGED } from "@thi.ng/router/api"; import { HTMLRouter } from "@thi.ng/router/history"; import { AppConfig, ViewSpec, AppViews, AppContext } from "./api"; +import * as ev from "./events"; +import * as fx from "./effects"; import { nav } from "./components/nav"; import { debugContainer } from "./components/debug-container"; @@ -27,9 +29,6 @@ import { debugContainer } from "./components/debug-container"; */ export class App { - static readonly EV_ROUTE_TO = "route-to"; - static readonly FX_ROUTE_TO = "route-to"; - config: AppConfig; ctx: AppContext; state: Atom; @@ -51,10 +50,10 @@ export class App { ); this.ctx.bus.addHandlers({ [EVENT_ROUTE_CHANGED]: valueSetter("route"), - [App.EV_ROUTE_TO]: (_, [__, route]) => ({ [App.FX_ROUTE_TO]: route }) + [ev.ROUTE_TO]: (_, [__, route]) => ({ [ev.ROUTE_TO]: route }) }); this.ctx.bus.addEffect( - App.FX_ROUTE_TO, + fx.ROUTE_TO, ([id, params]) => this.router.routeTo(this.router.format(id, params)) ); this.addViews({ @@ -63,7 +62,7 @@ export class App { "route.id", (id) => (this.config.components[id] || - (() => ["div", `missing component for route: ${id}`]))(this.ctx, this.config.ui) + (() => ["div", `missing component for route: ${id}`]))(this.ctx) ] }); } @@ -75,12 +74,13 @@ export class App { * @param specs */ addViews(specs: IObjectOf) { + const views = this.ctx.views; for (let id in specs) { const spec = specs[id]; if (isArray(spec)) { - this.ctx.views[id] = this.state.addView(spec[0], spec[1]); + views[id] = this.state.addView(spec[0], spec[1]); } else { - this.ctx.views[id] = this.state.addView(spec); + views[id] = this.state.addView(spec); } } } diff --git a/examples/router-basics/src/components/all-users.ts b/examples/router-basics/src/components/all-users.ts index 927cd52093..18eaa5f742 100644 --- a/examples/router-basics/src/components/all-users.ts +++ b/examples/router-basics/src/components/all-users.ts @@ -1,5 +1,6 @@ import { AppContext, StatusType, User } from "../api"; -import { EV_LOAD_USER_LIST, EV_SET_STATUS, ROUTE_USER_PROFILE } from "../config"; +import { LOAD_USER_LIST, SET_STATUS } from "../events"; +import { USER_PROFILE } from "../routes"; import { routeLink } from "./route-link"; import { status } from "./status"; @@ -13,8 +14,8 @@ import { status } from "./status"; export function allUsers(ctx: AppContext) { ctx.bus.dispatch( ctx.views.userlist.deref().length ? - [EV_SET_STATUS, [StatusType.SUCCESS, "list loaded from cache", true]] : - [EV_LOAD_USER_LIST] + [SET_STATUS, [StatusType.SUCCESS, "list loaded from cache", true]] : + [LOAD_USER_LIST] ); return ["div", status, userList]; } @@ -49,7 +50,7 @@ function user(ctx: AppContext, user: User, cached: boolean) { ["img", { ...ui.thumb, src: user.img }]], ["div", ui.body, ["h1", ui.title, - [routeLink, ROUTE_USER_PROFILE.id, { id: user.id }, null, user.name]], + [routeLink, USER_PROFILE.id, { id: user.id }, null, user.name]], ["h2", ui.subtitle, `@${user.alias}`]], cached ? ["div", ui.meta, "cached"] : diff --git a/examples/router-basics/src/components/debug-container.ts b/examples/router-basics/src/components/debug-container.ts index 00c6365f4e..321821e40a 100644 --- a/examples/router-basics/src/components/debug-container.ts +++ b/examples/router-basics/src/components/debug-container.ts @@ -1,9 +1,9 @@ import { AppContext } from "../api"; -import { EV_TOGGLE_DEBUG } from "../config"; +import { TOGGLE_DEBUG } from "../events"; import { eventLink } from "./event-link"; /** - * Collapsable component showing stringified app state. + * Collapsible component showing stringified app state. * * @param ctx injected context object * @param debug @@ -11,7 +11,7 @@ import { eventLink } from "./event-link"; */ export function debugContainer(ctx: AppContext, debug: any, json: string) { return ["div#debug", ctx.ui.column.debug[debug], - [eventLink, [EV_TOGGLE_DEBUG], ctx.ui.debugToggle, + [eventLink, [TOGGLE_DEBUG], ctx.ui.debugToggle, debug ? "close \u25bc" : "open \u25b2"], ["pre", ctx.ui.code, json] ]; diff --git a/examples/router-basics/src/components/nav.ts b/examples/router-basics/src/components/nav.ts index f6eb3fe54e..57480a3041 100644 --- a/examples/router-basics/src/components/nav.ts +++ b/examples/router-basics/src/components/nav.ts @@ -1,10 +1,10 @@ import { AppContext } from "../api"; -import { ROUTE_USER_LIST, ROUTE_HOME, ROUTE_CONTACT } from "../config"; +import { USER_LIST, HOME, CONTACT } from "../routes"; import { routeLink } from "./route-link"; /** - * Main nav component with hardcoded routes. + * Main nav component with hard coded routes. * * @param ctx injected context object */ @@ -13,9 +13,9 @@ export function nav(ctx: AppContext) { return ["nav", ["h1", ui.title, "Demo app"], ["div", ui.inner, - [routeLink, ROUTE_HOME.id, null, ui.link, "Home"], - [routeLink, ROUTE_USER_LIST.id, null, ui.link, "Users"], - [routeLink, ROUTE_CONTACT.id, null, ui.linkLast, "Contact"], + [routeLink, HOME.id, null, ui.link, "Home"], + [routeLink, USER_LIST.id, null, ui.link, "Users"], + [routeLink, CONTACT.id, null, ui.linkLast, "Contact"], ] ]; } diff --git a/examples/router-basics/src/components/route-link.ts b/examples/router-basics/src/components/route-link.ts index 82c7a2918c..7a4a4a3b99 100644 --- a/examples/router-basics/src/components/route-link.ts +++ b/examples/router-basics/src/components/route-link.ts @@ -1,5 +1,5 @@ -import { App } from "../app"; import { AppContext } from "../api"; +import { ROUTE_TO } from "../events"; /** * Customizable hyperlink component emitting EV_ROUTE_TO event when clicked. @@ -16,7 +16,7 @@ export function routeLink(ctx: AppContext, routeID: PropertyKey, routeParams: an ...attribs, onclick: (e) => { e.preventDefault(); - ctx.bus.dispatch([App.EV_ROUTE_TO, [routeID, routeParams]]); + ctx.bus.dispatch([ROUTE_TO, [routeID, routeParams]]); } }, body]; diff --git a/examples/router-basics/src/components/user-profile.ts b/examples/router-basics/src/components/user-profile.ts index a79009db21..32316f8e86 100644 --- a/examples/router-basics/src/components/user-profile.ts +++ b/examples/router-basics/src/components/user-profile.ts @@ -1,5 +1,5 @@ import { StatusType, AppContext } from "../api"; -import { EV_LOAD_USER, EV_SET_STATUS } from "../config"; +import { LOAD_USER, SET_STATUS } from "../events"; import { status } from "./status"; @@ -13,8 +13,8 @@ export function userProfile(ctx: AppContext) { const id = ctx.views.route.deref().params.id; ctx.bus.dispatch( ctx.views.users.deref()[id] ? - [EV_SET_STATUS, [StatusType.SUCCESS, "loaded from cache", true]] : - [EV_LOAD_USER, id]); + [SET_STATUS, [StatusType.SUCCESS, "loaded from cache", true]] : + [LOAD_USER, id]); return ["div", [status], [userCard, id]]; } diff --git a/examples/router-basics/src/config.ts b/examples/router-basics/src/config.ts index 7a5818890e..e2b8cb31bb 100644 --- a/examples/router-basics/src/config.ts +++ b/examples/router-basics/src/config.ts @@ -1,72 +1,15 @@ import { Event, FX_DISPATCH_ASYNC, FX_DISPATCH_NOW, EV_SET_VALUE, FX_DELAY } from "@thi.ng/interceptors/api"; -import { valueSetter, valueUpdater, trace } from "@thi.ng/interceptors/interceptors"; -import { Route, RouteMatch, EVENT_ROUTE_CHANGED } from "@thi.ng/router/api"; +import { valueUpdater, trace } from "@thi.ng/interceptors/interceptors"; import { AppConfig, StatusType } from "./api"; +import * as ev from "./events"; +import * as fx from "./effects"; +import * as routes from "./routes"; import { home } from "./components/home"; import { allUsers } from "./components/all-users"; import { userProfile } from "./components/user-profile"; import { contact } from "./components/contact"; -import { App } from "./app"; - -// route definitions: -// routes are 1st class objects and used directly throughout the app -// without ever referring to their specific string representation - -// the `match` arrays specify the individual route elements -// docs here: -// https://github.com/thi-ng/umbrella/blob/master/packages/router/ -// https://github.com/thi-ng/umbrella/blob/master/packages/router/src/api.ts#L31 - -export const ROUTE_HOME: Route = { - id: "home", - match: ["home"] -}; - -export const ROUTE_CONTACT: Route = { - id: "contact", - match: ["contact"] -}; - -export const ROUTE_USER_LIST: Route = { - id: "user-list", - match: ["users"], -}; - -// this is a parametric route w/ parameter coercion & validation -// if coercion or validation fails, the route will not be matched -// if no other route matches, the configured default route will -// be used (see full router config further below) - -export const ROUTE_USER_PROFILE: Route = { - id: "user-profile", - match: ["users", "?id"], - validate: { - id: { - coerce: (x) => parseInt(x), - check: (x) => x > 0 && x < 100 - } - } -}; - -// best practice tip: define event & effect names as consts or enums and -// avoid hardcoded strings for more safety and easier refactoring - -export const EV_DONE = "done"; -export const EV_ERROR = "error"; -export const EV_LOAD_USER = "load-user"; -export const EV_LOAD_USER_ERROR = "load-user-error" -export const EV_LOAD_USER_LIST = "load-users"; -export const EV_RECEIVE_USER = "receive-user"; -export const EV_RECEIVE_USERS = "receive-users"; -export const EV_SET_STATUS = "set-status"; -export const EV_TOGGLE_DEBUG = "toggle-debug"; - -// side effect IDs (these don't / shouldn't need to be exported. other -// parts of the app should only use events) - -const FX_JSON = "load-json"; // main App configuration export const CONFIG: AppConfig = { @@ -77,19 +20,19 @@ export const CONFIG: AppConfig = { // use URI hash for routes (KISS) useFragment: true, // route ID if no other matches (MUST be non-parametric!) - defaultRouteID: ROUTE_HOME.id, + defaultRouteID: routes.HOME.id, // IMPORTANT: rules with common prefixes MUST be specified in // order of highest precision / longest path routes: [ - ROUTE_HOME, - ROUTE_CONTACT, - ROUTE_USER_PROFILE, - ROUTE_USER_LIST, + routes.HOME, + routes.CONTACT, + routes.USER_PROFILE, + routes.USER_LIST, ] }, // event handlers events are queued and batch processed in app's RAF - // renderloop event handlers can be single functions, interceptor + // render loop event handlers can be single functions, interceptor // objects with `pre`/`post` keys or arrays of either. // the event handlers' only task is to transform the event into a @@ -101,77 +44,74 @@ export const CONFIG: AppConfig = { events: { // sets status to "done" - [EV_DONE]: () => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.DONE, "done"]] + [ev.DONE]: () => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.DONE, "done"]] }), // sets status to thrown error's message - [EV_ERROR]: (_, [__, err]) => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.ERROR, err.message]], + [ev.ERROR]: (_, [__, err]) => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.ERROR, err.message]], }), // triggers loading of JSON for single user, sets status - [EV_LOAD_USER]: (_, [__, id]) => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.INFO, `loading user data...`]], - [FX_DISPATCH_ASYNC]: [FX_JSON, `assets/user-${id}.json`, EV_RECEIVE_USER, EV_LOAD_USER_ERROR] + [ev.LOAD_USER]: (_, [__, id]) => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.INFO, `loading user data...`]], + [FX_DISPATCH_ASYNC]: [fx.JSON, `assets/user-${id}.json`, ev.RECEIVE_USER, ev.LOAD_USER_ERROR] }), // triggered after successful IO // stores received user data under `users.{id}`, sets status // note: we assign multiple value/events as array to the FX_DISPATCH_NOW side effect - [EV_RECEIVE_USER]: (_, [__, json]) => ({ + [ev.RECEIVE_USER]: (_, [__, json]) => ({ [FX_DISPATCH_NOW]: [ [EV_SET_VALUE, [["users", json.id], json]], - [EV_SET_STATUS, [StatusType.SUCCESS, "JSON succesfully loaded", true]] + [ev.SET_STATUS, [StatusType.SUCCESS, "JSON successfully loaded", true]] ], }), // error event for user profile IO requests (i.e. in this demo for user ID 3) // set status, then redirects to /users after 1sec - [EV_LOAD_USER_ERROR]: (_, [__, err]) => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.ERROR, err.message]], - [FX_DISPATCH_ASYNC]: [FX_DELAY, [1000, [ROUTE_USER_LIST.id]], App.EV_ROUTE_TO, EV_ERROR], + [ev.LOAD_USER_ERROR]: (_, [__, err]) => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.ERROR, err.message]], + [FX_DISPATCH_ASYNC]: [FX_DELAY, [1000, [routes.USER_LIST.id]], ev.ROUTE_TO, ev.ERROR], }), // triggers loading of JSON summary of all users, sets status - [EV_LOAD_USER_LIST]: () => ({ - [FX_DISPATCH_NOW]: [EV_SET_STATUS, [StatusType.INFO, `loading user data...`]], - [FX_DISPATCH_ASYNC]: [FX_JSON, `assets/users.json`, EV_RECEIVE_USERS, EV_ERROR] + [ev.LOAD_USER_LIST]: () => ({ + [FX_DISPATCH_NOW]: [ev.SET_STATUS, [StatusType.INFO, `loading user data...`]], + [FX_DISPATCH_ASYNC]: [fx.JSON, `assets/users.json`, ev.RECEIVE_USERS, ev.ERROR] }), // triggered after successful IO // note: we assign multiple value/events as array to the FX_DISPATCH_NOW side effect - [EV_RECEIVE_USERS]: (_, [__, json]) => ({ + [ev.RECEIVE_USERS]: (_, [__, json]) => ({ [FX_DISPATCH_NOW]: [ [EV_SET_VALUE, ["userlist", json]], - [EV_SET_STATUS, [StatusType.SUCCESS, "JSON succesfully loaded", true]] + [ev.SET_STATUS, [StatusType.SUCCESS, "JSON successfully loaded", true]] ], }), - // stores current route details - [EVENT_ROUTE_CHANGED]: valueSetter("route"), - // stores status (a tuple of `[type, message, done?]`) in app state // if status type != DONE & `done` == true, also triggers delayed EV_DONE // Note: we inject the `trace` interceptor to log the event to the console - [EV_SET_STATUS]: [ + [ev.SET_STATUS]: [ trace, (_, [__, status]) => ({ [FX_DISPATCH_NOW]: [EV_SET_VALUE, ["status", status]], [FX_DISPATCH_ASYNC]: (status[0] !== StatusType.DONE && status[2]) ? - [FX_DELAY, [1000], EV_DONE, EV_ERROR] : + [FX_DELAY, [1000], ev.DONE, ev.ERROR] : undefined }) ], // toggles debug state flag on/off - [EV_TOGGLE_DEBUG]: valueUpdater("debug", (x) => x ^ 1) + [ev.TOGGLE_DEBUG]: valueUpdater("debug", (x) => x ^ 1) }, // side effects effects: { // generic JSON loader via fetch() - [FX_JSON]: (req) => + [fx.JSON]: (req) => fetch(req).then((resp) => { if (!resp.ok) { throw new Error(resp.statusText); @@ -184,10 +124,10 @@ export const CONFIG: AppConfig = { // those functions are called automatically by the app's root component // base on the currently active route components: { - [ROUTE_HOME.id]: home, - [ROUTE_CONTACT.id]: contact, - [ROUTE_USER_LIST.id]: allUsers, - [ROUTE_USER_PROFILE.id]: userProfile, + [routes.HOME.id]: home, + [routes.CONTACT.id]: contact, + [routes.USER_LIST.id]: allUsers, + [routes.USER_PROFILE.id]: userProfile, }, // DOM root element (or ID) diff --git a/examples/router-basics/src/effects.ts b/examples/router-basics/src/effects.ts new file mode 100644 index 0000000000..9bd410f132 --- /dev/null +++ b/examples/router-basics/src/effects.ts @@ -0,0 +1,7 @@ +// best practice tip: define event & effect names as consts or enums +// and avoid hardcoded strings for more safety and easier refactoring +// also see pre-defined event handlers & interceptors in @thi.ng/atom: +// https://github.com/thi-ng/umbrella/blob/master/packages/interceptors/src/api.ts#L14 + +export const JSON = "load-json"; +export const ROUTE_TO = "route-to"; \ No newline at end of file diff --git a/examples/router-basics/src/events.ts b/examples/router-basics/src/events.ts new file mode 100644 index 0000000000..5403211ab7 --- /dev/null +++ b/examples/router-basics/src/events.ts @@ -0,0 +1,13 @@ +// best practice tip: define event & effect names as consts or enums and +// avoid hardcoded strings for more safety and easier refactoring + +export const DONE = "done"; +export const ERROR = "error"; +export const LOAD_USER = "load-user"; +export const LOAD_USER_ERROR = "load-user-error" +export const LOAD_USER_LIST = "load-users"; +export const RECEIVE_USER = "receive-user"; +export const RECEIVE_USERS = "receive-users"; +export const ROUTE_TO = "route-to"; +export const SET_STATUS = "set-status"; +export const TOGGLE_DEBUG = "toggle-debug"; diff --git a/examples/router-basics/src/routes.ts b/examples/router-basics/src/routes.ts new file mode 100644 index 0000000000..01a68be3fb --- /dev/null +++ b/examples/router-basics/src/routes.ts @@ -0,0 +1,41 @@ +import { Route } from "@thi.ng/router/api"; + +// route definitions: +// routes are 1st class objects and used directly throughout the app +// without ever referring to their specific string representation + +// the `match` arrays specify the individual route elements +// docs here: +// https://github.com/thi-ng/umbrella/blob/master/packages/router/ +// https://github.com/thi-ng/umbrella/blob/master/packages/router/src/api.ts#L31 + +export const HOME: Route = { + id: "home", + match: ["home"] +}; + +export const CONTACT: Route = { + id: "contact", + match: ["contact"] +}; + +export const USER_LIST: Route = { + id: "user-list", + match: ["users"], +}; + +// this is a parametric route w/ parameter coercion & validation +// if coercion or validation fails, the route will not be matched +// if no other route matches, the configured default route will +// be used (see full router config further below) + +export const USER_PROFILE: Route = { + id: "user-profile", + match: ["users", "?id"], + validate: { + id: { + coerce: (x) => parseInt(x), + check: (x) => x > 0 && x < 100 + } + } +}; diff --git a/lerna.json b/lerna.json index 10a6c65a42..e220b15a35 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "lerna": "2.8.0", + "lerna": "2.10.1", "packages": [ "packages/*" ], diff --git a/package.json b/package.json index b8b8c344b1..61f16ddc21 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ ], "devDependencies": { "benchmark": "^2.1.4", - "lerna": "^2.9.1", + "lerna": "^2.10.1", "nyc": "^11.6.0", "tslint": "^5.9.1", "typescript": "^2.8.1", diff --git a/packages/associative/.npmignore b/packages/associative/.npmignore new file mode 100644 index 0000000000..d703bda97a --- /dev/null +++ b/packages/associative/.npmignore @@ -0,0 +1,10 @@ +build +coverage +dev +doc +src* +test +.nyc_output +tsconfig.json +*.tgz +*.html diff --git a/packages/associative/CHANGELOG.md b/packages/associative/CHANGELOG.md new file mode 100644 index 0000000000..085ecb1a81 --- /dev/null +++ b/packages/associative/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log + +All notable changes to this project will be documented in this file. +See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + + +# 0.2.0 (2018-04-10) + + +### Features + +* **associative:** add EquivSet.first() ([0dc9f64](https://github.com/thi-ng/umbrella/commit/0dc9f64)) +* **associative:** initial import [@thi](https://github.com/thi).ng/associative ([cc70dbc](https://github.com/thi-ng/umbrella/commit/cc70dbc)) diff --git a/packages/associative/LICENSE b/packages/associative/LICENSE new file mode 100644 index 0000000000..8dada3edaf --- /dev/null +++ b/packages/associative/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/associative/README.md b/packages/associative/README.md new file mode 100644 index 0000000000..00520d717c --- /dev/null +++ b/packages/associative/README.md @@ -0,0 +1,101 @@ +# @thi.ng/associative + +[![npm (scoped)](https://img.shields.io/npm/v/@thi.ng/associative.svg)](https://www.npmjs.com/package/@thi.ng/associative) + +## About + +This package provided alternative Set & Map data type implementations +with customizable equality semantics, as well as common operations +working with these types: + +- `EquivSet` & `EquivMap` types implement the full ES6 Set/Map APIs +- Polymorphic set operations (union, intersection, difference) - works + with both native and custom Sets and retains their types +- Natural & selective + [joins](https://en.wikipedia.org/wiki/Relational_algebra#Joins_and_join-like_operators) + (incl. key renaming, ported from Clojure) +- Key-value pair inversion for maps and vanilla objects (`K -> V => V -> K`) +- Single or multi-property index generation for maps and objects +- Key selection & renaming for maps and objects + +### Why? + +The native ES6 implementations use object reference identity to +determine key containment, but often it's more practical and useful to +use equivalent value semantics for this purpose, especially when keys +are structured data (arrays / objects). + +**Note**: It's the user's responsibility to ensure the inserted keys are +kept immutable (even if technically they're not). + +### Comparison + +```ts +// two objects w/ equal values +const a = { a: 1 }; +const b = { a: 1 }; + +// using native implementations +const set = new Set(); +set.add(a); +set.has(b); +// false + +const map = new Map(); +map.set(a, "foo"); +map.get(b); +// undefined +``` + +```ts +import { EquivSet, EquivMap } from "@thi.ng/associative"; + +// using custom implementations +const set = new EquivSet(); +set.add(a); +set.has(b); +// true + +const map = new EquivMap(); +map.set(a, "foo"); +map.get(b); +// "foo" +``` + +## Installation + +``` +yarn add @thi.ng/associative +``` + +## Types + +### EquivSet + +This `Set` implementation uses +[@thi.ng/dcons](https://github.com/thi-ng/umbrella/tree/master/packages/dcons) +as backing storage for values and by default uses +[@thi.ng/api/equiv](https://github.com/thi-ng/umbrella/tree/master/packages/api/src/equiv.ts) +for equivalence checking. + +### EquivMap + +This `Map` implementation uses a native ES6 `Map` as backing storage for +key-value pairs and additional `EquivSet` for canonical keys. By default +it too uses +[@thi.ng/api/equiv](https://github.com/thi-ng/umbrella/tree/master/packages/api/src/equiv.ts) +for equivalence checking of keys. + +## Usage examples + +TODO... Please see +[tests](https://github.com/thi-ng/umbrella/tree/master/packages/associative/test/) +and documentation in source code for now... + +## Authors + +- Karsten Schmidt + +## License + +© 2017 - 2018 Karsten Schmidt // Apache Software License 2.0 diff --git a/packages/associative/package.json b/packages/associative/package.json new file mode 100644 index 0000000000..734c6b0da5 --- /dev/null +++ b/packages/associative/package.json @@ -0,0 +1,45 @@ +{ + "name": "@thi.ng/associative", + "version": "0.2.0", + "description": "Alternative Set & Map data type implementations with customizable equality semantics & supporting operations", + "main": "./index.js", + "typings": "./index.d.ts", + "repository": "https://github.com/thi-ng/umbrella", + "author": "Karsten Schmidt ", + "license": "Apache-2.0", + "scripts": { + "build": "yarn run clean && tsc --declaration", + "clean": "rm -rf *.js *.d.ts build doc", + "cover": "yarn test && nyc report --reporter=lcov", + "doc": "node_modules/.bin/typedoc --mode modules --out doc src", + "pub": "yarn run build && yarn publish --access public", + "test": "rm -rf build && tsc -p test && nyc mocha build/test/*.js" + }, + "devDependencies": { + "@types/mocha": "^5.0.0", + "@types/node": "^9.6.1", + "mocha": "^5.0.5", + "nyc": "^11.6.0", + "typedoc": "^0.11.1", + "typescript": "^2.8.1" + }, + "dependencies": { + "@thi.ng/api": "^2.1.0", + "@thi.ng/dcons": "^0.2.0" + }, + "keywords": [ + "data structures", + "difference", + "equality", + "ES6", + "intersection", + "join", + "map", + "set", + "typescript", + "union" + ], + "publishConfig": { + "access": "public" + } +} diff --git a/packages/associative/src/api.ts b/packages/associative/src/api.ts new file mode 100644 index 0000000000..2bcb9a0591 --- /dev/null +++ b/packages/associative/src/api.ts @@ -0,0 +1 @@ +export const SEMAPHORE = Symbol("SEMAPHORE"); diff --git a/packages/associative/src/difference.ts b/packages/associative/src/difference.ts new file mode 100644 index 0000000000..b3af70cb30 --- /dev/null +++ b/packages/associative/src/difference.ts @@ -0,0 +1,12 @@ +import { copy, empty } from "./utils"; + +export function difference(a: Set, b: Set): Set { + if (a === b) { + return empty(a, Set); + } + const res = copy(a, Set); + for (let i of b) { + res.delete(i); + } + return res; +} diff --git a/packages/associative/src/equiv-map.ts b/packages/associative/src/equiv-map.ts new file mode 100644 index 0000000000..1418b4b861 --- /dev/null +++ b/packages/associative/src/equiv-map.ts @@ -0,0 +1,149 @@ +import { ICopy, IEmpty, IEquiv, IObjectOf, Predicate2 } from "@thi.ng/api/api"; +import { equiv } from "@thi.ng/api/equiv"; + +import { SEMAPHORE } from "./api"; +import { EquivSet } from "./equiv-set"; + +export class EquivMap extends Map implements + Iterable<[K, V]>, + ICopy>, + IEmpty>, + IEquiv { + + static fromObject(obj: IObjectOf): EquivMap { + const m = new EquivMap(); + for (let k in obj) { + if (obj.hasOwnProperty(k)) { + m.set(k, obj[k]); + } + } + return m; + } + + protected _keys: EquivSet; + protected _map: Map; + + constructor(pairs?: Iterable<[K, V]>, eq: Predicate2 = equiv) { + super(); + this._keys = new EquivSet(null, eq); + this._map = new Map(); + Object.defineProperties(this, { + _keys: { enumerable: false }, + _map: { enumerable: false } + }); + if (pairs) { + this.into(pairs); + } + } + + [Symbol.iterator]() { + return this.entries(); + } + + get [Symbol.species]() { + return EquivMap; + } + + get size() { + return this._keys.size; + } + + clear() { + this._keys.clear(); + this._map.clear(); + } + + empty() { + return new EquivMap(null, (this._keys)._equiv); + } + + copy() { + const m = new EquivMap(null, (this._keys)._equiv); + m._keys = this._keys.copy(); + m._map = new Map(this._map); + return m; + } + + equiv(o: any) { + if (this === o) { + return true; + } + if (!(o instanceof Map)) { + return false; + } + if (this.size !== o.size) { + return false; + } + for (let k of this._map.keys()) { + if (!equiv(o.get(k), this._map.get(k))) { + return false; + } + } + return true; + } + + delete(key: K) { + key = this._keys.get(key, SEMAPHORE); + if (key !== SEMAPHORE) { + this._map.delete(key); + this._keys.delete(key); + return true; + } + return false; + } + + dissoc(...keys: K[]) { + for (let k of keys) { + this.delete(k); + } + return this; + } + + forEach(fn: (val: V, key: K, map: Map) => void, thisArg?: any) { + for (let pair of this._map) { + fn.call(thisArg, pair[1], pair[0], this); + } + } + + get(key: K, notFound?: any) { + key = this._keys.get(key, SEMAPHORE); + if (key !== SEMAPHORE) { + return this._map.get(key); + } + return notFound; + } + + has(key: K) { + return this._keys.has(key); + } + + set(key: K, value: V) { + const k = this._keys.get(key, SEMAPHORE); + if (k !== SEMAPHORE) { + this._map.set(k, value); + } else { + this._keys.add(key); + this._map.set(key, value); + } + return this; + } + + into(pairs: Iterable<[K, V]>) { + for (let p of pairs) { + this.set(p[0], p[1]); + } + return this; + } + + entries() { + return this._map.entries(); + } + + keys() { + return this._keys.keys(); + } + + values() { + return this._map.values(); + } +} \ No newline at end of file diff --git a/packages/associative/src/equiv-set.ts b/packages/associative/src/equiv-set.ts new file mode 100644 index 0000000000..6d351a97a5 --- /dev/null +++ b/packages/associative/src/equiv-set.ts @@ -0,0 +1,163 @@ +import { ICopy, IEmpty, IEquiv, Predicate2 } from "@thi.ng/api/api"; +import { equiv } from "@thi.ng/api/equiv"; +import { DCons } from "@thi.ng/dcons"; +import { SEMAPHORE } from "./api"; + +/** + * An alternative set implementation to the native ES6 Set type. Uses + * customizable equality/equivalence predicate and so is more useful + * when dealing with structured data. Implements full API of native Set + * and by the default uses `@thi.ng/api/equiv` for equivalence checking. + * + * Additionally, the type also implements the `ICopy`, `IEmpty` and + * `IEquiv` interfaces itself. + */ +export class EquivSet extends Set implements + Iterable, + ICopy>, + IEmpty>, + IEquiv { + + protected _vals: DCons; + protected _equiv: Predicate2; + + constructor(vals?: Iterable, eq: Predicate2 = equiv) { + super(); + this._equiv = eq; + this._vals = new DCons(); + Object.defineProperties(this, { + _vals: { enumerable: false }, + _equiv: { enumerable: false } + }); + vals && this.into(vals); + } + + *[Symbol.iterator]() { + yield* this._vals; + } + + get [Symbol.species]() { + return EquivSet; + } + + get size() { + return this._vals.length; + } + + copy() { + const s = new EquivSet(null, this._equiv); + s._vals = this._vals.copy(); + return s; + } + + empty() { + return new EquivSet(null, this._equiv); + } + + clear() { + this._vals.clear(); + } + + first() { + if (this.size) { + return this._vals.head.value; + } + } + + add(x: T) { + !this.has(x) && this._vals.push(x); + return this; + } + + into(xs: Iterable) { + for (let x of xs) { + this.add(x); + } + return this; + } + + has(x: T) { + return this.get(x, SEMAPHORE) !== SEMAPHORE; + } + + /** + * Returns the canonical value for `x`, if present. If the set + * contains no equivalent for `x`, returns `notFound`. + * + * @param x + * @param notFound + */ + get(x: T, notFound?: any) { + const eq = this._equiv; + let i = this._vals.head; + while (i) { + if (eq(i.value, x)) { + return i.value; + } + i = i.next; + } + return notFound; + } + + delete(x: T) { + const eq = this._equiv; + let i = this._vals.head; + while (i) { + if (eq(i.value, x)) { + this._vals.splice(i, 1); + return true; + } + i = i.next; + } + return false; + } + + disj(...xs: T[]) { + for (let x of xs) { + this.delete(x); + } + return this; + } + + equiv(o: any) { + if (this === o) { + return true; + } + if (!(o instanceof Set)) { + return false; + } + if (this.size !== o.size) { + return false; + } + let i = this._vals.head; + while (i) { + if (!o.has(i.value)) { + return false; + } + i = i.next; + } + return true; + } + + forEach(fn: (val: T, val2: T, set: Set) => void, thisArg?: any) { + let i = this._vals.head; + while (i) { + fn.call(thisArg, i.value, i.value, this); + i = i.next; + } + } + + *entries(): IterableIterator<[T, T]> { + for (let v of this._vals) { + yield [v, v]; + } + } + + *keys() { + yield* this._vals; + } + + *values() { + yield* this._vals; + } +} diff --git a/packages/associative/src/index.ts b/packages/associative/src/index.ts new file mode 100644 index 0000000000..5d3b2d8fb9 --- /dev/null +++ b/packages/associative/src/index.ts @@ -0,0 +1,13 @@ +export * from "./equiv-map"; +export * from "./equiv-set"; + +export * from "./difference"; +export * from "./intersection"; +export * from "./union"; + +export * from "./indexed"; +export * from "./invert"; +export * from "./join"; +export * from "./merge"; +export * from "./rename-keys"; +export * from "./select-keys"; diff --git a/packages/associative/src/indexed.ts b/packages/associative/src/indexed.ts new file mode 100644 index 0000000000..d67c428190 --- /dev/null +++ b/packages/associative/src/indexed.ts @@ -0,0 +1,21 @@ +import { EquivMap } from "./equiv-map"; +import { selectKeysObj } from "./select-keys"; + +/** + * + * @param records + * @param ks keys used for indexing + */ +export function indexed(records: Set, ks: PropertyKey[]) { + const res = new EquivMap>(); + let m, ik, rv; + for (m of records) { + ik = selectKeysObj(m, ks); + rv = res.get(ik); + if (!rv) { + res.set(ik, rv = new Set()); + } + rv.add(m); + } + return res; +} diff --git a/packages/associative/src/intersection.ts b/packages/associative/src/intersection.ts new file mode 100644 index 0000000000..31ec753705 --- /dev/null +++ b/packages/associative/src/intersection.ts @@ -0,0 +1,17 @@ +import { empty } from "./utils"; + +export function intersection(a: Set, b: Set): Set { + if (a === b) { + return a; + } + if (b.size < a.size) { + return intersection(b, a); + } + const res = empty(a, Set); + for (let i of b) { + if (a.has(i)) { + res.add(i); + } + } + return res; +} diff --git a/packages/associative/src/invert.ts b/packages/associative/src/invert.ts new file mode 100644 index 0000000000..b54f5b73f5 --- /dev/null +++ b/packages/associative/src/invert.ts @@ -0,0 +1,19 @@ +import { IObjectOf } from "@thi.ng/api/api"; + +import { empty } from "./utils"; + +export function invertMap(src: Map) { + const dest: Map = empty(src, Map); + for (let p of src) { + dest.set(p[1], p[0]); + } + return dest; +} + +export function invertObj(src: IObjectOf) { + const dest: IObjectOf = {}; + for (let k in src) { + dest[src[k]] = k; + } + return dest; +} diff --git a/packages/associative/src/join.ts b/packages/associative/src/join.ts new file mode 100644 index 0000000000..59a3572b1d --- /dev/null +++ b/packages/associative/src/join.ts @@ -0,0 +1,68 @@ +import { IObjectOf } from "@thi.ng/api/api"; + +import { EquivSet } from "./equiv-set"; +import { intersection } from "./intersection"; +import { indexed } from "./indexed"; +import { invertObj } from "./invert"; +import { mergeObj } from "./merge"; +import { renameKeysObj } from "./rename-keys"; +import { selectKeysObj } from "./select-keys"; +import { empty, first, objValues } from "./utils"; + +export function join(xrel: Set, yrel: Set) { + if (xrel.size && yrel.size) { + const ks = [...intersection( + new Set(Object.keys(first(xrel) || {})), + new Set(Object.keys(first(yrel) || {}))) + ]; + let r, s; + if (xrel.size <= yrel.size) { + r = xrel; + s = yrel; + } else { + r = yrel; + s = xrel; + } + const idx = indexed(r, ks); + const res = new EquivSet(); + for (let x of s) { + const found = idx.get(selectKeysObj(x, ks)); + if (found) { + for (let f of found) { + res.add(mergeObj({ ...f }, x)); + } + } + } + return res; + } + return empty(xrel, Set); +} + +export function joinWith(xrel: Set, yrel: Set, km: IObjectOf) { + if (xrel.size && yrel.size) { + let r: Set, s: Set; + let k: IObjectOf; + if (xrel.size <= yrel.size) { + r = xrel; + s = yrel; + k = invertObj(km); + } else { + r = yrel; + s = xrel; + k = km; + } + const idx = indexed(r, objValues(k)); + const ks = Object.keys(k); + const res = new EquivSet(); + for (let x of s) { + const found = idx.get(renameKeysObj(selectKeysObj(x, ks), k)); + if (found) { + for (let f of found) { + res.add(mergeObj({ ...f }, x)); + } + } + } + return res; + } + return empty(xrel, Set); +} diff --git a/packages/associative/src/merge.ts b/packages/associative/src/merge.ts new file mode 100644 index 0000000000..7b5e582f62 --- /dev/null +++ b/packages/associative/src/merge.ts @@ -0,0 +1,12 @@ +export function mergeMaps(m: Map, ...maps: Map[]) { + for (let mm of maps) { + for (let p of mm) { + m.set(p[0], p[1]); + } + } + return m; +} + +export function mergeObj(m, ...maps: any[]) { + return Object.assign(m, ...maps); +} diff --git a/packages/associative/src/rename-keys.ts b/packages/associative/src/rename-keys.ts new file mode 100644 index 0000000000..ab9f68ad0e --- /dev/null +++ b/packages/associative/src/rename-keys.ts @@ -0,0 +1,10 @@ +import { IObjectOf } from "@thi.ng/api/api"; + +export function renameKeysObj(src: any, km: IObjectOf) { + const dest = {}; + for (let k in src) { + const kk = km[k]; + dest[kk !== undefined ? kk : k] = src[k]; + } + return dest; +} diff --git a/packages/associative/src/select-keys.ts b/packages/associative/src/select-keys.ts new file mode 100644 index 0000000000..936ab96a7b --- /dev/null +++ b/packages/associative/src/select-keys.ts @@ -0,0 +1,23 @@ +import { IObjectOf } from "@thi.ng/api/api"; + +import { empty } from "./utils"; + +export function selectKeysMap(src: Map, ks: Iterable): any { + const dest = empty(src, Map); + for (let k of ks) { + if (src.has(k)) { + dest.set(k, src.get(k)); + } + } + return dest; +} + +export function selectKeysObj(src: IObjectOf, ks: Iterable): any { + const dest: IObjectOf = {}; + for (let k of ks) { + if (src.hasOwnProperty(k)) { + dest[k] = src[k]; + } + } + return dest; +} diff --git a/packages/associative/src/union.ts b/packages/associative/src/union.ts new file mode 100644 index 0000000000..ce185e89eb --- /dev/null +++ b/packages/associative/src/union.ts @@ -0,0 +1,15 @@ +import { copy } from "./utils"; + +export function union(a: Set, b: Set): Set { + if (a === b) { + return a; + } + if (b.size > a.size) { + return union(b, a); + } + const res = copy(a, Set); + for (let i of b) { + res.add(i); + } + return res; +} \ No newline at end of file diff --git a/packages/associative/src/utils.ts b/packages/associative/src/utils.ts new file mode 100644 index 0000000000..be99465a24 --- /dev/null +++ b/packages/associative/src/utils.ts @@ -0,0 +1,15 @@ +import { implementsFunction } from "@thi.ng/checks/implements-function"; + +export const empty = (x, ctor) => implementsFunction(x, "empty") ? x.empty() : new (x[Symbol.species] || ctor)(); + +export const copy = (x, ctor) => implementsFunction(x, "copy") ? x.copy() : new (x[Symbol.species] || ctor)(x); + +export const first = (x: Iterable) => x[Symbol.iterator]().next().value; + +export const objValues = (src: any) => { + const vals = []; + for (let k in src) { + vals.push(src[k]); + } + return vals; +}; diff --git a/packages/associative/test/difference.ts b/packages/associative/test/difference.ts new file mode 100644 index 0000000000..a387f4414a --- /dev/null +++ b/packages/associative/test/difference.ts @@ -0,0 +1,39 @@ +import * as assert from "assert"; + +import { EquivSet } from "../src/equiv-set"; +import { difference } from "../src/difference"; + +describe("difference", () => { + + it("native (numbers)", () => { + const a = new Set([1, 2, 3, 4]); + const b = new Set([3, 4, 5]); + assert.deepEqual(difference(a, b), new Set([1, 2])); + }); + + it("equiv (numbers)", () => { + const a = new EquivSet([1, 2, 3, 4]); + const b = new EquivSet([3, 4, 5]); + assert.deepEqual(difference(a, b), new EquivSet([1, 2])); + }); + + it("native (obj)", () => { + const a = new Set([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]); + const b = new Set([{ a: 3 }, { a: 4 }, { a: 5 }]); + const d = difference(a, b); + assert.equal(d.size, 4); // verifies that it doesn't work w/ native sets! + assert.deepEqual(d, a); + assert.notStrictEqual(d, a); + assert.notStrictEqual(d, b); + }); + + it("equiv (obj)", () => { + const a = new EquivSet([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]); + const b = new EquivSet([{ a: 3 }, { a: 4 }, { a: 5 }]); + const d = difference(a, b); + assert.equal(d.size, 2); + assert.deepEqual(d, new Set([{ a: 1 }, { a: 2 }])); + assert.notStrictEqual(d, a); + assert.notStrictEqual(d, b); + }); +}); diff --git a/packages/associative/test/intersection.ts b/packages/associative/test/intersection.ts new file mode 100644 index 0000000000..c67484cd47 --- /dev/null +++ b/packages/associative/test/intersection.ts @@ -0,0 +1,35 @@ +import * as assert from "assert"; + +import { EquivSet } from "../src/equiv-set"; +import { intersection } from "../src/intersection"; + +describe("intersection", () => { + + it("native (numbers)", () => { + const a = new Set([1, 2, 3, 4]); + const b = new Set([3, 4, 5, 6]); + assert.deepEqual(intersection(a, b), new Set([3, 4])); + }); + + it("equiv (numbers)", () => { + const a = new EquivSet([1, 2, 3, 4]); + const b = new EquivSet([3, 4, 5, 6]); + assert.deepEqual(intersection(a, b), new EquivSet([3, 4])); + }); + + it("native (obj)", () => { + const a = new Set([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]); + const b = new Set([{ a: 3 }, { a: 4 }, { a: 5 }]); + const i = intersection(a, b); + assert.deepEqual(i, new Set()); // verifies that it doesn't work w/ native sets! + assert.notStrictEqual(i, a); + assert.notStrictEqual(i, b); + }); + + it("equiv (obj)", () => { + const a = new EquivSet([{ a: 1 }, { a: 2 }, { a: 3 }, { a: 4 }]); + const b = new EquivSet([{ a: 3 }, { a: 4 }, { a: 5 }]); + const i = intersection(a, b); + assert.deepEqual(i, new EquivSet([{ a: 3 }, { a: 4 }])); + }); +}); diff --git a/packages/associative/test/join.ts b/packages/associative/test/join.ts new file mode 100644 index 0000000000..2c5616d211 --- /dev/null +++ b/packages/associative/test/join.ts @@ -0,0 +1,36 @@ +import * as assert from "assert"; + +import { EquivSet } from "../src/equiv-set"; +import { join, joinWith } from "../src/join"; + +describe("join", () => { + it("simple", () => { + const a = new EquivSet([{ a: 1 }, { a: 2 }]); + const b = new EquivSet([{ b: 1 }, { b: 2 }]); + assert.deepEqual(join(a, b), new EquivSet([{ a: 1, b: 1 }, { a: 2, b: 1 }, { a: 1, b: 2 }, { a: 2, b: 2 }])); + }); + + it("simple isec", () => { + const a = new EquivSet([{ id: "a", type: 1 }, { id: "b", type: 1 }, { id: "c", type: 2 }]); + const b = new EquivSet([{ type: 1, label: "foo" }, { type: 2, label: "bar" }, { type: 3, label: "baz" }]); + assert.deepEqual( + join(a, b), + new EquivSet([ + { id: "a", type: 1, label: "foo" }, + { id: "b", type: 1, label: "foo" }, + { id: "c", type: 2, label: "bar" } + ])); + }); + + it("joinWith", () => { + const a = new EquivSet([{ id: "a", type: 1 }, { id: "b", type: 1 }, { id: "c", type: 2 }]); + const b = new EquivSet([{ xyz: 1, label: "foo" }, { xyz: 2, label: "bar" }, { xyz: 3, label: "baz" }]); + assert.deepEqual( + joinWith(a, b, { type: "xyz" }), + new EquivSet([ + { id: "a", type: 1, xyz: 1, label: "foo" }, + { id: "b", type: 1, xyz: 1, label: "foo" }, + { id: "c", type: 2, xyz: 2, label: "bar" } + ])); + }); +}); diff --git a/packages/associative/test/tsconfig.json b/packages/associative/test/tsconfig.json new file mode 100644 index 0000000000..bcf29ace54 --- /dev/null +++ b/packages/associative/test/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "outDir": "../build" + }, + "include": [ + "./**/*.ts", + "../src/**/*.ts" + ] +} diff --git a/packages/associative/test/union.ts b/packages/associative/test/union.ts new file mode 100644 index 0000000000..b386216cbb --- /dev/null +++ b/packages/associative/test/union.ts @@ -0,0 +1,39 @@ +import * as assert from "assert"; + +import { EquivSet } from "../src/equiv-set"; +import { union } from "../src/union"; + +describe("union", () => { + + it("native (numbers)", () => { + const a = new Set([1, 2, 3, 4]); + const b = new Set([3, 4, 5, 6]); + assert.deepEqual(union(a, b), new Set([1, 2, 3, 4, 5, 6])); + }); + + it("equiv (numbers)", () => { + const a = new EquivSet([1, 2, 3, 4]); + const b = new EquivSet([3, 4, 5, 6]); + assert.deepEqual(union(a, b), new EquivSet([1, 2, 3, 4, 5, 6])); + }); + + it("native (obj)", () => { + const a = new Set([{ a: 1 }, { a: 2 }]); + const b = new Set([{ a: 2 }, { a: 3 }]); + const u = union(a, b); + assert.equal(u.size, 4); + assert.deepEqual(u, new Set([{ a: 1 }, { a: 2 }, { a: 2 }, { a: 3 }])); + assert.notStrictEqual(u, a); + assert.notStrictEqual(u, b); + }); + + it("equiv (obj)", () => { + const a = new EquivSet([{ a: 1 }, { a: 2 }]); + const b = new EquivSet([{ a: 2 }, { a: 3 }]); + const u = union(a, b); + assert.equal(u.size, 3); + assert.deepEqual(u, new EquivSet([{ a: 1 }, { a: 2 }, { a: 3 }])); + assert.notStrictEqual(u, a); + assert.notStrictEqual(u, b); + }); +}); diff --git a/packages/associative/tsconfig.json b/packages/associative/tsconfig.json new file mode 100644 index 0000000000..bd6481a5a6 --- /dev/null +++ b/packages/associative/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "." + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/packages/csp/CHANGELOG.md b/packages/csp/CHANGELOG.md index 356692bf8b..8b3dc42f89 100644 --- a/packages/csp/CHANGELOG.md +++ b/packages/csp/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.3.26](https://github.com/thi-ng/umbrella/compare/@thi.ng/csp@0.3.25...@thi.ng/csp@0.3.26) (2018-04-10) + + + + +**Note:** Version bump only for package @thi.ng/csp + ## [0.3.25](https://github.com/thi-ng/umbrella/compare/@thi.ng/csp@0.3.24...@thi.ng/csp@0.3.25) (2018-04-08) diff --git a/packages/csp/package.json b/packages/csp/package.json index b976fa447e..9d561db514 100644 --- a/packages/csp/package.json +++ b/packages/csp/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/csp", - "version": "0.3.25", + "version": "0.3.26", "description": "ES6 promise based CSP implementation", "main": "./index.js", "typings": "./index.d.ts", @@ -28,7 +28,7 @@ "typescript": "^2.8.1" }, "dependencies": { - "@thi.ng/dcons": "^0.1.19", + "@thi.ng/dcons": "^0.2.0", "@thi.ng/transducers": "^1.7.5" }, "keywords": [ diff --git a/packages/dcons/CHANGELOG.md b/packages/dcons/CHANGELOG.md index 28257e1e8d..b0e7dfa13a 100644 --- a/packages/dcons/CHANGELOG.md +++ b/packages/dcons/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.2.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/dcons@0.1.19...@thi.ng/dcons@0.2.0) (2018-04-10) + + +### Features + +* **dcons:** add IEmpty impl, minor refactoring ([10c089a](https://github.com/thi-ng/umbrella/commit/10c089a)) + + + + ## [0.1.19](https://github.com/thi-ng/umbrella/compare/@thi.ng/dcons@0.1.18...@thi.ng/dcons@0.1.19) (2018-04-08) diff --git a/packages/dcons/package.json b/packages/dcons/package.json index dbc6235c8a..7ef18cb484 100644 --- a/packages/dcons/package.json +++ b/packages/dcons/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/dcons", - "version": "0.1.19", + "version": "0.2.0", "description": "Comprehensive doubly linked list structure w/ iterator support", "main": "./index.js", "typings": "./index.d.ts", diff --git a/packages/dcons/src/index.ts b/packages/dcons/src/index.ts index cfb98485f7..24361be445 100644 --- a/packages/dcons/src/index.ts +++ b/packages/dcons/src/index.ts @@ -13,13 +13,14 @@ export interface ConsCell { export class DCons implements api.ICompare>, api.ICopy>, + api.IEmpty>, api.IEquiv, api.ILength, api.IRelease, api.IStack> { - public head: ConsCell; - public tail: ConsCell; + head: ConsCell; + tail: ConsCell; protected _length: number = 0; constructor(src?: Iterable) { @@ -28,15 +29,23 @@ export class DCons implements } } - public get length() { + get length() { return this._length; } - public copy() { - return new DCons(this); + copy() { + return new DCons(this); } - public release() { + empty() { + return new DCons(); + } + + clear() { + this.release(); + } + + release() { let cell = this.head, next; while (cell) { next = cell.next; @@ -51,7 +60,7 @@ export class DCons implements return true; } - public compare(o: DCons) { + compare(o: DCons) { if (this._length < o._length) { return -1; } else if (this._length > o._length) { @@ -69,7 +78,7 @@ export class DCons implements } } - public equiv(o: any) { + equiv(o: any) { if ((o instanceof DCons || isArrayLike(o)) && this._length === o.length) { let cell = this.head; for (let x of o) { @@ -83,7 +92,7 @@ export class DCons implements return false; } - public *[Symbol.iterator]() { + *[Symbol.iterator]() { let cell = this.head; while (cell) { yield cell.value; @@ -91,13 +100,13 @@ export class DCons implements } } - public *cycle() { + *cycle() { while (true) { yield* this; } } - public drop() { + drop() { const cell = this.head; if (cell) { this.head = cell.next; @@ -111,7 +120,7 @@ export class DCons implements } } - public cons(value: T): DCons { + cons(value: T): DCons { const cell = >{ value, next: this.head }; if (this.head) { this.head.prev = cell; @@ -123,7 +132,7 @@ export class DCons implements return this; } - public insertBefore(cell: ConsCell, value: T): DCons { + insertBefore(cell: ConsCell, value: T): DCons { if (!cell) { illegalArgs("cell is undefined"); } @@ -138,7 +147,7 @@ export class DCons implements return this; } - public insertAfter(cell: ConsCell, value: T): DCons { + insertAfter(cell: ConsCell, value: T): DCons { if (!cell) { illegalArgs("cell is undefined"); } @@ -153,7 +162,7 @@ export class DCons implements return this; } - public insertBeforeNth(n: number, x: T) { + insertBeforeNth(n: number, x: T) { if (n < 0) { n += this._length; } @@ -164,7 +173,7 @@ export class DCons implements } } - public insertAfterNth(n: number, x: T) { + insertAfterNth(n: number, x: T) { if (n < 0) { n += this._length; } @@ -175,7 +184,7 @@ export class DCons implements } } - public insertSorted(value: T, cmp?: api.Comparator) { + insertSorted(value: T, cmp?: api.Comparator) { cmp = cmp || compare; let cell = this.head; while (cell) { @@ -187,7 +196,7 @@ export class DCons implements return this.push(value); } - public find(value: T) { + find(value: T) { let cell = this.head; while (cell) { if (cell.value === value) { @@ -197,7 +206,7 @@ export class DCons implements } } - public findWith(fn: api.Predicate) { + findWith(fn: api.Predicate) { let cell = this.head; while (cell) { if (fn(cell.value)) { @@ -207,7 +216,7 @@ export class DCons implements } } - public concat(...slices: Iterable[]) { + concat(...slices: Iterable[]) { const res = this.copy(); for (let slice of slices) { res.into(slice); @@ -215,13 +224,13 @@ export class DCons implements return res; } - public into(src: Iterable) { + into(src: Iterable) { for (let x of src) { this.push(x); } } - public slice(from = 0, to = this.length) { + slice(from = 0, to = this.length) { let a = from < 0 ? from + this._length : from; let b = to < 0 ? to + this._length : to; if (a < 0 || b < 0) { @@ -236,7 +245,7 @@ export class DCons implements return res; } - public splice(at: ConsCell | number, del = 0, insert?: Iterable): DCons { + splice(at: ConsCell | number, del = 0, insert?: Iterable): DCons { let cell: ConsCell; if (typeof at === "number") { if (at < 0) { @@ -270,7 +279,7 @@ export class DCons implements return res; } - public remove(cell: ConsCell) { + remove(cell: ConsCell) { if (cell.prev) { cell.prev.next = cell.next; } else { @@ -285,7 +294,7 @@ export class DCons implements return this; } - public swap(a: ConsCell, b: ConsCell): DCons { + swap(a: ConsCell, b: ConsCell): DCons { if (a !== b) { const t = a.value; a.value = b.value; @@ -294,7 +303,7 @@ export class DCons implements return this; } - public push(value: T): DCons { + push(value: T): DCons { if (this.tail) { const cell = >{ value, prev: this.tail }; this.tail.next = cell; @@ -306,7 +315,7 @@ export class DCons implements } } - public pop(): DCons { + pop(): DCons { const cell = this.tail; if (cell) { this.tail = cell.prev; @@ -322,15 +331,15 @@ export class DCons implements return this; } - public first() { + first() { return this.head ? this.head.value : undefined; } - public peek() { + peek() { return this.tail ? this.tail.value : undefined; } - public setHead(v: T) { + setHead(v: T) { if (this.head) { this.head.value = v; return this; @@ -338,7 +347,7 @@ export class DCons implements return this.cons(v); } - public setTail(v: T) { + setTail(v: T) { if (this.tail) { this.tail.value = v; return this; @@ -346,7 +355,7 @@ export class DCons implements return this.push(v); } - public setNth(n: number, v: T) { + setNth(n: number, v: T) { const cell = this.nthCell(n); if (!cell) { illegalArgs(`index out of bounds: ${n}`); @@ -355,12 +364,12 @@ export class DCons implements return this; } - public nth(n: number, notFound?: T) { + nth(n: number, notFound?: T) { const cell = this.nthCell(n); return cell ? cell.value : notFound; } - public nthCell(n: number) { + nthCell(n: number) { if (n < 0) { n += this._length; } @@ -382,7 +391,7 @@ export class DCons implements return cell; } - public rotateLeft() { + rotateLeft() { switch (this._length) { case 0: case 1: @@ -394,7 +403,7 @@ export class DCons implements } } - public rotateRight() { + rotateRight() { switch (this._length) { case 0: case 1: @@ -407,7 +416,7 @@ export class DCons implements } } - public map(fn: (x: T) => R) { + map(fn: (x: T) => R) { const res = new DCons(); let cell = this.head; while (cell) { @@ -417,7 +426,7 @@ export class DCons implements return res; } - public filter(pred: api.Predicate) { + filter(pred: api.Predicate) { const res = new DCons(); let cell = this.head; while (cell) { @@ -427,7 +436,7 @@ export class DCons implements return res; } - public reduce(rfn: (acc: R, x: T) => R, initial: R) { + reduce(rfn: (acc: R, x: T) => R, initial: R) { let acc: R = initial; let cell = this.head; while (cell) { @@ -438,7 +447,7 @@ export class DCons implements return acc; } - public shuffle() { + shuffle() { let n = this._length; let cell = this.tail; while (n > 0) { @@ -450,7 +459,7 @@ export class DCons implements return this; } - public reverse() { + reverse() { let head = this.head; let tail = this.tail; let n = (this._length >>> 1) + (this._length & 1); @@ -465,7 +474,7 @@ export class DCons implements return this; } - public toString() { + toString() { let res: any = []; let cell = this.head; while (cell) { @@ -477,7 +486,7 @@ export class DCons implements return res.join(", "); } - public toJSON() { + toJSON() { return [...this]; } } diff --git a/packages/dgraph/CHANGELOG.md b/packages/dgraph/CHANGELOG.md index e472c3886b..263b0595f7 100644 --- a/packages/dgraph/CHANGELOG.md +++ b/packages/dgraph/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +# [0.1.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/dgraph@0.0.3...@thi.ng/dgraph@0.1.0) (2018-04-10) + + +### Features + +* **dgraph:** re-import DGraph impl & tests, update readme ([e086be6](https://github.com/thi-ng/umbrella/commit/e086be6)) + + + + ## [0.0.3](https://github.com/thi-ng/umbrella/compare/@thi.ng/dgraph@0.0.2...@thi.ng/dgraph@0.0.3) (2018-04-08) diff --git a/packages/dgraph/README.md b/packages/dgraph/README.md index a9c6693495..9cb4cda68c 100644 --- a/packages/dgraph/README.md +++ b/packages/dgraph/README.md @@ -4,9 +4,16 @@ ## About -Nothing to see here yet. Pls ignore for now. +Type-agnostic directed acyclic graph (DAG), using +[@thi.ng/associative](https://github.com/thi-ng/umbrella/tree/master/packages/associative) +maps & sets as backend. -TODO... +### Features + +- cycle detection +- accessors for direct & transitive dependencies / dependents +- topological sorting +- iterable ## Installation @@ -17,7 +24,15 @@ yarn add @thi.ng/dgraph ## Usage examples ```typescript -import * as dgraph from "@thi.ng/dgraph"; +import { DGraph } from "@thi.ng/dgraph"; + +g = new DGraph(); +g.addDependency([1, 2], [10, 20]); +g.addDependency([3, 4], [30, 40]); +g.addDependency([1, 2], [3, 4]); + +g.sort() +// [[30, 40], [3, 4], [10, 20], [1, 2]] ``` ## Authors diff --git a/packages/dgraph/package.json b/packages/dgraph/package.json index 0e0fb963f0..0a2f42ba33 100644 --- a/packages/dgraph/package.json +++ b/packages/dgraph/package.json @@ -1,7 +1,7 @@ { "name": "@thi.ng/dgraph", - "version": "0.0.3", - "description": "TODO", + "version": "0.1.0", + "description": "Type-agnostic directed acyclic graph (DAG) & graph operations", "main": "./index.js", "typings": "./index.d.ts", "repository": "https://github.com/thi-ng/umbrella", @@ -24,10 +24,16 @@ "typescript": "^2.8.1" }, "dependencies": { - "@thi.ng/api": "^2.2.0" + "@thi.ng/api": "^2.2.0", + "@thi.ng/associative": "^0.0.1", + "@thi.ng/iterators": "^4.1.5" }, "keywords": [ + "data structure", + "dependencies", + "DAG", "ES6", + "graph", "typescript" ], "publishConfig": { diff --git a/packages/dgraph/src/index.ts b/packages/dgraph/src/index.ts index 7101a741ec..59f86d921a 100644 --- a/packages/dgraph/src/index.ts +++ b/packages/dgraph/src/index.ts @@ -1,2 +1,140 @@ -// TODO stub only -export class DGraph { } +import { ICopy } from "@thi.ng/api/api"; +import { equiv } from "@thi.ng/api/equiv"; +import { illegalArgs } from "@thi.ng/api/error"; +import { EquivMap } from "@thi.ng/associative/equiv-map"; +import { EquivSet } from "@thi.ng/associative/equiv-set"; +import { union } from "@thi.ng/associative/union"; +import { filter } from "@thi.ng/iterators/filter"; +import { reduce } from "@thi.ng/iterators/reduce"; + +export class DGraph implements + Iterable, + ICopy> { + + dependencies: EquivMap>; + dependents: EquivMap>; + + constructor() { + this.dependencies = new EquivMap>(); + this.dependents = new EquivMap>(); + } + + *[Symbol.iterator]() { + yield* this.sort(); + } + + get [Symbol.species]() { + return DGraph; + } + + copy() { + const g = new DGraph(); + for (let e of this.dependencies) { + g.dependencies.set(e[0], e[1].copy()); + } + for (let e of this.dependents) { + g.dependents.set(e[0], e[1].copy()); + } + return g; + } + + addDependency(node: T, dep: T) { + if (equiv(node, dep) || this.depends(dep, node)) { + illegalArgs(`Circular dependency between: ${node} & ${dep}`); + } + let d: EquivSet = this.dependencies.get(node); + this.dependencies.set(node, d ? d.add(dep) : new EquivSet([dep])); + d = this.dependents.get(dep); + this.dependents.set(dep, d ? d.add(node) : new EquivSet([node])); + return this; + } + + removeEdge(node: T, dep: T) { + let d = this.dependencies.get(node); + if (d) { + d.delete(dep); + } + d = this.dependents.get(dep); + if (d) { + d.delete(node); + } + return this; + } + + removeNode(x: T) { + this.dependencies.delete(x); + return this; + } + + depends(x: T, y: T) { + return this.transitiveDependencies(x).has(y); + } + + dependent(x: T, y: T) { + return this.transitiveDependents(x).has(y); + } + + immediateDependencies(x: T): Set { + return this.dependencies.get(x) || new EquivSet(); + } + + immediateDependents(x: T): Set { + return this.dependents.get(x) || new EquivSet(); + } + + isLeaf(x: T) { + return this.immediateDependents(x).size === 0; + } + + isRoot(x: T) { + return this.immediateDependencies(x).size === 0; + } + + nodes(): Set { + return union( + new EquivSet(this.dependencies.keys()), + new EquivSet(this.dependents.keys()), + ); + } + + transitiveDependencies(x: T) { + return transitive(this.dependencies, x); + } + + transitiveDependents(x: T) { + return transitive(this.dependents, x); + } + + sort() { + const sorted: T[] = []; + const g = this.copy(); + let queue = new EquivSet(filter((node: T) => g.isLeaf(node), g.nodes())); + while (true) { + if (!queue.size) { + return sorted.reverse(); + } + const node = queue.first(); + queue.delete(node); + for (let d of (>g.immediateDependencies(node)).copy()) { + g.removeEdge(node, d); + if (g.isLeaf(d)) { + queue.add(d); + } + } + sorted.push(node); + g.removeNode(node); + } + } +} + +function transitive(nodes: EquivMap>, x: T): EquivSet { + const deps: EquivSet = nodes.get(x); + if (deps) { + return reduce( + (acc, k: T) => >union(acc, transitive(nodes, k)), + deps, + deps + ); + } + return new EquivSet(); +} diff --git a/packages/dgraph/test/index.ts b/packages/dgraph/test/index.ts index 5e8e91d02a..65ecff57d9 100644 --- a/packages/dgraph/test/index.ts +++ b/packages/dgraph/test/index.ts @@ -1,6 +1,53 @@ -// import * as assert from "assert"; -// import * as dgraph from "../src/index"; +import * as assert from "assert"; + +import { DGraph } from "../src/index"; describe("dgraph", () => { - it("tests pending"); + + let g: DGraph; + + beforeEach(() => { + g = new DGraph(); + g.addDependency([1, 2], [10, 20]); + g.addDependency([3, 4], [30, 40]); + g.addDependency([1, 2], [3, 4]); + }); + + it("depends", () => { + assert(g.depends([1, 2], [10, 20])); + assert(!g.depends([10, 20], [1, 2])); + }); + + it("dependent", () => { + assert(g.dependent([10, 20], [1, 2])); + assert(!g.dependent([1, 2], [10, 20])); + }); + + it("isLeaf", () => { + assert(g.isLeaf([1, 2])); + assert(!g.isLeaf([10, 20])); + assert(!g.isLeaf([3, 4])); + }); + + it("isRoot", () => { + assert(g.isRoot([10, 20])); + assert(g.isRoot([30, 40])); + assert(!g.isRoot([3, 4])); + }); + + it("cyclic", () => { + assert.throws(() => g.addDependency([10, 20], [1, 2])); + assert.throws(() => g.addDependency([1, 2], [1, 2])); + }); + + it("sort", () => { + assert.deepEqual(g.sort(), [[30, 40], [3, 4], [10, 20], [1, 2]]); + g.addDependency([30, 40], [50, 60]); + assert.deepEqual(g.sort(), [[50, 60], [30, 40], [3, 4], [10, 20], [1, 2]]); + }); + + it("iterator", () => { + assert.deepEqual([...g], [[30, 40], [3, 4], [10, 20], [1, 2]]); + assert.deepEqual([...g], [[30, 40], [3, 4], [10, 20], [1, 2]]); + }); }); diff --git a/packages/hdom/AUTHORS.md b/packages/hdom/AUTHORS.md new file mode 100644 index 0000000000..2740a05f86 --- /dev/null +++ b/packages/hdom/AUTHORS.md @@ -0,0 +1,7 @@ +## Maintainer + +- Karsten Schmidt (@postspectacular) + +## Contributors + +- Kevin Nolan (@allforabit) diff --git a/packages/hdom/CHANGELOG.md b/packages/hdom/CHANGELOG.md index d6ad58b532..8aee47bdf1 100644 --- a/packages/hdom/CHANGELOG.md +++ b/packages/hdom/CHANGELOG.md @@ -3,6 +3,25 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [3.0.2](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom@3.0.1...@thi.ng/hdom@3.0.2) (2018-04-10) + + + + +**Note:** Version bump only for package @thi.ng/hdom + + +## [3.0.1](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom@3.0.0...@thi.ng/hdom@3.0.1) (2018-04-09) + + +### Performance Improvements + +* **hdom:** intern imported checks, update normalizeTree(), add docs, fix tests ([2a91e30](https://github.com/thi-ng/umbrella/commit/2a91e30)) + + + + # [3.0.0](https://github.com/thi-ng/umbrella/compare/@thi.ng/hdom@2.3.3...@thi.ng/hdom@3.0.0) (2018-04-08) diff --git a/packages/hdom/package.json b/packages/hdom/package.json index 2d944479f1..9369efda8e 100644 --- a/packages/hdom/package.json +++ b/packages/hdom/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/hdom", - "version": "3.0.0", + "version": "3.0.2", "description": "Lightweight vanilla ES6 UI component & virtual DOM system", "main": "./index.js", "typings": "./index.d.ts", @@ -28,7 +28,7 @@ "@thi.ng/api": "^2.2.0", "@thi.ng/diff": "^1.0.6", "@thi.ng/hiccup": "^1.3.4", - "@thi.ng/iterators": "^4.1.4" + "@thi.ng/iterators": "^4.1.5" }, "keywords": [ "browser", diff --git a/packages/hdom/src/diff.ts b/packages/hdom/src/diff.ts index 1730892522..43aa4a3fe6 100644 --- a/packages/hdom/src/diff.ts +++ b/packages/hdom/src/diff.ts @@ -1,10 +1,12 @@ -import { isArray } from "@thi.ng/checks/is-array"; -import { isString } from "@thi.ng/checks/is-string"; +import * as isa from "@thi.ng/checks/is-array"; +import * as iss from "@thi.ng/checks/is-string"; import * as diff from "@thi.ng/diff"; // import { DEBUG } from "./api"; import { createDOM, removeAttribs, setAttrib, removeChild } from "./dom"; +const isArray = isa.isArray; +const isString = iss.isString; const diffArray = diff.diffArray; const diffObject = diff.diffObject; diff --git a/packages/hdom/src/dom.ts b/packages/hdom/src/dom.ts index 44fa892165..a9c197517a 100644 --- a/packages/hdom/src/dom.ts +++ b/packages/hdom/src/dom.ts @@ -1,15 +1,20 @@ -import { isArray } from "@thi.ng/checks/is-array"; -import { isFunction } from "@thi.ng/checks/is-function"; -import { isIterable } from "@thi.ng/checks/is-iterable"; -import { isString } from "@thi.ng/checks/is-string"; +import * as isa from "@thi.ng/checks/is-array"; +import * as isf from "@thi.ng/checks/is-function"; +import * as isi from "@thi.ng/checks/is-iterable"; +import * as iss from "@thi.ng/checks/is-string"; import { SVG_TAGS, SVG_NS } from "@thi.ng/hiccup/api"; import { css } from "@thi.ng/hiccup/css"; import { map } from "@thi.ng/iterators/map"; +const isArray = isa.isArray; +const isFunction = isf.isFunction; +const isIterable = isi.isIterable +const isString = iss.isString; + /** * Creates an actual DOM tree from given hiccup component and `parent` * element. Calls `init` with created element (user provided context and - * other args) for any components with `init` lifecycle method. Returns + * other args) for any components with `init` life cycle method. Returns * created root element(s) - usually only a single one, but can be an * array of elements, if the provided tree is an iterable. Creates DOM * text nodes for non-component values. Returns `parent` if tree is diff --git a/packages/hdom/src/normalize.ts b/packages/hdom/src/normalize.ts index 79e2988b69..f0892f6c37 100644 --- a/packages/hdom/src/normalize.ts +++ b/packages/hdom/src/normalize.ts @@ -1,12 +1,19 @@ import { illegalArgs } from "@thi.ng/api/error"; -import { isArray } from "@thi.ng/checks/is-array"; -import { implementsFunction } from "@thi.ng/checks/implements-function"; -import { isFunction } from "@thi.ng/checks/is-function"; -import { isIterable } from "@thi.ng/checks/is-iterable"; -import { isPlainObject } from "@thi.ng/checks/is-plain-object"; -import { isString } from "@thi.ng/checks/is-string"; +import * as isa from "@thi.ng/checks/is-array"; +import * as impf from "@thi.ng/checks/implements-function"; +import * as isf from "@thi.ng/checks/is-function"; +import * as isi from "@thi.ng/checks/is-iterable"; +import * as iso from "@thi.ng/checks/is-plain-object"; +import * as iss from "@thi.ng/checks/is-string"; import { TAG_REGEXP } from "@thi.ng/hiccup/api"; +const isArray = isa.isArray; +const isFunction = isf.isFunction; +const implementsFunction = impf.implementsFunction; +const isIterable = isi.isIterable +const isPlainObject = iso.isPlainObject; +const isString = iss.isString; + /** * Expands single hiccup element/component into its canonical form: * @@ -36,7 +43,7 @@ export function normalizeElement(spec: any[], keys: boolean) { if (!isString(tag) || !(match = TAG_REGEXP.exec(tag))) { illegalArgs(`${tag} is not a valid tag name`); } - // return orig if already normalized and satifies key requirement + // return orig if already normalized and satisfies key requirement if (tag === match[1] && hasAttribs && (!keys || spec[1].key)) { return spec; } @@ -86,6 +93,13 @@ const NO_SPANS = { * or will need to be replaced/removed. The `key` values are defined by * the `path` array arg. * + * In terms of life cycle methods: `render` should ALWAYS return an + * array or another function, else the component's `init` or `release` + * fns will NOT be able to be called. E.g. If the return value of + * `render` evaluates as a string or number, the return value should be + * wrapped as `["span", "foo"]`. If no `init` or `release` are used, + * this requirement is relaxed. + * * For normal usage only the first 2 args should be specified and the * rest kept at their defaults. * @@ -110,21 +124,17 @@ export function normalizeTree(tree: any, ctx?: any, path = [0], keys = true, spa // use result of function call // pass ctx as first arg and remaining array elements as rest args if (isFunction(tag)) { - return normalizeTree(tag.apply(null, [ctx, ...tree.slice(1)]), ctx, path.slice(), keys, span); + return normalizeTree(tag.apply(null, [ctx, ...tree.slice(1)]), ctx, path, keys, span); } // component object w/ life cycle methods // (render() is the only required hook) if (implementsFunction(tag, "render")) { const args = [ctx, ...tree.slice(1)]; - norm = normalizeTree(tag.render.apply(null, args), ctx, path.slice(), keys, span); - if (norm !== undefined) { - nattribs = norm[1]; - if (keys && nattribs.key === undefined) { - nattribs.key = path.join("-"); - } - norm.__init = tag.init; - norm.__release = tag.release; - norm.__args = args; + norm = normalizeTree(tag.render.apply(null, args), ctx, path, keys, span); + if (isArray(norm)) { + (norm).__init = tag.init; + (norm).__release = tag.release; + (norm).__args = args; } return norm; } @@ -166,7 +176,7 @@ export function normalizeTree(tree: any, ctx?: any, path = [0], keys = true, spa return normalizeTree(tree(ctx), ctx, path, keys, span); } if (implementsFunction(tree, "deref")) { - return normalizeTree(tree.deref(), ctx, path.slice(), keys, span); + return normalizeTree(tree.deref(), ctx, path, keys, span); } return span ? ["span", keys ? { key: path.join("-") } : {}, tree.toString()] : diff --git a/packages/hdom/test/index.ts b/packages/hdom/test/index.ts index 0b90a6481d..1a440c5d67 100644 --- a/packages/hdom/test/index.ts +++ b/packages/hdom/test/index.ts @@ -5,8 +5,8 @@ import { map } from "@thi.ng/iterators/map"; import { range } from "@thi.ng/iterators/range"; import { normalizeTree } from "../src/normalize"; -function _check(a, b) { - assert.deepEqual(normalizeTree(a, [], false, false), b); +function _check(a, b, ctx = null) { + assert.deepEqual(normalizeTree(a, ctx, [], false, false), b); } function check(id, a, b) { @@ -76,7 +76,7 @@ describe("hdom", () => { check( "tag fn w/ args", - [(id, body) => ["div#" + id, body], "foo", "bar"], + [(_, id, body) => ["div#" + id, body], "foo", "bar"], ["div", { id: "foo" }, "bar"] ); @@ -110,12 +110,19 @@ describe("hdom", () => { ["a", {}, ["b", {}]] ); - it("lifecycle", () => { - const res: any = ["div", {}]; + it("life cycle", () => { + let res: any = ["div", {}]; res.__init = res.__release = undefined; - res.__args = []; + res.__args = [null]; assert.deepEqual( - normalizeTree([{ render: () => ["div"] }], [], false, false), + normalizeTree([{ render: () => ["div"] }], null, [], false, false), + res + ); + res = ["div", { key: "0" }]; + res.__init = res.__release = undefined; + res.__args = [null]; + assert.deepEqual( + normalizeTree([{ render: () => ["div"] }], null, [0], true, false), res ); }); diff --git a/packages/hiccup-svg/CHANGELOG.md b/packages/hiccup-svg/CHANGELOG.md index f84df32714..580c9652d4 100644 --- a/packages/hiccup-svg/CHANGELOG.md +++ b/packages/hiccup-svg/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.2.1](https://github.com/thi-ng/umbrella/compare/@thi.ng/hiccup-svg@0.2.0...@thi.ng/hiccup-svg@0.2.1) (2018-04-09) + + +### Bug Fixes + +* **hiccup-svg:** path(), update add null check for points() ([b9d9a49](https://github.com/thi-ng/umbrella/commit/b9d9a49)) + + + + # 0.2.0 (2018-04-08) diff --git a/packages/hiccup-svg/package.json b/packages/hiccup-svg/package.json index c2a89e81d9..b231d87681 100644 --- a/packages/hiccup-svg/package.json +++ b/packages/hiccup-svg/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/hiccup-svg", - "version": "0.2.0", + "version": "0.2.1", "description": "SVG element functions for @thi.ng/hiccup & @thi.ng/hdom", "main": "./index.js", "typings": "./index.d.ts", diff --git a/packages/hiccup-svg/src/format.ts b/packages/hiccup-svg/src/format.ts index 2185d33d4e..4e03630714 100644 --- a/packages/hiccup-svg/src/format.ts +++ b/packages/hiccup-svg/src/format.ts @@ -6,7 +6,7 @@ const ff = (x: number) => x.toFixed(PRECISION); const point = (p: ArrayLike) => ff(p[0]) + "," + ff(p[1]); -const points = (pts: ArrayLike[], sep = " ") => pts.map(point).join(sep); +const points = (pts: ArrayLike[], sep = " ") => pts ? pts.map(point).join(sep) : ""; export { ff, diff --git a/packages/hiccup-svg/src/path.ts b/packages/hiccup-svg/src/path.ts index 341dc07fa3..1ebd9e6bc5 100644 --- a/packages/hiccup-svg/src/path.ts +++ b/packages/hiccup-svg/src/path.ts @@ -6,6 +6,6 @@ export const path = (segments: PathSegment[], attr?) => "path", { ...attr, - d: segments.map((seg) => seg[0] + points(seg[1], ",")), + d: segments.map((seg) => seg[0] + points(seg[1], ",")).join(""), } ]; diff --git a/packages/iterators/CHANGELOG.md b/packages/iterators/CHANGELOG.md index 80669ff10a..8eab01ccd8 100644 --- a/packages/iterators/CHANGELOG.md +++ b/packages/iterators/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [4.1.5](https://github.com/thi-ng/umbrella/compare/@thi.ng/iterators@4.1.4...@thi.ng/iterators@4.1.5) (2018-04-10) + + + + +**Note:** Version bump only for package @thi.ng/iterators + ## [4.1.4](https://github.com/thi-ng/umbrella/compare/@thi.ng/iterators@4.1.3...@thi.ng/iterators@4.1.4) (2018-04-08) diff --git a/packages/iterators/package.json b/packages/iterators/package.json index 7b4208284b..b0ab86e365 100644 --- a/packages/iterators/package.json +++ b/packages/iterators/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/iterators", - "version": "4.1.4", + "version": "4.1.5", "description": "clojure.core inspired, composable ES6 iterators & generators", "main": "./index.js", "typings": "./index.d.ts", @@ -25,7 +25,7 @@ }, "dependencies": { "@thi.ng/api": "^2.2.0", - "@thi.ng/dcons": "^0.1.19" + "@thi.ng/dcons": "^0.2.0" }, "keywords": [ "clojure", diff --git a/packages/rstream-csp/CHANGELOG.md b/packages/rstream-csp/CHANGELOG.md index 57128b4ab0..22c58e12c7 100644 --- a/packages/rstream-csp/CHANGELOG.md +++ b/packages/rstream-csp/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. + +## [0.1.46](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream-csp@0.1.45...@thi.ng/rstream-csp@0.1.46) (2018-04-10) + + + + +**Note:** Version bump only for package @thi.ng/rstream-csp + ## [0.1.45](https://github.com/thi-ng/umbrella/compare/@thi.ng/rstream-csp@0.1.44...@thi.ng/rstream-csp@0.1.45) (2018-04-08) diff --git a/packages/rstream-csp/package.json b/packages/rstream-csp/package.json index ba0c1493d9..0808a9fd13 100644 --- a/packages/rstream-csp/package.json +++ b/packages/rstream-csp/package.json @@ -1,6 +1,6 @@ { "name": "@thi.ng/rstream-csp", - "version": "0.1.45", + "version": "0.1.46", "description": "@thi.ng/csp bridge module for @thi.ng/rstream", "main": "./index.js", "typings": "./index.d.ts", @@ -24,7 +24,7 @@ "typescript": "^2.8.1" }, "dependencies": { - "@thi.ng/csp": "^0.3.25", + "@thi.ng/csp": "^0.3.26", "@thi.ng/rstream": "^1.2.5" }, "keywords": [ diff --git a/yarn.lock b/yarn.lock index 79e97bb5f6..bf9a2e472a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3727,9 +3727,9 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" -lerna@^2.9.1: - version "2.9.1" - resolved "https://registry.yarnpkg.com/lerna/-/lerna-2.9.1.tgz#d7d21793ad35ae7733733ced34ce30f5b3bb1abe" +lerna@^2.10.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/lerna/-/lerna-2.10.1.tgz#3b0ffe6b80d3312e8efdc7003a50d47d90ebdf03" dependencies: async "^1.5.0" chalk "^2.1.0"