Skip to content

Commit

Permalink
Merge pull request #546 from feathersjs-ecosystem/pullrequests/fratzi…
Browse files Browse the repository at this point in the history
…nger/debounce-events

Pullrequests/fratzinger/debounce events
  • Loading branch information
marshallswain authored Oct 29, 2020
2 parents 4cc137f + 542d12a commit 8e30056
Show file tree
Hide file tree
Showing 9 changed files with 551 additions and 145 deletions.
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

0 comments on commit 8e30056

Please sign in to comment.