Skip to content

Commit 8e30056

Browse files
Merge pull request #546 from feathersjs-ecosystem/pullrequests/fratzinger/debounce-events
Pullrequests/fratzinger/debounce events
2 parents 4cc137f + 542d12a commit 8e30056

9 files changed

+551
-145
lines changed

src/service-module/make-service-plugin.ts

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ eslint
66
import { FeathersVuexOptions, MakeServicePluginOptions } from './types'
77
import makeServiceModule from './make-service-module'
88
import { globalModels, prepareAddModel } from './global-models'
9+
import enableServiceEvents from './service-module.events'
910
import { makeNamespace, getServicePath, assignIfNotPresent } from '../utils'
1011
import _get from 'lodash/get'
1112

@@ -18,6 +19,7 @@ interface ServiceOptionsDefaults {
1819
actions: {}
1920
instanceDefaults: () => {}
2021
setupInstance: (instance: {}) => {}
22+
debounceEventsMaxWait: number
2123
}
2224

2325
const defaults: ServiceOptionsDefaults = {
@@ -28,7 +30,8 @@ const defaults: ServiceOptionsDefaults = {
2830
mutations: {}, // for custom mutations
2931
actions: {}, // for custom actions
3032
instanceDefaults: () => ({}), // Default instanceDefaults returns an empty object
31-
setupInstance: instance => instance // Default setupInstance returns the instance
33+
setupInstance: instance => instance, // Default setupInstance returns the instance
34+
debounceEventsMaxWait: 1000
3235
}
3336
const events = ['created', 'patched', 'updated', 'removed']
3437

@@ -120,39 +123,7 @@ export default function prepareMakeServicePlugin(
120123

121124
// (3^) Setup real-time events
122125
if (options.enableEvents) {
123-
const handleEvent = (eventName, item, mutationName) => {
124-
const handler = options.handleEvents[eventName]
125-
const confirmOrArray = handler(item, {
126-
model: Model,
127-
models: globalModels
128-
})
129-
const [affectsStore, modified = item] = Array.isArray(confirmOrArray)
130-
? confirmOrArray
131-
: [confirmOrArray]
132-
if (affectsStore) {
133-
eventName === 'removed'
134-
? store.commit(`${options.namespace}/removeItem`, modified)
135-
: store.dispatch(`${options.namespace}/${mutationName}`, modified)
136-
}
137-
}
138-
139-
// Listen to socket events when available.
140-
service.on('created', item => {
141-
handleEvent('created', item, 'addOrUpdate')
142-
Model.emit && Model.emit('created', item)
143-
})
144-
service.on('updated', item => {
145-
handleEvent('updated', item, 'addOrUpdate')
146-
Model.emit && Model.emit('updated', item)
147-
})
148-
service.on('patched', item => {
149-
handleEvent('patched', item, 'addOrUpdate')
150-
Model.emit && Model.emit('patched', item)
151-
})
152-
service.on('removed', item => {
153-
handleEvent('removed', item, 'removeItem')
154-
Model.emit && Model.emit('removed', item)
155-
})
126+
enableServiceEvents({ service, Model, store, options })
156127
}
157128
}
158129
}

src/service-module/service-module.actions.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,8 @@ export default function makeServiceActions(service: Service<any>) {
301301
const toRemove = []
302302
const { idField, autoRemove } = state
303303

304+
const disableRemove = response.disableRemove || !autoRemove
305+
304306
list.forEach(item => {
305307
const id = getId(item, idField)
306308
const existingItem = state.keyedById[id]
@@ -310,13 +312,10 @@ export default function makeServiceActions(service: Service<any>) {
310312
}
311313
})
312314

313-
if (!isPaginated && autoRemove) {
315+
if (!isPaginated && !disableRemove) {
314316
// Find IDs from the state which are not in the list
315317
state.ids.forEach(id => {
316-
if (
317-
id !== state.currentId &&
318-
!list.some(item => getId(item, idField) === id)
319-
) {
318+
if (!list.some(item => getId(item, idField) === id)) {
320319
toRemove.push(state.keyedById[id])
321320
}
322321
})
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { getId } from '../utils'
2+
import _debounce from 'lodash/debounce'
3+
import { globalModels } from './global-models'
4+
5+
export interface ServiceEventsDebouncedQueue {
6+
addOrUpdateById: {}
7+
removeItemById: {}
8+
enqueueAddOrUpdate(item: any): void
9+
enqueueRemoval(item: any): void
10+
flushAddOrUpdateQueue(): void
11+
flushRemoveItemQueue(): void
12+
}
13+
14+
export default function enableServiceEvents({
15+
service,
16+
Model,
17+
store,
18+
options
19+
}): ServiceEventsDebouncedQueue {
20+
const debouncedQueue: ServiceEventsDebouncedQueue = {
21+
addOrUpdateById: {},
22+
removeItemById: {},
23+
enqueueAddOrUpdate(item): void {
24+
const id = getId(item, options.idField)
25+
this.addOrUpdateById[id] = item
26+
if (this.removeItemById.hasOwnProperty(id)) {
27+
delete this.removeItemById[id]
28+
}
29+
this.flushAddOrUpdateQueue()
30+
},
31+
enqueueRemoval(item): void {
32+
const id = getId(item, options.idField)
33+
this.removeItemById[id] = item
34+
if (this.addOrUpdateById.hasOwnProperty(id)) {
35+
delete this.addOrUpdateById[id]
36+
}
37+
this.flushRemoveItemQueue()
38+
},
39+
flushAddOrUpdateQueue: _debounce(
40+
async function () {
41+
const values = Object.values(this.addOrUpdateById)
42+
if (values.length === 0) return
43+
await store.dispatch(`${options.namespace}/addOrUpdateList`, {
44+
data: values,
45+
disableRemove: true
46+
})
47+
this.addOrUpdateById = {}
48+
},
49+
options.debounceEventsTime || 20,
50+
{ maxWait: options.debounceEventsMaxWait }
51+
),
52+
flushRemoveItemQueue: _debounce(
53+
function () {
54+
const values = Object.values(this.removeItemById)
55+
if (values.length === 0) return
56+
store.commit(`${options.namespace}/removeItems`, values)
57+
this.removeItemById = {}
58+
},
59+
options.debounceEventsTime || 20,
60+
{ maxWait: options.debounceEventsMaxWait }
61+
)
62+
}
63+
64+
const handleEvent = (eventName, item, mutationName): void => {
65+
const handler = options.handleEvents[eventName]
66+
const confirmOrArray = handler(item, {
67+
model: Model,
68+
models: globalModels
69+
})
70+
const [affectsStore, modified = item] = Array.isArray(confirmOrArray)
71+
? confirmOrArray
72+
: [confirmOrArray]
73+
if (affectsStore) {
74+
if (!options.debounceEventsTime) {
75+
eventName === 'removed'
76+
? store.commit(`${options.namespace}/removeItem`, modified)
77+
: store.dispatch(`${options.namespace}/${mutationName}`, modified)
78+
} else {
79+
eventName === 'removed'
80+
? debouncedQueue.enqueueRemoval(item)
81+
: debouncedQueue.enqueueAddOrUpdate(item)
82+
}
83+
}
84+
}
85+
86+
// Listen to socket events when available.
87+
service.on('created', item => {
88+
handleEvent('created', item, 'addOrUpdate')
89+
Model.emit && Model.emit('created', item)
90+
})
91+
service.on('updated', item => {
92+
handleEvent('updated', item, 'addOrUpdate')
93+
Model.emit && Model.emit('updated', item)
94+
})
95+
service.on('patched', item => {
96+
handleEvent('patched', item, 'addOrUpdate')
97+
Model.emit && Model.emit('patched', item)
98+
})
99+
service.on('removed', item => {
100+
handleEvent('removed', item, 'removeItem')
101+
Model.emit && Model.emit('removed', item)
102+
})
103+
104+
return debouncedQueue
105+
}

src/service-module/service-module.state.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export interface ServiceStateExclusiveDefaults {
3737
}
3838
paramsForServer: string[]
3939
modelName?: string
40-
40+
debounceEventsTime: number
4141
isIdCreatePending: Id[]
4242
isIdUpdatePending: Id[]
4343
isIdPatchPending: Id[]
@@ -83,6 +83,8 @@ export interface ServiceState<M extends Model = Model> {
8383
default?: PaginationState
8484
}
8585
modelName?: string
86+
debounceEventsTime: number
87+
debounceEventsMaxWait: number
8688
isIdCreatePending: Id[]
8789
isIdUpdatePending: Id[]
8890
isIdPatchPending: Id[]
@@ -121,6 +123,7 @@ export default function makeDefaultState(options: MakeServicePluginOptions) {
121123
defaultSkip: null
122124
},
123125
paramsForServer: ['$populateParams'],
126+
debounceEventsTime: null,
124127

125128
isFindPending: false,
126129
isGetPending: false,

src/service-module/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export interface FeathersVuexOptions {
2020
idField?: string
2121
tempIdField?: string
2222
keepCopiesInStore?: boolean
23+
debounceEventsTime?: number
24+
debounceEventsMaxWait?: number
2325
nameStyle?: string
2426
paramsForServer?: string[]
2527
preferUpdate?: boolean
@@ -50,6 +52,8 @@ export interface MakeServicePluginOptions {
5052
replaceItems?: boolean
5153
skipRequestIfExists?: boolean
5254
nameStyle?: string
55+
debounceEventsTime?: number
56+
debounceEventsMaxWait?: number
5357

5458
servicePath?: string
5559
namespace?: string
@@ -245,7 +249,9 @@ export interface ModelStatic extends EventEmitter {
245249
* A proxy for the `find` getter
246250
* @param params Find params
247251
*/
248-
findInStore<M extends Model = Model>(params?: Params | Ref<Params>): Paginated<M>
252+
findInStore<M extends Model = Model>(
253+
params?: Params | Ref<Params>
254+
): Paginated<M>
249255

250256
/**
251257
* A proxy for the `count` action

test/fixtures/feathers-client.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,52 @@ const baseUrl = 'http://localhost:3030'
1111

1212
// These are fixtures used in the service-modulet.test.js under socket events.
1313
let id = 0
14-
mockServer.on('things::create', function(data) {
14+
mockServer.on('things::create', function (data) {
1515
data.id = id
1616
id++
1717
mockServer.emit('things created', data)
1818
})
19-
mockServer.on('things::patch', function(id, data) {
19+
mockServer.on('things::patch', function (id, data) {
2020
Object.assign(data, { id, test: true })
2121
mockServer.emit('things patched', data)
2222
})
23-
mockServer.on('things::update', function(id, data) {
23+
mockServer.on('things::update', function (id, data) {
2424
Object.assign(data, { id, test: true })
2525
mockServer.emit('things updated', data)
2626
})
27-
mockServer.on('things::remove', function(id) {
27+
mockServer.on('things::remove', function (id) {
2828
mockServer.emit('things removed', { id, test: true })
2929
})
3030

31+
let idDebounce = 0
32+
33+
mockServer.on('things-debounced::create', function (data) {
34+
data.id = idDebounce
35+
idDebounce++
36+
mockServer.emit('things-debounced created', data)
37+
})
38+
mockServer.on('things-debounced::patch', function (id, data) {
39+
Object.assign(data, { id, test: true })
40+
mockServer.emit('things-debounced patched', data)
41+
})
42+
mockServer.on('things-debounced::update', function (id, data) {
43+
Object.assign(data, { id, test: true })
44+
mockServer.emit('things-debounced updated', data)
45+
})
46+
mockServer.on('things-debounced::remove', function (id) {
47+
mockServer.emit('things-debounced removed', { id, test: true })
48+
})
49+
3150
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
3251
export function makeFeathersSocketClient(baseUrl) {
3352
const socket = io(baseUrl)
3453

35-
return feathers()
36-
.configure(socketio(socket))
37-
.configure(auth())
54+
return feathers().configure(socketio(socket)).configure(auth())
3855
}
3956

4057
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
4158
export function makeFeathersRestClient(baseUrl) {
42-
return feathers()
43-
.configure(rest(baseUrl).axios(axios))
44-
.configure(auth())
59+
return feathers().configure(rest(baseUrl).axios(axios)).configure(auth())
4560
}
4661

4762
const sock = io(baseUrl)

test/service-module/make-service-plugin.test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import _omit from 'lodash/omit'
1616

1717
Vue.use(Vuex)
1818

19-
describe('makeServicePlugin', function() {
19+
describe('makeServicePlugin', function () {
2020
beforeEach(() => {
2121
clearModels()
2222
})
@@ -28,7 +28,7 @@ describe('makeServicePlugin', function() {
2828
assert(clients.byAlias['this is a test'], 'got a reference to the client.')
2929
})
3030

31-
it('registers the vuex module with options', function() {
31+
it('registers the vuex module with options', function () {
3232
interface RootState {
3333
todos: {}
3434
}
@@ -73,6 +73,8 @@ describe('makeServicePlugin', function() {
7373
isRemovePending: false,
7474
isUpdatePending: false,
7575
keepCopiesInStore: false,
76+
debounceEventsTime: null,
77+
debounceEventsMaxWait: 1000,
7678
keyedById: {},
7779
modelName: 'Todo',
7880
nameStyle: 'short',
@@ -98,7 +100,7 @@ describe('makeServicePlugin', function() {
98100
assert.deepEqual(_omit(received), _omit(expected), 'defaults in place.')
99101
})
100102

101-
it('sets up Model.store && service.FeathersVuexModel', function() {
103+
it('sets up Model.store && service.FeathersVuexModel', function () {
102104
const serverAlias = 'default'
103105
const { makeServicePlugin, BaseModel } = feathersVuex(feathers, {
104106
serverAlias
@@ -118,7 +120,7 @@ describe('makeServicePlugin', function() {
118120
assert.equal(service.FeathersVuexModel, Todo, 'Model accessible on service')
119121
})
120122

121-
it('allows accessing other models', function() {
123+
it('allows accessing other models', function () {
122124
const serverAlias = 'default'
123125
const { makeServicePlugin, BaseModel, models } = feathersVuex(feathers, {
124126
idField: '_id',
@@ -144,7 +146,7 @@ describe('makeServicePlugin', function() {
144146
assert(Todo.store === store)
145147
})
146148

147-
it('allows service specific handleEvents', async function() {
149+
it('allows service specific handleEvents', async function () {
148150
// feathers.use('todos', new TodosService())
149151
const serverAlias = 'default'
150152
const { makeServicePlugin, BaseModel } = feathersVuex(feathers, {
@@ -239,7 +241,7 @@ describe('makeServicePlugin', function() {
239241
assert(removedCalled, 'removed handler called')
240242
})
241243

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

@@ -343,7 +345,7 @@ describe('makeServicePlugin', function() {
343345
assert(globalRemovedCalled, 'global removed handler called')
344346
})
345347

346-
it('allow handleEvents handlers to return extracted event data', async function() {
348+
it('allow handleEvents handlers to return extracted event data', async function () {
347349
const serverAlias = 'default'
348350

349351
const { makeServicePlugin, BaseModel } = feathersVuex(feathers, {

0 commit comments

Comments
 (0)