Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support symbols or strings as milestone keys #12

Merged
merged 1 commit into from
Sep 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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