forked from GoogleChromeLabs/ProjectVisBug
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
State machine for extension lifecycle
- Loading branch information
1 parent
5fddfda
commit 3bb5a11
Showing
3 changed files
with
277 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,69 @@ | ||
const state = { | ||
loaded: {}, | ||
injected: {}, | ||
} | ||
import { createTransition, createStateMachine } from "../lib/state-machine" | ||
|
||
const toggleIn = ({id:tab_id}) => { | ||
// toggle out: it's currently loaded and injected | ||
if (state.loaded[tab_id] && state.injected[tab_id]) { | ||
chrome.tabs.executeScript(tab_id, { file: 'toolbar/eject.js' }) | ||
state.injected[tab_id] = false | ||
// chrome.storage.sync.remove([storagekey]) | ||
} | ||
// extension states (per tab) | ||
// 1. unloaded | ||
// 2. loaded | ||
// 3. injected | ||
// 4. ejected | ||
|
||
// toggle in: it's loaded and needs injected | ||
else if (state.loaded[tab_id] && !state.injected[tab_id]) { | ||
chrome.tabs.executeScript(tab_id, { file: 'toolbar/inject.js' }) | ||
state.injected[tab_id] = true | ||
getColorMode() | ||
} | ||
// extension state actions (per tab) | ||
// 1. load : unloaded -> loaded | ||
// 2. inject: loaded|ejected -> injected | ||
// 3. eject : injected -> ready | ||
|
||
// fresh start in tab | ||
else { | ||
chrome.tabs.insertCSS(tab_id, { file: 'toolbar/bundle.css' }) | ||
chrome.tabs.executeScript(tab_id, { file: 'web-components.polyfill.js' }) | ||
chrome.tabs.executeScript(tab_id, { file: 'toolbar/bundle.js' }) | ||
chrome.tabs.executeScript(tab_id, { file: 'toolbar/inject.js' }) | ||
const extensionStateMachineConfig = { | ||
initial: 'unloaded', | ||
transitions: { | ||
load: createTransition('unloaded', 'loaded'), | ||
inject: createTransition(['loaded', 'ejected'], 'injected'), | ||
eject: createTransition('injected', 'ejected'), | ||
unload: createTransition(['loaded', 'ejected', 'injected'], 'unloaded') | ||
}, | ||
methods: { | ||
onLoad: () => { | ||
chrome.tabs.insertCSS(tab_id, { file: 'build/bundle.css' }) | ||
chrome.tabs.executeScript(tab_id, { file: 'web-components.polyfill.js' }) | ||
chrome.tabs.executeScript(tab_id, { file: 'build/bundle.js' }) | ||
}, | ||
onInject: () => { | ||
chrome.tabs.executeScript(tab_id, { file: 'toolbar/inject.js' }) | ||
|
||
state.loaded[tab_id] = true | ||
state.injected[tab_id] = true | ||
getColorMode() | ||
getColorMode() | ||
}, | ||
onEject: () => { | ||
chrome.tabs.executeScript(tab_id, { file: 'toolbar/eject.js' }) | ||
} | ||
} | ||
}; | ||
|
||
const tabs = {} | ||
|
||
chrome.tabs.onUpdated.addListener(function(tabId) { | ||
if (tabId === tab_id) | ||
state.loaded[tabId] = false | ||
}) | ||
export const toggleIn = ({id:tab_id}) => { | ||
// if current tab doesn't have extension state yet - create it | ||
if (!tabs[tab_id]) | ||
tabs[tab_id] = createStateMachine(extensionStateMachineConfig) | ||
|
||
const extension = tabs[tab_id] | ||
|
||
switch (extension.state) { | ||
case 'loaded': | ||
// initial call - load and inject the extension | ||
extension.load() | ||
extension.inject() | ||
break | ||
case 'injected': | ||
// eject the extension | ||
extension.eject() | ||
break | ||
case 'loaded': | ||
case 'ejected': | ||
// extension is already loaded - inject it | ||
extension.inject() | ||
break | ||
} | ||
} | ||
|
||
chrome.tabs.onUpdated.addListener(function(tab_id) { | ||
// tab was updated - unload its state | ||
if (tabs[tab_id]) tabs[tab_id].unload() | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
/** | ||
* @typedef StateMachineParams Config object for state machine creator function | ||
* @property {String} initial Initial value for state machine | ||
* @property {Object} transitions Object of available transitions | ||
* @property {Object} methods | ||
* | ||
* | ||
* @param {StateMachineParams} config State machine configuration object | ||
* @returns {Object} New finite state machine | ||
* @example | ||
* const extensionStateMachine = stateMachine({ | ||
* initial: 'unloaded', | ||
* transitions: { | ||
* load: createTransition('unloaded', 'loaded'), | ||
* inject: createTransition('loaded' , 'injected'), | ||
* eject: createTransition('injected', 'loaded'), | ||
* }, | ||
* methods: { | ||
* onLoad: () => console.log('loading'), | ||
* onInject: () => console.log('injecting'), | ||
* onEject: () => console.log("ejecting") | ||
* } | ||
* }) | ||
*/ | ||
|
||
export function createStateMachine ({ initial, transitions, methods }) { | ||
const proxyObject = { | ||
state: initial, | ||
can: (transition) => transition.from === proxyObject.state || (Array.isArray(transition.from) && transition.from.includes(proxyObject.state)) | ||
} | ||
|
||
return new Proxy(proxyObject, { | ||
get: (self, prop) => { | ||
if (prop in transitions) { | ||
const transition = transitions[prop] | ||
|
||
|
||
return (...params) => { | ||
if (!self.can(transition)) | ||
throw new ReferenceError(`Cannot call '${prop}' on '${self.state}'`) | ||
|
||
const handlerName = 'on' + prop.charAt(0).toUpperCase() + prop.slice(1) | ||
|
||
self.state = transition.to | ||
|
||
if (methods && typeof methods[handlerName] === "function") | ||
methods[handlerName](self, ...params) | ||
} | ||
} else { | ||
return self[prop] | ||
} | ||
} | ||
}) | ||
} | ||
|
||
/** | ||
* @function createTransition | ||
* @param {String|Array} from | ||
* @param {String} to | ||
* @returns {Object} transition object | ||
*/ | ||
export const createTransition = (from, to) => ({from, to}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
import test from 'ava' | ||
import { createStateMachine, createTransition } from "./state-machine" | ||
|
||
test("createTransition should return transition object", t => { | ||
const transition = createTransition("start", "finish") | ||
|
||
t.is(transition.from, "start") | ||
t.is(transition.to, "finish") | ||
}) | ||
|
||
test("should have transition methods available", t => { | ||
const tsm = createStateMachine({ | ||
initial: 'init', | ||
transitions: { | ||
first: createTransition('init' , 'first'), | ||
second: createTransition('first' , 'second'), | ||
third: createTransition('second', 'third') | ||
} | ||
}) | ||
|
||
t.true(typeof tsm.first === "function") | ||
t.true(typeof tsm.second === "function") | ||
t.true(typeof tsm.third === "function") | ||
}) | ||
|
||
test("should have .state property available and representing current state", t => { | ||
const result = [false, false, false] | ||
const tsm = createStateMachine({ | ||
initial: 'init', | ||
transitions: { | ||
first: createTransition('init' , 'first'), | ||
second: createTransition('first' , 'second'), | ||
third: createTransition('second', 'third') | ||
}, | ||
methods: { | ||
onFirst: () => result[0] = true, | ||
onSecond: () => result[1] = true, | ||
onThird: () => result[2] = true | ||
} | ||
}) | ||
|
||
tsm.first() | ||
t.is(tsm.state, 'first') | ||
|
||
tsm.second() | ||
t.is(tsm.state, 'second') | ||
|
||
tsm.third() | ||
t.is(tsm.state, 'third') | ||
}) | ||
|
||
test("should have all handler methods called", t => { | ||
const result = [false, false, false] | ||
const tsm = createStateMachine({ | ||
initial: 'init', | ||
transitions: { | ||
first: createTransition('init' , 'first'), | ||
second: createTransition('first' , 'second'), | ||
third: createTransition('second', 'third') | ||
}, | ||
methods: { | ||
onFirst: () => result[0] = true, | ||
onSecond: () => result[1] = true, | ||
onThird: () => result[2] = true | ||
} | ||
}) | ||
|
||
tsm.first() | ||
tsm.second() | ||
tsm.third() | ||
|
||
t.deepEqual(result, [true, true, true]) | ||
}) | ||
|
||
|
||
test("can update state within handler method", t => { | ||
let executedCalled = false | ||
let loaded = false | ||
|
||
const tsm = createStateMachine({ | ||
initial: "unready", | ||
transitions: { | ||
ready: createTransition('unready', 'unloaded'), | ||
loadAndExecute: createTransition('unloaded', 'loaded'), | ||
execute: createTransition('loaded', 'executed') | ||
}, | ||
methods: { | ||
onLoadAndExecute: () => { | ||
tsm.execute() | ||
}, | ||
onExecute: () => executedCalled = true | ||
} | ||
}) | ||
|
||
t.is(tsm.state, 'unready') | ||
|
||
tsm.ready() | ||
t.is(tsm.state, 'unloaded') | ||
|
||
tsm.loadAndExecute() | ||
t.is(tsm.state, 'executed') | ||
t.true(executedCalled) | ||
|
||
}) | ||
|
||
|
||
test("can have multiple transitions from one state", t => { | ||
const water = createStateMachine({ | ||
initial: 'liquid', | ||
transitions: { | ||
melt: createTransition('solid', 'liquid'), | ||
freeze: createTransition('liquid', 'solid'), | ||
|
||
vaporize: createTransition('liquid', 'gas'), | ||
condense: createTransition('gas', 'liquid') | ||
} | ||
}) | ||
|
||
water.freeze() | ||
t.is(water.state, 'solid') | ||
|
||
water.melt() | ||
t.is(water.state, 'liquid') | ||
|
||
water.vaporize() | ||
t.is(water.state, 'gas') | ||
|
||
water.condense() | ||
t.is(water.state, 'liquid') | ||
}) | ||
|
||
test("can have multiple 'from' values as array", t => { | ||
const cd = createStateMachine({ | ||
initial: 'on_sale', | ||
transitions: { | ||
buy: createTransition('on_sale', 'bought'), | ||
play: createTransition(['bought', 'stopped'], 'playing'), | ||
stop: createTransition('playing', 'stopped') | ||
} | ||
}) | ||
|
||
cd.buy() | ||
t.is(cd.state, 'bought') | ||
|
||
cd.play() | ||
t.is(cd.state, 'playing') | ||
|
||
cd.stop() | ||
t.is(cd.state, 'stopped') | ||
|
||
cd.play() | ||
t.is(cd.state, 'playing') | ||
}) |
3bb5a11
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oooOOOOooooo, i like this 👍 cool work