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

Pullrequests/fratzinger/debounce events #546

Merged
merged 13 commits into from
Oct 29, 2020
39 changes: 5 additions & 34 deletions src/service-module/make-service-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ eslint
import { FeathersVuexOptions, MakeServicePluginOptions } from './types'
import makeServiceModule from './make-service-module'
import { globalModels, prepareAddModel } from './global-models'
import enableServiceEvents from './service-module.events'
import { makeNamespace, getServicePath, assignIfNotPresent } from '../utils'
import _get from 'lodash/get'

Expand All @@ -18,6 +19,7 @@ interface ServiceOptionsDefaults {
actions: {}
instanceDefaults: () => {}
setupInstance: (instance: {}) => {}
debounceEventsMaxWait: number
}

const defaults: ServiceOptionsDefaults = {
Expand All @@ -28,7 +30,8 @@ const defaults: ServiceOptionsDefaults = {
mutations: {}, // for custom mutations
actions: {}, // for custom actions
instanceDefaults: () => ({}), // Default instanceDefaults returns an empty object
setupInstance: instance => instance // Default setupInstance returns the instance
setupInstance: instance => instance, // Default setupInstance returns the instance
debounceEventsMaxWait: 1000
}
const events = ['created', 'patched', 'updated', 'removed']

Expand Down Expand Up @@ -120,39 +123,7 @@ export default function prepareMakeServicePlugin(

// (3^) Setup real-time events
if (options.enableEvents) {
const handleEvent = (eventName, item, mutationName) => {
const handler = options.handleEvents[eventName]
const confirmOrArray = handler(item, {
model: Model,
models: globalModels
})
const [affectsStore, modified = item] = Array.isArray(confirmOrArray)
? confirmOrArray
: [confirmOrArray]
if (affectsStore) {
eventName === 'removed'
? store.commit(`${options.namespace}/removeItem`, modified)
: store.dispatch(`${options.namespace}/${mutationName}`, modified)
}
}

// Listen to socket events when available.
service.on('created', item => {
handleEvent('created', item, 'addOrUpdate')
Model.emit && Model.emit('created', item)
})
service.on('updated', item => {
handleEvent('updated', item, 'addOrUpdate')
Model.emit && Model.emit('updated', item)
})
service.on('patched', item => {
handleEvent('patched', item, 'addOrUpdate')
Model.emit && Model.emit('patched', item)
})
service.on('removed', item => {
handleEvent('removed', item, 'removeItem')
Model.emit && Model.emit('removed', item)
})
enableServiceEvents({ service, Model, store, options })
}
}
}
Expand Down
9 changes: 4 additions & 5 deletions src/service-module/service-module.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ export default function makeServiceActions(service: Service<any>) {
const toRemove = []
const { idField, autoRemove } = state

const disableRemove = response.disableRemove || !autoRemove

list.forEach(item => {
const id = getId(item, idField)
const existingItem = state.keyedById[id]
Expand All @@ -310,13 +312,10 @@ export default function makeServiceActions(service: Service<any>) {
}
})

if (!isPaginated && autoRemove) {
if (!isPaginated && !disableRemove) {
// Find IDs from the state which are not in the list
state.ids.forEach(id => {
if (
id !== state.currentId &&
!list.some(item => getId(item, idField) === id)
) {
if (!list.some(item => getId(item, idField) === id)) {
toRemove.push(state.keyedById[id])
}
})
Expand Down
105 changes: 105 additions & 0 deletions src/service-module/service-module.events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { getId } from '../utils'
import _debounce from 'lodash/debounce'
import { globalModels } from './global-models'

export interface ServiceEventsDebouncedQueue {
addOrUpdateById: {}
removeItemById: {}
enqueueAddOrUpdate(item: any): void
enqueueRemoval(item: any): void
flushAddOrUpdateQueue(): void
flushRemoveItemQueue(): void
}

export default function enableServiceEvents({
service,
Model,
store,
options
}): ServiceEventsDebouncedQueue {
const debouncedQueue: ServiceEventsDebouncedQueue = {
addOrUpdateById: {},
removeItemById: {},
enqueueAddOrUpdate(item): void {
const id = getId(item, options.idField)
this.addOrUpdateById[id] = item
if (this.removeItemById.hasOwnProperty(id)) {
delete this.removeItemById[id]
}
this.flushAddOrUpdateQueue()
},
enqueueRemoval(item): void {
const id = getId(item, options.idField)
this.removeItemById[id] = item
if (this.addOrUpdateById.hasOwnProperty(id)) {
delete this.addOrUpdateById[id]
}
this.flushRemoveItemQueue()
},
flushAddOrUpdateQueue: _debounce(
async function () {
const values = Object.values(this.addOrUpdateById)
if (values.length === 0) return
await store.dispatch(`${options.namespace}/addOrUpdateList`, {
data: values,
disableRemove: true
})
this.addOrUpdateById = {}
},
options.debounceEventsTime || 20,
{ maxWait: options.debounceEventsMaxWait }
),
flushRemoveItemQueue: _debounce(
function () {
const values = Object.values(this.removeItemById)
if (values.length === 0) return
store.commit(`${options.namespace}/removeItems`, values)
this.removeItemById = {}
},
options.debounceEventsTime || 20,
{ maxWait: options.debounceEventsMaxWait }
)
}

const handleEvent = (eventName, item, mutationName): void => {
const handler = options.handleEvents[eventName]
const confirmOrArray = handler(item, {
model: Model,
models: globalModels
})
const [affectsStore, modified = item] = Array.isArray(confirmOrArray)
? confirmOrArray
: [confirmOrArray]
if (affectsStore) {
if (!options.debounceEventsTime) {
eventName === 'removed'
? store.commit(`${options.namespace}/removeItem`, modified)
: store.dispatch(`${options.namespace}/${mutationName}`, modified)
} else {
eventName === 'removed'
? debouncedQueue.enqueueRemoval(item)
: debouncedQueue.enqueueAddOrUpdate(item)
}
}
}

// Listen to socket events when available.
service.on('created', item => {
handleEvent('created', item, 'addOrUpdate')
Model.emit && Model.emit('created', item)
})
service.on('updated', item => {
handleEvent('updated', item, 'addOrUpdate')
Model.emit && Model.emit('updated', item)
})
service.on('patched', item => {
handleEvent('patched', item, 'addOrUpdate')
Model.emit && Model.emit('patched', item)
})
service.on('removed', item => {
handleEvent('removed', item, 'removeItem')
Model.emit && Model.emit('removed', item)
})

return debouncedQueue
}
5 changes: 4 additions & 1 deletion src/service-module/service-module.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface ServiceStateExclusiveDefaults {
}
paramsForServer: string[]
modelName?: string

debounceEventsTime: number
isIdCreatePending: Id[]
isIdUpdatePending: Id[]
isIdPatchPending: Id[]
Expand Down Expand Up @@ -83,6 +83,8 @@ export interface ServiceState<M extends Model = Model> {
default?: PaginationState
}
modelName?: string
debounceEventsTime: number
debounceEventsMaxWait: number
isIdCreatePending: Id[]
isIdUpdatePending: Id[]
isIdPatchPending: Id[]
Expand Down Expand Up @@ -121,6 +123,7 @@ export default function makeDefaultState(options: MakeServicePluginOptions) {
defaultSkip: null
},
paramsForServer: ['$populateParams'],
debounceEventsTime: null,

isFindPending: false,
isGetPending: false,
Expand Down
8 changes: 7 additions & 1 deletion src/service-module/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface FeathersVuexOptions {
idField?: string
tempIdField?: string
keepCopiesInStore?: boolean
debounceEventsTime?: number
debounceEventsMaxWait?: number
nameStyle?: string
paramsForServer?: string[]
preferUpdate?: boolean
Expand Down Expand Up @@ -50,6 +52,8 @@ export interface MakeServicePluginOptions {
replaceItems?: boolean
skipRequestIfExists?: boolean
nameStyle?: string
debounceEventsTime?: number
debounceEventsMaxWait?: number

servicePath?: string
namespace?: string
Expand Down Expand Up @@ -245,7 +249,9 @@ export interface ModelStatic extends EventEmitter {
* A proxy for the `find` getter
* @param params Find params
*/
findInStore<M extends Model = Model>(params?: Params | Ref<Params>): Paginated<M>
findInStore<M extends Model = Model>(
params?: Params | Ref<Params>
): Paginated<M>

/**
* A proxy for the `count` action
Expand Down
35 changes: 25 additions & 10 deletions test/fixtures/feathers-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,37 +11,52 @@ const baseUrl = 'http://localhost:3030'

// These are fixtures used in the service-modulet.test.js under socket events.
let id = 0
mockServer.on('things::create', function(data) {
mockServer.on('things::create', function (data) {
data.id = id
id++
mockServer.emit('things created', data)
})
mockServer.on('things::patch', function(id, data) {
mockServer.on('things::patch', function (id, data) {
Object.assign(data, { id, test: true })
mockServer.emit('things patched', data)
})
mockServer.on('things::update', function(id, data) {
mockServer.on('things::update', function (id, data) {
Object.assign(data, { id, test: true })
mockServer.emit('things updated', data)
})
mockServer.on('things::remove', function(id) {
mockServer.on('things::remove', function (id) {
mockServer.emit('things removed', { id, test: true })
})

let idDebounce = 0

mockServer.on('things-debounced::create', function (data) {
data.id = idDebounce
idDebounce++
mockServer.emit('things-debounced created', data)
})
mockServer.on('things-debounced::patch', function (id, data) {
Object.assign(data, { id, test: true })
mockServer.emit('things-debounced patched', data)
})
mockServer.on('things-debounced::update', function (id, data) {
Object.assign(data, { id, test: true })
mockServer.emit('things-debounced updated', data)
})
mockServer.on('things-debounced::remove', function (id) {
mockServer.emit('things-debounced removed', { id, test: true })
})

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function makeFeathersSocketClient(baseUrl) {
const socket = io(baseUrl)

return feathers()
.configure(socketio(socket))
.configure(auth())
return feathers().configure(socketio(socket)).configure(auth())
}

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export function makeFeathersRestClient(baseUrl) {
return feathers()
.configure(rest(baseUrl).axios(axios))
.configure(auth())
return feathers().configure(rest(baseUrl).axios(axios)).configure(auth())
}

const sock = io(baseUrl)
Expand Down
16 changes: 9 additions & 7 deletions test/service-module/make-service-plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import _omit from 'lodash/omit'

Vue.use(Vuex)

describe('makeServicePlugin', function() {
describe('makeServicePlugin', function () {
beforeEach(() => {
clearModels()
})
Expand All @@ -28,7 +28,7 @@ describe('makeServicePlugin', function() {
assert(clients.byAlias['this is a test'], 'got a reference to the client.')
})

it('registers the vuex module with options', function() {
it('registers the vuex module with options', function () {
interface RootState {
todos: {}
}
Expand Down Expand Up @@ -73,6 +73,8 @@ describe('makeServicePlugin', function() {
isRemovePending: false,
isUpdatePending: false,
keepCopiesInStore: false,
debounceEventsTime: null,
debounceEventsMaxWait: 1000,
keyedById: {},
modelName: 'Todo',
nameStyle: 'short',
Expand All @@ -98,7 +100,7 @@ describe('makeServicePlugin', function() {
assert.deepEqual(_omit(received), _omit(expected), 'defaults in place.')
})

it('sets up Model.store && service.FeathersVuexModel', function() {
it('sets up Model.store && service.FeathersVuexModel', function () {
const serverAlias = 'default'
const { makeServicePlugin, BaseModel } = feathersVuex(feathers, {
serverAlias
Expand All @@ -118,7 +120,7 @@ describe('makeServicePlugin', function() {
assert.equal(service.FeathersVuexModel, Todo, 'Model accessible on service')
})

it('allows accessing other models', function() {
it('allows accessing other models', function () {
const serverAlias = 'default'
const { makeServicePlugin, BaseModel, models } = feathersVuex(feathers, {
idField: '_id',
Expand All @@ -144,7 +146,7 @@ describe('makeServicePlugin', function() {
assert(Todo.store === store)
})

it('allows service specific handleEvents', async function() {
it('allows service specific handleEvents', async function () {
// feathers.use('todos', new TodosService())
const serverAlias = 'default'
const { makeServicePlugin, BaseModel } = feathersVuex(feathers, {
Expand Down Expand Up @@ -239,7 +241,7 @@ describe('makeServicePlugin', function() {
assert(removedCalled, 'removed handler called')
})

it('fall back to globalOptions handleEvents if service specific handleEvents handler is missing', async function() {
it('fall back to globalOptions handleEvents if service specific handleEvents handler is missing', async function () {
// feathers.use('todos', new TodosService())
const serverAlias = 'default'

Expand Down Expand Up @@ -343,7 +345,7 @@ describe('makeServicePlugin', function() {
assert(globalRemovedCalled, 'global removed handler called')
})

it('allow handleEvents handlers to return extracted event data', async function() {
it('allow handleEvents handlers to return extracted event data', async function () {
const serverAlias = 'default'

const { makeServicePlugin, BaseModel } = feathersVuex(feathers, {
Expand Down
Loading