Skip to content

Commit

Permalink
feat(isolate): allow a scopes-per-channel object as second arg
Browse files Browse the repository at this point in the history
isolate(component, scopes) now allows scopes to be an object as a map
from key to scope, allowing giving a different scope for each isolation
channel. For instance, you can give the scope 'foo' to the DOM
source/sink and the scope 'bar' to the HTTP source/sink. This
scopes-per-channel also accepts the wildcard scope under the key '*',
which will work as the default isolation scope for channels not
specified in the scopes-per-channel object. If the wildcard is not
present, an arbitrary scope string will be generated.

ISSUES CLOSED: 526
  • Loading branch information
staltz committed Mar 8, 2017
1 parent b50a773 commit e35b731
Show file tree
Hide file tree
Showing 2 changed files with 274 additions and 31 deletions.
105 changes: 74 additions & 31 deletions isolate/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
let counter = 0;
export type Component<So, Si> = (sources: So, ...rest: Array<any>) => Si;

function newScope(): string {
return `cycle${++counter}`;
export interface IsolateableSource {

This comment has been minimized.

Copy link
@TylorS

TylorS Mar 10, 2017

Member

"Isolateable" is not a word 😄
http://www.dictionary.com/browse/isolatable

This comment has been minimized.

Copy link
@staltz

staltz Mar 11, 2017

Author Member

Here comes ... a breaking change!

isolateSource(source: Partial<IsolateableSource>, scope: any): Partial<IsolateableSource>;
isolateSink<T>(sink: T, scope: any): T;
}

export interface Sources {
[name: string]: Partial<IsolateableSource>;
}

export type WildcardScope = {
['*']?: string;
};

export type ScopesPerChannel<So> = {
[K in keyof So]: any;
};

export type Scopes<So> = (Partial<ScopesPerChannel<So>> & WildcardScope) | string;

function checkIsolateArgs<So, Si>(dataflowComponent: Component<So, Si>, scope: any) {
if (typeof dataflowComponent !== `function`) {
throw new Error(`First argument given to isolate() must be a ` +
Expand All @@ -14,47 +29,66 @@ function checkIsolateArgs<So, Si>(dataflowComponent: Component<So, Si>, scope: a
}
}

export interface IsolateableSource {
isolateSource(source: Partial<IsolateableSource>, scope: string): Partial<IsolateableSource>;
isolateSink<T>(sink: T, scope: string): T;
}

export interface Sources {
[name: string]: Partial<IsolateableSource>;
function normalizeScopes<So>(sources: So,
scopes: Scopes<So>,
randomScope: string): ScopesPerChannel<So> {
const perChannel = {} as ScopesPerChannel<So>;
Object.keys(sources).forEach((channel: keyof So) => {
if (typeof scopes === 'string') {
perChannel[channel] = scopes;
return;
}
const candidate = (scopes as ScopesPerChannel<So>)[channel];
if (typeof candidate !== 'undefined') {
perChannel[channel] = candidate;
return;
}
const wildcard = (scopes as WildcardScope)['*'];
if (typeof wildcard !== 'undefined') {
perChannel[channel] = wildcard;
return;
}
perChannel[channel] = randomScope;
});
return perChannel;
}

function isolateAllSources<So extends Sources>(outerSources: So, scope: string): So {
function isolateAllSources<So extends Sources>(
outerSources: So,
scopes: ScopesPerChannel<So>): So {
const innerSources = {} as So;
for (const key in outerSources) {
const source = outerSources[key] as Partial<IsolateableSource>;
if (outerSources.hasOwnProperty(key)
&& source
&& typeof source.isolateSource === 'function') {
innerSources[key] = source.isolateSource(source, scope);
} else if (outerSources.hasOwnProperty(key)) {
innerSources[key] = outerSources[key];
for (const channel in outerSources) {
const outerSource = outerSources[channel] as Partial<IsolateableSource>;
if (outerSources.hasOwnProperty(channel)
&& outerSource
&& typeof outerSource.isolateSource === 'function') {
innerSources[channel] = outerSource.isolateSource(outerSource, scopes[channel]);
} else if (outerSources.hasOwnProperty(channel)) {
innerSources[channel] = outerSources[channel];
}
}
return innerSources;
}

function isolateAllSinks<So extends Sources, Si>(sources: So, innerSinks: Si, scope: string): Si {
function isolateAllSinks<So extends Sources, Si>(
sources: So,
innerSinks: Si,
scopes: ScopesPerChannel<So>): Si {
const outerSinks = {} as Si;
for (const key in innerSinks) {
const source = sources[key] as Partial<IsolateableSource>;
if (innerSinks.hasOwnProperty(key)
for (const channel in innerSinks) {
const source = sources[channel] as Partial<IsolateableSource>;
const innerSink = innerSinks[channel];
if (innerSinks.hasOwnProperty(channel)
&& source
&& typeof source.isolateSink === 'function') {
outerSinks[key] = source.isolateSink(innerSinks[key], scope);
} else if (innerSinks.hasOwnProperty(key)) {
outerSinks[key] = innerSinks[key];
outerSinks[channel] = source.isolateSink(innerSink, scopes[channel]);
} else if (innerSinks.hasOwnProperty(channel)) {
outerSinks[channel] = innerSinks[channel];
}
}
return outerSinks;
}

export type Component<So, Si> = (sources: So, ...rest: Array<any>) => Si;

/**
* `isolate` takes a small component as input, and returns a big component.
* A "small" component is a component that operates in a deeper scope.
Expand All @@ -72,6 +106,11 @@ export type Component<So, Si> = (sources: So, ...rest: Array<any>) => Si;
export type OuterSo = any;
export type OuterSi = any;

let counter = 0;
function newScope(): string {
return `cycle${++counter}`;
}

/**
* Takes a `component` function and an optional `scope` string, and returns a
* scoped version of the `component` function.
Expand Down Expand Up @@ -105,11 +144,15 @@ export type OuterSi = any;
function isolate<InnerSo, InnerSi>(component: Component<InnerSo, InnerSi>,
scope: any = newScope()): Component<OuterSo, OuterSi> {
checkIsolateArgs(component, scope);
const convertedScope: string = typeof scope === 'string' ? scope : scope.toString();
const randomScope = typeof scope === 'object' ? newScope() : '';
const scopes: any = typeof scope === 'string' || typeof scope === 'object' ?
scope :
scope.toString();
return function wrappedComponent(outerSources: OuterSo, ...rest: Array<any>): OuterSi {
const innerSources = isolateAllSources(outerSources, convertedScope);
const scopesPerChannel = normalizeScopes(outerSources, scopes, randomScope);
const innerSources = isolateAllSources(outerSources, scopesPerChannel);
const innerSinks = component(innerSources, ...rest);
const outerSinks = isolateAllSinks(outerSources, innerSinks, convertedScope);
const outerSinks = isolateAllSinks(outerSources, innerSinks, scopesPerChannel);
return outerSinks;
};
}
Expand Down
200 changes: 200 additions & 0 deletions isolate/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import isolate from '../lib/index';
import * as sinon from 'sinon';

describe('isolate', function () {
beforeEach(function () {
(isolate as any).reset();
});

it('should be a function', function () {
assert.strictEqual(typeof isolate, 'function');
});
Expand Down Expand Up @@ -41,6 +45,202 @@ describe('isolate', function () {
assert.strictEqual(typeof scopedMyDataflowComponent, `function`);
});

it('should accept a scopes-per-channel object as the second argument', function () {
function Component(sources: any) {
return {
first: sources.first.getSink(),
second: sources.second.getSink(),
};
}

const scopedComponent = isolate(Component, {first: 'scope1', second: 'scope2'});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';

const sources = {
first: {
getSink() { return 10; },
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},

second: {
getSink() { return 20; },
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);

assert.strictEqual(actual1, 'scope1');
assert.strictEqual(actual2, 'scope1');
assert.strictEqual(actual3, 'scope2');
assert.strictEqual(actual4, 'scope2');
assert.strictEqual(sinks.first, 10);
assert.strictEqual(sinks.second, 20);
});

it('should generate a scope if a channel is undefined in scopes-per-channel', function () {
function Component(sources: any) {
return {
first: sources.first.getSink(),
second: sources.second.getSink(),
};
}

const scopedComponent = isolate(Component, {first: 'scope1'});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';

const sources = {
first: {
getSink() { return 10; },
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},

second: {
getSink() { return 20; },
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);

assert.strictEqual(actual1, 'scope1');
assert.strictEqual(actual2, 'scope1');
assert.strictEqual(actual3, 'cycle1');
assert.strictEqual(actual4, 'cycle1');
assert.strictEqual(sinks.first, 10);
assert.strictEqual(sinks.second, 20);
});

it('should accept a wildcard * in the scopes-per-channel object', function () {
function Component(sources: any) {
return {
first: sources.first.getSink(),
second: sources.second.getSink(),
};
}

const scopedComponent = isolate(Component, {first: 'scope1', '*': 'default'});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';

const sources = {
first: {
getSink() { return 10; },
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},

second: {
getSink() { return 20; },
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);

assert.strictEqual(actual1, 'scope1');
assert.strictEqual(actual2, 'scope1');
assert.strictEqual(actual3, 'default');
assert.strictEqual(actual4, 'default');
assert.strictEqual(sinks.first, 10);
assert.strictEqual(sinks.second, 20);
});

it('should not convert to string values in scopes-per-channel object', function () {
function Component(sources: any) {
return {
first: sources.first.getSink(),
second: sources.second.getSink(),
};
}

const scopedComponent = isolate(Component, {first: 123, second: 456});
let actual1 = '';
let actual2 = '';
let actual3 = '';
let actual4 = '';

const sources = {
first: {
getSink() { return 10; },
isolateSource(source: any, scope: string) {
actual1 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual2 = scope;
return sink;
},
},

second: {
getSink() { return 20; },
isolateSource(source: any, scope: string) {
actual3 = scope;
return source;
},
isolateSink(sink: any, scope: string) {
actual4 = scope;
return sink;
},
},
};
const sinks = scopedComponent(sources);

assert.strictEqual(actual1, 123);
assert.strictEqual(actual2, 123);
assert.strictEqual(actual3, 456);
assert.strictEqual(actual4, 456);
assert.strictEqual(sinks.first, 10);
assert.strictEqual(sinks.second, 20);
});

describe('scopedDataflowComponent', function () {
it('should return a valid dataflow component', function () {
function driver() {
Expand Down

0 comments on commit e35b731

Please sign in to comment.