jason-rails 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/.ruby-version +1 -0
- data/Gemfile.lock +152 -2
- data/README.md +117 -5
- data/app/controllers/jason/api/pusher_controller.rb +15 -0
- data/app/controllers/jason/api_controller.rb +44 -2
- data/client/lib/JasonContext.d.ts +6 -1
- data/client/lib/JasonProvider.d.ts +2 -2
- data/client/lib/JasonProvider.js +5 -124
- data/client/lib/createJasonReducers.js +48 -3
- data/client/lib/createOptDis.js +0 -2
- data/client/lib/createPayloadHandler.d.ts +9 -1
- data/client/lib/createPayloadHandler.js +47 -55
- data/client/lib/createServerActionQueue.d.ts +10 -0
- data/client/lib/createServerActionQueue.js +48 -0
- data/client/lib/createServerActionQueue.test.d.ts +1 -0
- data/client/lib/createServerActionQueue.test.js +37 -0
- data/client/lib/createTransportAdapter.d.ts +5 -0
- data/client/lib/createTransportAdapter.js +20 -0
- data/client/lib/index.d.ts +3 -2
- data/client/lib/pruneIdsMiddleware.d.ts +2 -0
- data/client/lib/pruneIdsMiddleware.js +24 -0
- data/client/lib/restClient.d.ts +2 -0
- data/client/lib/restClient.js +17 -0
- data/client/lib/transportAdapters/actionCableAdapter.d.ts +5 -0
- data/client/lib/transportAdapters/actionCableAdapter.js +35 -0
- data/client/lib/transportAdapters/pusherAdapter.d.ts +5 -0
- data/client/lib/transportAdapters/pusherAdapter.js +68 -0
- data/client/lib/useJason.d.ts +5 -0
- data/client/lib/useJason.js +94 -0
- data/client/lib/useJason.test.d.ts +1 -0
- data/client/lib/useJason.test.js +85 -0
- data/client/lib/useSub.d.ts +1 -1
- data/client/lib/useSub.js +6 -3
- data/client/package.json +5 -3
- data/client/src/JasonProvider.tsx +5 -123
- data/client/src/createJasonReducers.ts +56 -3
- data/client/src/createOptDis.ts +0 -2
- data/client/src/createPayloadHandler.ts +53 -64
- data/client/src/createServerActionQueue.test.ts +42 -0
- data/client/src/createServerActionQueue.ts +47 -0
- data/client/src/createTransportAdapter.ts +13 -0
- data/client/src/pruneIdsMiddleware.ts +24 -0
- data/client/src/restClient.ts +14 -0
- data/client/src/transportAdapters/actionCableAdapter.ts +38 -0
- data/client/src/transportAdapters/pusherAdapter.ts +72 -0
- data/client/src/useJason.test.ts +87 -0
- data/client/src/useJason.ts +110 -0
- data/client/src/useSub.ts +6 -3
- data/client/yarn.lock +71 -3
- data/config/routes.rb +5 -1
- data/jason-rails.gemspec +4 -0
- data/lib/jason.rb +61 -1
- data/lib/jason/api_model.rb +2 -12
- data/lib/jason/broadcaster.rb +19 -0
- data/lib/jason/channel.rb +50 -21
- data/lib/jason/graph_helper.rb +165 -0
- data/lib/jason/includes_helper.rb +108 -0
- data/lib/jason/lua_generator.rb +71 -0
- data/lib/jason/publisher.rb +82 -37
- data/lib/jason/publisher_old.rb +112 -0
- data/lib/jason/subscription.rb +349 -97
- data/lib/jason/subscription_old.rb +171 -0
- data/lib/jason/version.rb +1 -1
- metadata +80 -11
- data/app/assets/config/jason_engine_manifest.js +0 -1
- data/app/assets/images/jason/engine/.keep +0 -0
- data/app/assets/stylesheets/jason/engine/application.css +0 -15
- data/app/helpers/jason/engine/application_helper.rb +0 -6
- data/app/jobs/jason/engine/application_job.rb +0 -6
- data/app/mailers/jason/engine/application_mailer.rb +0 -8
- data/app/models/jason/engine/application_record.rb +0 -7
- data/app/views/layouts/jason/engine/application.html.erb +0 -15
@@ -1,130 +1,12 @@
|
|
1
|
-
import
|
2
|
-
import
|
3
|
-
import JasonContext from './JasonContext'
|
4
|
-
import axios from 'axios'
|
5
|
-
import applyCaseMiddleware from 'axios-case-converter'
|
1
|
+
import React from 'react'
|
2
|
+
import useJason from './useJason'
|
6
3
|
import { Provider } from 'react-redux'
|
7
|
-
import
|
8
|
-
import createJasonReducers from './createJasonReducers'
|
9
|
-
import createPayloadHandler from './createPayloadHandler'
|
10
|
-
import createOptDis from './createOptDis'
|
11
|
-
import makeEager from './makeEager'
|
12
|
-
import { camelizeKeys } from 'humps'
|
13
|
-
import md5 from 'blueimp-md5'
|
14
|
-
import _ from 'lodash'
|
15
|
-
import React, { useState, useEffect } from 'react'
|
16
|
-
import { validate as isUuid } from 'uuid'
|
4
|
+
import JasonContext from './JasonContext'
|
17
5
|
|
18
6
|
const JasonProvider = ({ reducers, middleware, extraActions, children }: { reducers?: any, middleware?: any, extraActions?: any, children?: React.FC }) => {
|
19
|
-
const [store,
|
20
|
-
const [value, setValue] = useState(null)
|
21
|
-
const [connected, setConnected] = useState(false)
|
22
|
-
|
23
|
-
const csrfToken = (document.querySelector("meta[name=csrf-token]") as any).content
|
24
|
-
axios.defaults.headers.common['X-CSRF-Token'] = csrfToken
|
25
|
-
const restClient = applyCaseMiddleware(axios.create(), {
|
26
|
-
preservedKeys: (key) => {
|
27
|
-
return isUuid(key)
|
28
|
-
}
|
29
|
-
})
|
30
|
-
|
31
|
-
useEffect(() => {
|
32
|
-
restClient.get('/jason/api/schema')
|
33
|
-
.then(({ data: snakey_schema }) => {
|
34
|
-
const schema = camelizeKeys(snakey_schema)
|
35
|
-
|
36
|
-
const serverActionQueue = function() {
|
37
|
-
const queue: any[] = []
|
38
|
-
let inFlight = false
|
39
|
-
|
40
|
-
return {
|
41
|
-
addItem: (item) => queue.push(item),
|
42
|
-
getItem: () => {
|
43
|
-
if (inFlight) return false
|
44
|
-
|
45
|
-
const item = queue.shift()
|
46
|
-
if (item) {
|
47
|
-
inFlight = true
|
48
|
-
return item
|
49
|
-
}
|
50
|
-
return false
|
51
|
-
},
|
52
|
-
itemProcessed: () => inFlight = false,
|
53
|
-
fullySynced: () => queue.length === 0 && !inFlight,
|
54
|
-
getData: () => ({ queue, inFlight })
|
55
|
-
}
|
56
|
-
}()
|
57
|
-
|
58
|
-
const consumer = createConsumer()
|
59
|
-
const allReducers = {
|
60
|
-
...reducers,
|
61
|
-
...createJasonReducers(schema)
|
62
|
-
}
|
63
|
-
|
64
|
-
console.log({ schema, allReducers })
|
65
|
-
const store = configureStore({ reducer: allReducers, middleware })
|
66
|
-
|
67
|
-
let payloadHandlers = {}
|
68
|
-
function handlePayload(payload) {
|
69
|
-
const { model, md5Hash } = payload
|
70
|
-
console.log({ md5Hash, fn: `${model}:${md5Hash}`, payloadHandlers, model: _.camelCase(model), payload })
|
71
|
-
const handler = payloadHandlers[`${_.camelCase(model)}:${md5Hash}`]
|
72
|
-
if (handler) {
|
73
|
-
handler({ ...payload, model: _.camelCase(model) })
|
74
|
-
}
|
75
|
-
}
|
76
|
-
|
77
|
-
const subscription = (consumer.subscriptions.create({
|
78
|
-
channel: 'Jason::Channel'
|
79
|
-
}, {
|
80
|
-
connected: () => {
|
81
|
-
setConnected(true)
|
82
|
-
},
|
83
|
-
received: payload => {
|
84
|
-
console.log("Payload received", payload)
|
85
|
-
handlePayload(payload)
|
86
|
-
},
|
87
|
-
disconnected: () => console.warn('Disconnected from ActionCable')
|
88
|
-
}));
|
89
|
-
|
90
|
-
console.log('sending message')
|
91
|
-
subscription.send({ message: 'test' })
|
92
|
-
|
93
|
-
function createSubscription(config) {
|
94
|
-
const md5Hash = md5(JSON.stringify(config))
|
95
|
-
console.log('Subscribe with', config, md5Hash)
|
96
|
-
|
97
|
-
_.map(config, (v, model) => {
|
98
|
-
payloadHandlers[`${model}:${md5Hash}`] = createPayloadHandler(store.dispatch, serverActionQueue, subscription, model, schema[model])
|
99
|
-
})
|
100
|
-
subscription.send({ createSubscription: config })
|
101
|
-
|
102
|
-
return () => removeSubscription(config)
|
103
|
-
}
|
104
|
-
|
105
|
-
function removeSubscription(config) {
|
106
|
-
subscription.send({ removeSubscription: config })
|
107
|
-
const md5Hash = md5(JSON.stringify(config))
|
108
|
-
_.map(config, (v, model) => {
|
109
|
-
delete payloadHandlers[`${model}:${md5Hash}`]
|
110
|
-
})
|
111
|
-
}
|
112
|
-
const optDis = createOptDis(schema, store.dispatch, restClient, serverActionQueue)
|
113
|
-
const actions = createActions(schema, store, restClient, optDis, extraActions)
|
114
|
-
const eager = makeEager(schema)
|
115
|
-
|
116
|
-
console.log({ actions })
|
117
|
-
|
118
|
-
setValue({
|
119
|
-
actions: actions,
|
120
|
-
subscribe: (config) => createSubscription(config),
|
121
|
-
eager
|
122
|
-
})
|
123
|
-
setStore(store)
|
124
|
-
})
|
125
|
-
}, [])
|
7
|
+
const [store, value] = useJason({ reducers, middleware, extraActions })
|
126
8
|
|
127
|
-
if(!(store && value
|
9
|
+
if(!(store && value)) return <div /> // Wait for async fetch of schema to complete
|
128
10
|
|
129
11
|
return <Provider store={store}>
|
130
12
|
<JasonContext.Provider value={value}>{ children }</JasonContext.Provider>
|
@@ -2,8 +2,8 @@ import { createEntityAdapter, createSlice } from '@reduxjs/toolkit'
|
|
2
2
|
import pluralize from 'pluralize'
|
3
3
|
import _ from 'lodash'
|
4
4
|
|
5
|
-
function generateSlices(
|
6
|
-
const sliceNames =
|
5
|
+
function generateSlices(models) {
|
6
|
+
const sliceNames = models.map(k => pluralize(k))
|
7
7
|
const adapter = createEntityAdapter()
|
8
8
|
|
9
9
|
return _.fromPairs(_.map(sliceNames, name => {
|
@@ -30,6 +30,59 @@ function generateSlices(schema) {
|
|
30
30
|
}))
|
31
31
|
}
|
32
32
|
|
33
|
+
function generateJasonSlices(models) {
|
34
|
+
const initialState = _.fromPairs(_.map(models, (model_name) => {
|
35
|
+
return [model_name, {}]
|
36
|
+
}))
|
37
|
+
|
38
|
+
const modelSliceReducer = createSlice({
|
39
|
+
name: 'jasonModels',
|
40
|
+
initialState,
|
41
|
+
reducers: {
|
42
|
+
setSubscriptionIds(s,a) {
|
43
|
+
const { payload } = a
|
44
|
+
const { subscriptionId, model, ids } = payload
|
45
|
+
console.log({ initialState })
|
46
|
+
s[model][subscriptionId] = ids
|
47
|
+
},
|
48
|
+
addSubscriptionId(s,a) {
|
49
|
+
const { payload } = a
|
50
|
+
const { subscriptionId, model, id } = payload
|
51
|
+
s[model][subscriptionId] = _.union(s[model][subscriptionId] || [], [id])
|
52
|
+
},
|
53
|
+
removeSubscriptionId(s,a) {
|
54
|
+
const { payload } = a
|
55
|
+
const { subscriptionId, model, id } = payload
|
56
|
+
s[model][subscriptionId] = _.remove(s[model][subscriptionId] || [], id)
|
57
|
+
},
|
58
|
+
removeSubscription(s, a) {
|
59
|
+
const { payload: { subscriptionId } } = a
|
60
|
+
_.map(models, model => {
|
61
|
+
delete s[model][subscriptionId]
|
62
|
+
})
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}).reducer
|
66
|
+
|
67
|
+
const jasonSliceReducer = createSlice({
|
68
|
+
name: 'jason',
|
69
|
+
initialState: {
|
70
|
+
connected: false,
|
71
|
+
queueSize: 0
|
72
|
+
},
|
73
|
+
reducers: {
|
74
|
+
upsert: (s,a) => ({ ...s, ...a.payload })
|
75
|
+
}
|
76
|
+
}).reducer
|
77
|
+
|
78
|
+
return { jason: jasonSliceReducer, jasonModels: modelSliceReducer }
|
79
|
+
}
|
80
|
+
|
33
81
|
export default function createJasonReducers(schema) {
|
34
|
-
|
82
|
+
const models = _.keys(schema)
|
83
|
+
|
84
|
+
return {
|
85
|
+
...generateSlices(models),
|
86
|
+
...generateJasonSlices(models)
|
87
|
+
}
|
35
88
|
}
|
data/client/src/createOptDis.ts
CHANGED
@@ -13,7 +13,6 @@ function enrich(type, payload) {
|
|
13
13
|
|
14
14
|
export default function createOptDis(schema, dispatch, restClient, serverActionQueue) {
|
15
15
|
const plurals = _.keys(schema).map(k => pluralize(k))
|
16
|
-
let inFlight = false
|
17
16
|
|
18
17
|
function enqueueServerAction (action) {
|
19
18
|
serverActionQueue.addItem(action)
|
@@ -23,7 +22,6 @@ export default function createOptDis(schema, dispatch, restClient, serverActionQ
|
|
23
22
|
const action = serverActionQueue.getItem()
|
24
23
|
if (!action) return
|
25
24
|
|
26
|
-
inFlight = true
|
27
25
|
restClient.post('/jason/api/action', action)
|
28
26
|
.then(serverActionQueue.itemProcessed)
|
29
27
|
.catch(e => {
|
@@ -2,18 +2,17 @@ import { apply_patch } from 'jsonpatch'
|
|
2
2
|
import deepCamelizeKeys from './deepCamelizeKeys'
|
3
3
|
import pluralize from 'pluralize'
|
4
4
|
import _ from 'lodash'
|
5
|
-
import { validate as isUuid } from 'uuid'
|
5
|
+
import { validate as isUuid, v4 as uuidv4 } from 'uuid'
|
6
6
|
|
7
7
|
function diffSeconds(dt2, dt1) {
|
8
8
|
var diff =(dt2.getTime() - dt1.getTime()) / 1000
|
9
9
|
return Math.abs(Math.round(diff))
|
10
10
|
}
|
11
11
|
|
12
|
-
export default function createPayloadHandler(dispatch, serverActionQueue,
|
13
|
-
|
14
|
-
|
15
|
-
let
|
16
|
-
let idx = 0
|
12
|
+
export default function createPayloadHandler({ dispatch, serverActionQueue, transportAdapter, config }) {
|
13
|
+
const subscriptionId = uuidv4()
|
14
|
+
|
15
|
+
let idx = {}
|
17
16
|
let patchQueue = {}
|
18
17
|
|
19
18
|
let lastCheckAt = new Date()
|
@@ -21,8 +20,7 @@ export default function createPayloadHandler(dispatch, serverActionQueue, subscr
|
|
21
20
|
let checkInterval
|
22
21
|
|
23
22
|
function getPayload() {
|
24
|
-
|
25
|
-
subscription.send({ getPayload: { model, config } })
|
23
|
+
setTimeout(() => transportAdapter.getPayload(config), 1000)
|
26
24
|
}
|
27
25
|
|
28
26
|
function camelizeKeys(item) {
|
@@ -31,87 +29,78 @@ export default function createPayloadHandler(dispatch, serverActionQueue, subscr
|
|
31
29
|
|
32
30
|
const tGetPayload = _.throttle(getPayload, 10000)
|
33
31
|
|
34
|
-
function
|
35
|
-
|
36
|
-
if (!serverActionQueue.fullySynced()) {
|
37
|
-
console.log(serverActionQueue.getData())
|
38
|
-
setTimeout(dispatchPayload, 100)
|
39
|
-
return
|
40
|
-
}
|
41
|
-
|
42
|
-
const includeModels = (config.includeModels || []).map(m => _.camelCase(m))
|
43
|
-
|
44
|
-
console.log("Dispatching", { payload, includeModels })
|
45
|
-
|
46
|
-
includeModels.forEach(m => {
|
47
|
-
const subPayload = _.flatten(_.compact(camelizeKeys(payload).map(instance => instance[m])))
|
48
|
-
const previousSubPayload = _.flatten(_.compact(camelizeKeys(previousPayload).map(instance => instance[m])))
|
49
|
-
|
50
|
-
// Find IDs that were in the payload but are no longer
|
51
|
-
const idsToRemove = _.difference(previousSubPayload.map(i => i.id), subPayload.map(i => i.id))
|
52
|
-
|
53
|
-
dispatch({ type: `${pluralize(m)}/upsertMany`, payload: subPayload })
|
54
|
-
dispatch({ type: `${pluralize(m)}/removeMany`, payload: idsToRemove })
|
55
|
-
})
|
56
|
-
|
57
|
-
const idsToRemove = _.difference(previousPayload.map(i => i.id), payload.map(i => i.id))
|
58
|
-
|
59
|
-
dispatch({ type: `${pluralize(model)}/upsertMany`, payload: camelizeKeys(payload) })
|
60
|
-
dispatch({ type: `${pluralize(model)}/removeMany`, payload: idsToRemove })
|
61
|
-
previousPayload = payload
|
62
|
-
}
|
63
|
-
|
64
|
-
function processQueue() {
|
65
|
-
console.log({ idx, patchQueue })
|
32
|
+
function processQueue(model) {
|
33
|
+
console.debug("processQueue", model, idx[model], patchQueue[model])
|
66
34
|
lastCheckAt = new Date()
|
67
|
-
if (patchQueue[idx]) {
|
68
|
-
|
69
|
-
|
70
|
-
|
35
|
+
if (patchQueue[model][idx[model]]) {
|
36
|
+
if (!serverActionQueue.fullySynced()) {
|
37
|
+
console.debug(serverActionQueue.getData())
|
38
|
+
setTimeout(() => processQueue(model), 100)
|
39
|
+
return
|
40
|
+
}
|
41
|
+
|
42
|
+
const { payload, destroy, id, type } = patchQueue[model][idx[model]]
|
43
|
+
|
44
|
+
if (type === 'payload') {
|
45
|
+
dispatch({ type: `${pluralize(model)}/upsertMany`, payload })
|
46
|
+
const ids = payload.map(instance => instance.id)
|
47
|
+
dispatch({ type: `jasonModels/setSubscriptionIds`, payload: { model, subscriptionId, ids }})
|
48
|
+
} else if (destroy) {
|
49
|
+
// Middleware will determine if this model should be removed if it isn't in any other subscriptions
|
50
|
+
dispatch({ type: `jasonModels/removeSubscriptionId`, payload: { model, subscriptionId, id }})
|
51
|
+
} else {
|
52
|
+
dispatch({ type: `${pluralize(model)}/upsert`, payload })
|
53
|
+
dispatch({ type: `jasonModels/addSubscriptionId`, payload: { model, subscriptionId, id }})
|
71
54
|
}
|
72
|
-
|
73
|
-
idx
|
55
|
+
|
56
|
+
delete patchQueue[model][idx[model]]
|
57
|
+
idx[model]++
|
74
58
|
updateDeadline = null
|
75
|
-
processQueue()
|
59
|
+
processQueue(model)
|
76
60
|
// If there are updates in the queue that are ahead of the index, some have arrived out of order
|
77
61
|
// Set a deadline for new updates before it declares the update missing and refetches.
|
78
|
-
} else if (_.keys(patchQueue).length > 0 && !updateDeadline) {
|
62
|
+
} else if (_.keys(patchQueue[model]).length > 0 && !updateDeadline) {
|
79
63
|
var t = new Date()
|
80
64
|
t.setSeconds(t.getSeconds() + 3)
|
81
65
|
updateDeadline = t
|
82
|
-
setTimeout(processQueue, 3100)
|
66
|
+
setTimeout(() => processQueue(model), 3100)
|
83
67
|
// If more than 10 updates in queue, or deadline has passed, restart
|
84
|
-
} else if (_.keys(patchQueue).length > 10 || (updateDeadline && diffSeconds(updateDeadline, new Date()) < 0)) {
|
68
|
+
} else if (_.keys(patchQueue[model]).length > 10 || (updateDeadline && diffSeconds(updateDeadline, new Date()) < 0)) {
|
85
69
|
tGetPayload()
|
86
70
|
updateDeadline = null
|
87
71
|
}
|
88
72
|
}
|
89
73
|
|
90
74
|
function handlePayload(data) {
|
91
|
-
const {
|
92
|
-
|
75
|
+
const { idx: newIdx, model: snake_model, type } = data
|
76
|
+
const model = _.camelCase(snake_model)
|
93
77
|
|
94
|
-
|
95
|
-
|
78
|
+
idx[model] = idx[model] || 0
|
79
|
+
patchQueue[model] = patchQueue[model] || {}
|
96
80
|
|
97
|
-
|
98
|
-
|
99
|
-
idx = newIdx + 1
|
81
|
+
if (type === 'payload') {
|
82
|
+
idx[model] = newIdx
|
100
83
|
// Clear any old changes left in the queue
|
101
|
-
patchQueue= _.pick(patchQueue, _.keys(patchQueue).filter(k => k > newIdx + 1))
|
102
|
-
return
|
84
|
+
patchQueue[model] = _.pick(patchQueue[model], _.keys(patchQueue[model]).filter(k => k > newIdx + 1))
|
103
85
|
}
|
104
86
|
|
105
|
-
patchQueue[newIdx] =
|
106
|
-
|
107
|
-
processQueue()
|
87
|
+
patchQueue[model][newIdx] = camelizeKeys({ ...data, model })
|
88
|
+
console.debug("Added to queue", model, idx[model], camelizeKeys({ ...data, model }), serverActionQueue.getData())
|
89
|
+
processQueue(model)
|
108
90
|
|
109
91
|
if (diffSeconds((new Date()), lastCheckAt) >= 3) {
|
110
92
|
lastCheckAt = new Date()
|
111
|
-
console.
|
93
|
+
console.debug('Interval lost. Pulling from server')
|
112
94
|
tGetPayload()
|
113
95
|
}
|
114
96
|
}
|
115
97
|
|
116
|
-
|
98
|
+
tGetPayload()
|
99
|
+
|
100
|
+
// Clean up after ourselves
|
101
|
+
function tearDown() {
|
102
|
+
dispatch({ type: `jasonModels/removeSubscription`, payload: { subscriptionId }})
|
103
|
+
}
|
104
|
+
|
105
|
+
return { handlePayload, tearDown }
|
117
106
|
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import createServerActionQueue from './createServerActionQueue'
|
2
|
+
|
3
|
+
test('Adding items', () => {
|
4
|
+
const serverActionQueue = createServerActionQueue()
|
5
|
+
serverActionQueue.addItem({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } })
|
6
|
+
const item = serverActionQueue.getItem()
|
7
|
+
expect(item).toStrictEqual({ type: 'entity/add', payload: { id: 'abc', attribute: 1 } })
|
8
|
+
})
|
9
|
+
|
10
|
+
test('Deduping of items that will overwrite each other', () => {
|
11
|
+
const serverActionQueue = createServerActionQueue()
|
12
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
13
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2 } })
|
14
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } })
|
15
|
+
|
16
|
+
const item = serverActionQueue.getItem()
|
17
|
+
|
18
|
+
expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 3 } })
|
19
|
+
})
|
20
|
+
|
21
|
+
test('Deduping of items with a superset', () => {
|
22
|
+
const serverActionQueue = createServerActionQueue()
|
23
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
24
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
|
25
|
+
|
26
|
+
const item = serverActionQueue.getItem()
|
27
|
+
|
28
|
+
expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 2, attribute2: 'test' } })
|
29
|
+
})
|
30
|
+
|
31
|
+
test("doesn't dedupe items with some attributes missing", () => {
|
32
|
+
const serverActionQueue = createServerActionQueue()
|
33
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
34
|
+
serverActionQueue.addItem({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } })
|
35
|
+
|
36
|
+
const item = serverActionQueue.getItem()
|
37
|
+
serverActionQueue.itemProcessed()
|
38
|
+
const item2 = serverActionQueue.getItem()
|
39
|
+
|
40
|
+
expect(item).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute: 1 } })
|
41
|
+
expect(item2).toStrictEqual({ type: 'entity/upsert', payload: { id: 'abc', attribute2: 'test' } })
|
42
|
+
})
|
@@ -0,0 +1,47 @@
|
|
1
|
+
// A FIFO queue with deduping of actions whose effect will be cancelled by later actions
|
2
|
+
|
3
|
+
import _ from 'lodash'
|
4
|
+
|
5
|
+
export default function createServerActionQueue() {
|
6
|
+
const queue: any[] = []
|
7
|
+
let inFlight = false
|
8
|
+
|
9
|
+
function addItem(item) {
|
10
|
+
// Check if there are any items ahead in the queue that this item would effectively overwrite.
|
11
|
+
// In that case we can remove them
|
12
|
+
// If this is an upsert && item ID is the same && current item attributes are a superset of the earlier item attributes
|
13
|
+
const { type, payload } = item
|
14
|
+
if (type.split('/')[1] !== 'upsert') {
|
15
|
+
queue.push(item)
|
16
|
+
return
|
17
|
+
}
|
18
|
+
|
19
|
+
_.remove(queue, item => {
|
20
|
+
const { type: itemType, payload: itemPayload } = item
|
21
|
+
if (type !== itemType) return false
|
22
|
+
if (itemPayload.id !== payload.id) return false
|
23
|
+
|
24
|
+
// Check that all keys of itemPayload are in payload.
|
25
|
+
return _.difference(_.keys(itemPayload),_.keys(payload)).length === 0
|
26
|
+
})
|
27
|
+
|
28
|
+
queue.push(item)
|
29
|
+
}
|
30
|
+
|
31
|
+
return {
|
32
|
+
addItem,
|
33
|
+
getItem: () => {
|
34
|
+
if (inFlight) return false
|
35
|
+
|
36
|
+
const item = queue.shift()
|
37
|
+
if (item) {
|
38
|
+
inFlight = true
|
39
|
+
return item
|
40
|
+
}
|
41
|
+
return false
|
42
|
+
},
|
43
|
+
itemProcessed: () => inFlight = false,
|
44
|
+
fullySynced: () => queue.length === 0 && !inFlight,
|
45
|
+
getData: () => ({ queue, inFlight })
|
46
|
+
}
|
47
|
+
}
|