Skip to content

Commit

Permalink
Merge pull request #12 from salsify/symbol-keys
Browse files Browse the repository at this point in the history
Support symbols or strings as milestone keys
  • Loading branch information
dfreeman authored Sep 10, 2018
2 parents 62201db + 9c26a77 commit 671da8c
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 224 deletions.
60 changes: 37 additions & 23 deletions addon/-private/milestone-coordinator.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
import { assert } from '@ember/debug';
import { CancelableDeferred } from 'ember-milestones';
import { CancelableDeferred, MilestoneKey } from 'ember-milestones';
import { MilestoneCoordinator as IMilestoneCoordinator } from 'ember-milestones';
import { defer } from './defer';
import MilestoneHandle from './milestone-handle';
import MilestoneTarget from './milestone-target';

/** @hide */
export const ACTIVE_COORDINATORS: { [key: string]: MilestoneCoordinator } = Object.create(null);
export const ACTIVE_COORDINATORS: {
// TypeScript doesn't allow symbols as an index type, which makes a number of things
// in this module painful :( https://github.com/Microsoft/TypeScript/issues/1863
[key: string]: MilestoneCoordinator;
} = Object.create(null);

/** @hide */
export default class MilestoneCoordinator implements IMilestoneCoordinator {
public static forMilestone(name: string): MilestoneCoordinator | undefined {
return ACTIVE_COORDINATORS[name];
public static forMilestone(name: MilestoneKey): MilestoneCoordinator | undefined {
return ACTIVE_COORDINATORS[name as any];
}

public static deactivateAll(): void {
let keys = Object.keys(ACTIVE_COORDINATORS);
for (; keys.length; keys = Object.keys(ACTIVE_COORDINATORS)) {
ACTIVE_COORDINATORS[keys[0]].deactivateAll();
for (let key of allKeys(ACTIVE_COORDINATORS)) {
let coordinator = this.forMilestone(key);
if (coordinator) {
coordinator.deactivateAll();
}
}
}

public names: string[];
public names: MilestoneKey[];

private _pendingActions: { [key: string]: { action: () => any, deferred: CancelableDeferred } };
private _nextTarget: MilestoneTarget | null;
private _pausedMilestone: MilestoneHandle | null;

constructor(names: string[]) {
constructor(names: MilestoneKey[]) {
names.forEach((name) => {
assert(`Milestone '${name}' is already active.`, !ACTIVE_COORDINATORS[name]);
ACTIVE_COORDINATORS[name] = this;
assert(`Milestone '${name.toString()}' is already active.`, !MilestoneCoordinator.forMilestone(name));
ACTIVE_COORDINATORS[name as any] = this;
});

this.names = names;
Expand All @@ -39,16 +45,16 @@ export default class MilestoneCoordinator implements IMilestoneCoordinator {
this._pausedMilestone = null;
}

public advanceTo(name: string): MilestoneTarget {
assert(`Milestone '${name}' is not active.`, this.names.indexOf(name) !== -1);
public advanceTo(name: MilestoneKey): MilestoneTarget {
assert(`Milestone '${name.toString()}' is not active.`, this.names.indexOf(name) !== -1);
let target = new MilestoneTarget(name);

this._nextTarget = target;
this._continueAll({ except: name });

let pending = this._pendingActions[name];
let pending = this._pendingActions[name as any];
if (pending) {
delete this._pendingActions[name];
delete this._pendingActions[name as any];
this._targetReached(target, pending.deferred, pending.action);
}

Expand All @@ -59,14 +65,14 @@ export default class MilestoneCoordinator implements IMilestoneCoordinator {
this._continueAll();

this.names.forEach((name) => {
delete ACTIVE_COORDINATORS[name];
delete ACTIVE_COORDINATORS[name as any];
});

this.names = [];
}

// Called from milestone()
public _milestoneReached<T extends PromiseLike<any>>(name: string, action: () => T): T {
public _milestoneReached<T extends PromiseLike<any>>(name: MilestoneKey, action: () => T): T {
let target = this._nextTarget;

// If we're already targeting another milestone, just pass through
Expand All @@ -78,8 +84,8 @@ export default class MilestoneCoordinator implements IMilestoneCoordinator {
if (target && target.name === name) {
this._targetReached(target, deferred, action);
} else {
assert(`Milestone '${name}' is already pending.`, !this._pendingActions[name]);
this._pendingActions[name] = { deferred, action };
assert(`Milestone '${name.toString()}' is already pending.`, !this._pendingActions[name as any]);
this._pendingActions[name as any] = { deferred, action };
}

// Playing fast and loose with our casting here under the assumption that
Expand All @@ -101,18 +107,26 @@ export default class MilestoneCoordinator implements IMilestoneCoordinator {
target._resolve(this._pausedMilestone);
}

private _continueAll({ except }: { except?: string } = {}) {
private _continueAll({ except }: { except?: MilestoneKey } = {}) {
let paused = this._pausedMilestone;
if (paused && paused.name !== except) {
paused.continue();
}

Object.keys(this._pendingActions).forEach((key) => {
allKeys(this._pendingActions).forEach((key) => {
if (key === except) { return; }

let { deferred, action } = this._pendingActions[key];
let { deferred, action } = this._pendingActions[key as any];
deferred.resolve(action());
delete this._pendingActions[key];
delete this._pendingActions[key as any];
});
}
}

function allKeys(object: any) {
let keys: MilestoneKey[] = Object.getOwnPropertyNames(object);
if (typeof Object.getOwnPropertySymbols === 'function') {
keys = keys.concat(Object.getOwnPropertySymbols(object));
}
return keys;
}
8 changes: 6 additions & 2 deletions addon/-private/milestone-handle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import MilestoneCoordinator from 'ember-milestones/-private/milestone-coordinato
import {
CancelableDeferred,
MilestoneHandle as IMilestoneHandle,
MilestoneKey,
ResolutionOptions,
} from 'ember-milestones';

Expand All @@ -17,7 +18,7 @@ export default class MilestoneHandle implements IMilestoneHandle {
private resolution: Resolution | null = null;

constructor(
public name: string,
public name: MilestoneKey,
private _coordinator: MilestoneCoordinator,
private _action: () => any,
private _deferred: CancelableDeferred,
Expand Down Expand Up @@ -49,7 +50,10 @@ export default class MilestoneHandle implements IMilestoneHandle {
}

private _complete(resolution: Resolution, options: ResolutionOptions = {}, finalizer: () => void): Promise<void> {
assert(`Multiple resolutions for milestone ${this.name}`, !this.resolution || this.resolution === resolution);
assert(
`Multiple resolutions for milestone '${this.name.toString()}'`,
!this.resolution || this.resolution === resolution,
);

if (!this.resolution) {
this.resolution = resolution;
Expand Down
3 changes: 2 additions & 1 deletion addon/-private/milestone-target.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logger from 'debug';
import {
MilestoneHandle,
MilestoneKey,
MilestoneTarget as IMilestoneTarget,
ResolutionOptions,
} from 'ember-milestones';
Expand All @@ -13,7 +14,7 @@ export default class MilestoneTarget implements IMilestoneTarget {
private _coordinatorDeferred: RSVP.Deferred<MilestoneHandle> = defer();

constructor(
public name: string,
public name: MilestoneKey,
) {}

public then<TResult1 = MilestoneHandle, TResult2 = never>(
Expand Down
21 changes: 13 additions & 8 deletions addon/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const debugInactive = logger('ember-milestones:inactive');
* Inactive milestones will pass through to their given callbacks as though the
* milestone wrapper weren't present at all...
*/
export function activateMilestones(milestones: string[]): MilestoneCoordinator {
export function activateMilestones(milestones: MilestoneKey[]): MilestoneCoordinator {
return new CoordinatorImpl(milestones);
}

Expand All @@ -38,10 +38,10 @@ export function deactivateAllMilestones(): void {
*
* await advanceTo('my-component#poller').andContinue();
*/
export function advanceTo(name: string): MilestoneTarget {
export function advanceTo(name: MilestoneKey): MilestoneTarget {
let coordinator = CoordinatorImpl.forMilestone(name);
if (!coordinator) {
throw new Error(`Milestone ${name} isn't currently active.`);
throw new Error(`Milestone ${name.toString()} isn't currently active.`);
} else {
return coordinator.advanceTo(name);
}
Expand All @@ -55,9 +55,9 @@ export function advanceTo(name: string): MilestoneTarget {
* When not activated, code wrapped in a milestone is immediately invoked as though
* the wrapper weren't there at all.
*/
export function milestone<T extends PromiseLike<any>>(name: string, callback: () => T): T;
export function milestone(name: string): PromiseLike<void>;
export function milestone(name: string, callback?: () => any): PromiseLike<any> {
export function milestone<T extends PromiseLike<any>>(name: MilestoneKey, callback: () => T): T;
export function milestone(name: MilestoneKey): PromiseLike<void>;
export function milestone(name: MilestoneKey, callback?: () => any): PromiseLike<any> {
let coordinator = CoordinatorImpl.forMilestone(name);
let action = callback || (() => Promise.resolve());
if (coordinator) {
Expand Down Expand Up @@ -87,7 +87,7 @@ export function milestone(name: string, callback?: () => any): PromiseLike<any>
* In most cases, using the importable `advanceTo` should mean you won't need to
* use the `as` parameter.
*/
export function setupMilestones(hooks: TestHooks, names: string[], options: MilestoneTestOptions = {}) {
export function setupMilestones(hooks: TestHooks, names: MilestoneKey[], options: MilestoneTestOptions = {}) {
let milestones: MilestoneCoordinator;

hooks.beforeEach(function(this: any) {
Expand All @@ -103,6 +103,11 @@ export function setupMilestones(hooks: TestHooks, names: string[], options: Mile
});
}

/**
* A valid key for a milestone, either a string or a symbol.
*/
export type MilestoneKey = string | symbol;

/**
* A `MilestoneCoordinator` is the result of an `activateMilestones` call,
* which provides the ability to interact with the milestones you've activated
Expand All @@ -117,7 +122,7 @@ export interface MilestoneCoordinator {
* Advance until the given milestone is reached, continuing past any others
* that are active for this coordinator in the meantime.
*/
advanceTo(name: string): MilestoneTarget;
advanceTo(name: MilestoneKey): MilestoneTarget;

/**
* Deactivate all milestones associated with this coordinator.
Expand Down
Loading

0 comments on commit 671da8c

Please sign in to comment.