Skip to content

Commit

Permalink
feat(history): impl history linked list
Browse files Browse the repository at this point in the history
  • Loading branch information
lbwa committed Mar 7, 2020
1 parent e20f16d commit 499450e
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 14 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"core-js": "^3.6.4",
"normalize.css": "^8.0.1",
"tiny-invariant": "^1.1.0",
"vue": "^2.6.11",
"vue-router": "^3.1.5",
"vuex": "^3.1.2"
Expand Down
11 changes: 11 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
/**
* This file is used to record all **symbolic constants**.
*/

/**
* the root router-view layout
*/
export const DEFAULT_LAYOUT = 'LMainWithFooter'
/**
* the max value of history records and should be greater than 1
*/
export const RECORD_MAX_VAL = 20
7 changes: 5 additions & 2 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import Vue from 'vue'
import Vuex from 'vuex'
import modules from './modules'

Vue.use(Vuex)

export default new Vuex.Store({
export type RootState = {}

export default new Vuex.Store<RootState>({
state: {},
mutations: {},
actions: {},
modules: {}
modules
})
84 changes: 84 additions & 0 deletions src/store/modules/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Module } from 'vuex'
import { Route } from 'vue-router'
import invariant from 'tiny-invariant'
import { RootState } from '../index'
import { RECORD_MAX_VAL } from '../../constants'

export type RecordItem = {
name?: string
fullPath: string
next: RecordItem | null
}

interface RecordItemConstructor {
(this: RecordItem, route: Route, next?: RecordItem): void
new (route: Route, next?: RecordItem): RecordItem
}

const RecordItem = function(this: RecordItem, route: Route, next?: RecordItem) {
if (route.name) this.name = route.name
this.fullPath = route.fullPath
this.next = next || null
} as RecordItemConstructor

const history: Module<Record<'recordHead', RecordItem | null>, RootState> = {
namespaced: true,

state: {
recordHead: null
},

mutations: {
append(state, route: Route) {
if (!state.recordHead) {
state.recordHead = new RecordItem(route)
return
}

let current: RecordItem | null = state.recordHead
let size = 1
invariant(
RECORD_MAX_VAL > 1,
'[module/history]: RECORD_MAX_VAL should be greater then 1'
)
while (current.next) {
++size
current = current.next
}
// drop the head when exceed
if (size === RECORD_MAX_VAL) {
state.recordHead = state.recordHead.next
}
// a new tail element of linked list
current.next = new RecordItem(route)
},

prepend(state, route: Route) {
if (!state.recordHead) {
state.recordHead = new RecordItem(route)
return
}

let current: RecordItem | null = state.recordHead
let size = 1
invariant(
RECORD_MAX_VAL > 1,
'[module/history]: RECORD_MAX_VAL should be greater then 1'
)

state.recordHead = new RecordItem(route, state.recordHead)

while (current.next) {
++size
if (size === RECORD_MAX_VAL) {
// drop the tail element
current.next = null
break
}
current = current.next
}
}
}
}

export default history
5 changes: 5 additions & 0 deletions src/store/modules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import history from './history'

export default {
history
}
12 changes: 0 additions & 12 deletions tests/unit/example.spec.ts

This file was deleted.

131 changes: 131 additions & 0 deletions tests/unit/store/history.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import Vue from 'vue'
import Vuex, { Store } from 'vuex'
import moduleHistory, { RecordItem } from '../../../src/store/modules/history'
import { RECORD_MAX_VAL } from '../../../src/constants'

Vue.use(Vuex)

describe('Vuex history module', () => {
let store: Store<Record<string, any>>

beforeEach(() => {
store = new Vuex.Store<Record<string, any>>({
modules: {
history: Object.assign(moduleHistory, {
// make state clear
state: {
recordHead: null
}
})
}
})
})

it('Should append a history record', () => {
const mockRoute = '/mock?query=append'
store.commit('history/append', {
fullPath: mockRoute
})
const { recordHead } = store.state.history
expect(recordHead.fullPath).toEqual(mockRoute)
expect(recordHead.next).toBeNull()
})

it('Should append multiple history records', () => {
const mockFirstRoute = '/mock?query=prepend-one'
const mockSecondRoute = '/mock?query=prepend-two'

store.commit('history/append', {
fullPath: mockFirstRoute
})
store.commit('history/append', {
fullPath: mockSecondRoute
})
const { recordHead } = store.state.history
expect(recordHead.fullPath).toEqual(mockFirstRoute)
expect(recordHead.next).toBeDefined()
expect(recordHead.next.fullPath).toEqual(mockSecondRoute)
expect(recordHead.next.next).toBeNull()
})

it('Should append a history record until exceed', () => {
const actualOffset = 5
new Array(RECORD_MAX_VAL + actualOffset)
.fill(null)
.forEach((item, index) => {
store.commit('history/append', {
fullPath: `/mock?query=${index}`
})
})

const { recordHead } = store.state.history
expect(recordHead.fullPath).toEqual(`/mock?query=${actualOffset}`)
expect(recordHead.next).toBeDefined()

let current: RecordItem = recordHead
let size = 1
while (current.next) {
++size
current = current.next
}
expect(size).toEqual(RECORD_MAX_VAL)
expect(current.fullPath).toEqual(
`/mock?query=${RECORD_MAX_VAL + actualOffset - 1}`
)
expect(current.next).toBeNull()
})

it('Should prepend a history record', () => {
const mockRoute = '/mock?query=prepend'
store.commit('history/prepend', {
fullPath: mockRoute
})
const { recordHead } = store.state.history
expect(recordHead.fullPath).toEqual(mockRoute)
expect(recordHead.next).toBeNull()
})

it('Should prepend multiple history records', () => {
const mockFirstRoute = '/mock?query=prepend-one'
const mockSecondRoute = '/mock?query=prepend-two'

store.commit('history/prepend', {
fullPath: mockFirstRoute
})
store.commit('history/prepend', {
fullPath: mockSecondRoute
})
const { recordHead } = store.state.history
expect(recordHead.fullPath).toEqual(mockSecondRoute)
expect(recordHead.next).toBeDefined()
expect(recordHead.next.fullPath).toEqual(mockFirstRoute)
expect(recordHead.next.next).toBeNull()
})

it('Should prepend a history record until exceed', () => {
const actualOffset = 5
new Array(RECORD_MAX_VAL + actualOffset)
.fill(null)
.forEach((item, index) => {
store.commit('history/prepend', {
fullPath: `/mock?query=${index}`
})
})

const { recordHead } = store.state.history
expect(recordHead.fullPath).toEqual(
`/mock?query=${RECORD_MAX_VAL + actualOffset - 1}`
)
expect(recordHead.next).toBeDefined()

let current: RecordItem = recordHead
let size = 1
while (current.next) {
++size
current = current.next
}
expect(size).toEqual(RECORD_MAX_VAL)
expect(current.fullPath).toEqual(`/mock?query=${actualOffset}`)
expect(current.next).toBeNull()
})
})
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9212,6 +9212,11 @@ timsort@^0.3.0:
resolved "https://registry.npm.taobao.org/timsort/download/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=

tiny-invariant@^1.1.0:
version "1.1.0"
resolved "https://registry.npm.taobao.org/tiny-invariant/download/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha1-Y0xfjv3CdxS384bDXmdgmR0jCHU=

tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.npm.taobao.org/tmp/download/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
Expand Down

0 comments on commit 499450e

Please sign in to comment.