Skip to content

Commit

Permalink
feat(atom): add Cursor, update interfaces, types, readme
Browse files Browse the repository at this point in the history
  • Loading branch information
postspectacular committed Jan 29, 2018
1 parent 52c25a8 commit 04c3d59
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 58 deletions.
35 changes: 32 additions & 3 deletions packages/atom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

## About

Clojure inspired mutable wrapper for (usually) immutable values, with support for watches.
Clojure inspired mutable wrappers for (usually) immutable values, with support for watches.

TODO

## Installation

Expand All @@ -14,10 +16,12 @@ yarn add @thi.ng/atom

## Usage examples

### Atom

```typescript
import { Atom } from "@thi.ng/atom";
import * as atom from "@thi.ng/atom";

const a = new Atom(23);
const a = new atom.Atom(23);

// obtain value via deref()
a.deref();
Expand All @@ -34,6 +38,31 @@ a.reset(42);
// foo: 24 -> 42
```

### Cursor

```typescript
// main state
main = new atom.Atom({a: 23, b: 42});

// cursor to `a` value
// requires both a lookup & update function to given value
// both fns will be called with cursor's parent state
// the updater MUST NOT mutate in place
cursor = new atom.Cursor(main, (state) => state.a, (state, x) => ({...state, a: x}));

// add watch just as with Atom
cursor.addWatch("foo", console.log);

cursor.deref()
// 23

cursor.swap(x => x + 1);
// foo 23 24

main.deref()
// { a: 24, b: 42 }
```

## Authors

- Karsten Schmidt
Expand Down
13 changes: 13 additions & 0 deletions packages/atom/src/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as api from "@thi.ng/api/api";

export type SwapFn<T> = (curr: T, ...args: any[]) => T;

export interface ReadonlyAtom<T> extends
api.IDeref<T>,
api.IWatch<T> {
}

export interface IAtom<T> extends ReadonlyAtom<T> {
reset(val: T): T;
swap(fn: SwapFn<T>, ...args: any[]): T;
}
56 changes: 56 additions & 0 deletions packages/atom/src/atom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { IEquiv, Watch } from "@thi.ng/api/api";
import { IWatch } from "@thi.ng/api/mixins/iwatch";

import { IAtom, SwapFn } from "./api";

/**
* Mutable wrapper for an (usually) immutable value.
* Support for watches.
*/
@IWatch
export class Atom<T> implements
IAtom<T>,
IEquiv {

protected value: T;

constructor(val?: T) {
this.value = val;
}

deref() {
return this.value;
}

equiv(o: any) {
return this === o;
}

reset(val: T) {
const old = this.value;
this.value = val;
this.notifyWatches(old, val);
return this.value;
}

swap(fn: SwapFn<T>, ...args: any[]) {
const old = this.value;
args.unshift(old);
this.value = fn.apply(null, args);
this.notifyWatches(old, this.value);
return this.value;
}

// mixin stub
addWatch(id: string, fn: Watch<T>) {
return false;
}

// mixin stub
removeWatch(id: string) {
return false;
}

// mixin stub
notifyWatches(oldState: T, newState: T) { }
}
71 changes: 71 additions & 0 deletions packages/atom/src/cursor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { IID, IRelease, Watch } from "@thi.ng/api/api";
import { IAtom, SwapFn } from "./api";
import { Atom } from "./atom";

export class Cursor<T> implements
IAtom<T>,
IID<string>,
IRelease {

static NEXT_ID = 0;

readonly id: string;
parent: IAtom<any>;

protected local: Atom<T>;
protected lookup: (s: any) => T;
protected selfUpdate: boolean;

constructor(parent: IAtom<any>, lookup: (s: any) => T, update: (s: any, v: T) => any) {
this.parent = parent;
this.id = `cursor-${Cursor.NEXT_ID++}`;
this.selfUpdate = false;
this.local = new Atom<T>(lookup(parent.deref()));
this.local.addWatch(this.id, (_, prev, curr) => {
if (prev !== curr) {
this.selfUpdate = true;
parent.swap((state) => update(state, curr));
this.selfUpdate = false;
}
});
parent.addWatch(this.id, (_, prev, curr) => {
if (!this.selfUpdate) {
const cval = lookup(curr);
if (cval !== lookup(prev)) {
this.local.reset(cval);
}
}
});
}

deref() {
return this.local.deref();
}

release() {
this.local.removeWatch(this.id);
this.parent.removeWatch(this.id);
delete this.local;
return true;
}

reset(val: T) {
return this.local.reset(val);
}

swap(fn: SwapFn<T>, ...args: any[]) {
return this.local.swap.apply(this.local, [fn, ...args]);
}

addWatch(id: string, fn: Watch<T>) {
return this.local.addWatch(id, fn);
}

removeWatch(id: string): boolean {
throw new Error("Method not implemented.");
}

notifyWatches(oldState: T, newState: T) {
throw new Error("Method not implemented.");
}
}
57 changes: 2 additions & 55 deletions packages/atom/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,2 @@
import * as api from "@thi.ng/api/api";
import { IWatch } from "@thi.ng/api/mixins/iwatch";

/**
* Mutable wrapper for an (usually) immutable value.
* Support for watches.
*/
@IWatch
export class Atom<T> implements
api.IDeref<T>,
api.IEquiv,
api.IWatch<T> {

protected value: T;

constructor(val?: T) {
this.value = val;
}

public deref() {
return this.value;
}

public equiv(o: any) {
return this === o;
}

public reset(val: T) {
const old = this.value;
this.value = val;
this.notifyWatches(old, val);
return this.value;
}

public swap(fn: (curr: T, ...args: any[]) => T, ...args: any[]) {
const old = this.value;
args.unshift(old);
this.value = fn.apply(null, args);
this.notifyWatches(old, this.value);
return this.value;
}

// mixin stub
public addWatch(id: string, fn: (id: string, oldState: T, newState: T) => void) {
return false;
}

// mixin stub
public removeWatch(id: string) {
return false;
}

// mixin stub
public notifyWatches(oldState: T, newState: T) { }
}
export * from "./atom";
export * from "./cursor";

0 comments on commit 04c3d59

Please sign in to comment.