Subscribe to notifications/remove polling

- We want to publish when a notification occurs for a specific user, not
  have the client poll the backend for ever user every minute.

- Co-authored-by: @Tirokk <wolle.huss@pjannto.com>
This commit is contained in:
mattwr18 2020-02-05 17:37:38 +01:00
parent 04f0467d2d
commit 2f43069ea0
13 changed files with 98 additions and 75 deletions

View File

@ -1,10 +1,11 @@
import createServer from './server'
import CONFIG from './config'
const { app, server, httpServer } = createServer()
const { server, httpServer } = createServer()
const url = new URL(CONFIG.GRAPHQL_URI)
httpServer.listen({ port: url.port }, () => {
/* eslint-disable-next-line no-console */
console.log(`🚀 Server ready at http://localhost:${url.port}${server.graphqlPath}`)
/* eslint-disable-next-line no-console */
console.log(`🚀 Subscriptions ready at ws://localhost:${url.port}${server.subscriptionsPath}`)
})

View File

@ -1,9 +1,10 @@
import extractMentionedUsers from './mentions/extractMentionedUsers'
import { validateNotifyUsers } from '../validation/validationMiddleware'
import { PubSub } from 'apollo-server'
const pubsub = new PubSub()
const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
import {
pubsub,
NOTIFICATION_ADDED,
transformReturnType,
} from '../../schema/resolvers/notifications'
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
const idsOfUsers = extractMentionedUsers(args.content)
@ -56,6 +57,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
WHERE user.id in $idsOfUsers
AND NOT (user)-[:BLOCKED]-(author)
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
WITH notification, post AS resource, user
`
break
}
@ -67,6 +69,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
AND NOT (user)-[:BLOCKED]-(author)
AND NOT (user)-[:BLOCKED]-(postAuthor)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
WITH notification, comment AS resource, user
`
break
}
@ -78,12 +81,16 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
WHEN notification.createdAt IS NULL
THEN notification END ).createdAt = toString(datetime())
SET notification.updatedAt = toString(datetime())
RETURN notification
RETURN notification, resource, user, labels(resource)[0] AS type
`
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async transaction => {
const notificationTransactionResponse = await transaction.run(mentionedCypher, { id, idsOfUsers, reason })
return notificationTransactionResponse.records.map(record => record.get('notification').properties)
const notificationTransactionResponse = await transaction.run(mentionedCypher, {
id,
idsOfUsers,
reason,
})
return notificationTransactionResponse.records.map(transformReturnType)
})
try {
const [notification] = await writeTxResultPromise

View File

@ -1,16 +1,14 @@
import log from './helpers/databaseLogger'
import { PubSub } from 'apollo-server'
import { withFilter } from 'graphql-subscriptions'
const pubsub = new PubSub()
const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
const resourceTypes = ['Post', 'Comment']
const transformReturnType = record => {
export const pubsub = new PubSub()
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
export const transformReturnType = record => {
return {
...record.get('notification').properties,
from: {
__typename: record.get('resource').labels.find(l => resourceTypes.includes(l)),
__typename: record.get('type'),
...record.get('resource').properties,
},
to: {
@ -22,7 +20,12 @@ const transformReturnType = record => {
export default {
Subscription: {
notificationAdded: {
subscribe: () => pubsub.asyncIterator([NOTIFICATION_ADDED]),
subscribe: withFilter(
() => pubsub.asyncIterator(NOTIFICATION_ADDED),
(payload, variables) => {
return payload.notificationAdded.to.id === variables.userId
},
),
},
},
Query: {
@ -90,7 +93,7 @@ export default {
`
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
SET notification.read = TRUE
RETURN resource, notification, user
RETURN resource, notification, user, labels(resource)[0] AS type
`,
{ resourceId: args.id, id: currentUser.id },
)

View File

@ -1,16 +1,14 @@
import uuid from 'uuid/v4'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { isEmpty } from 'lodash'
import { UserInputError } from 'apollo-server'
import { UserInputError, PubSub } from 'apollo-server'
import fileUpload from './fileUpload'
import Resolver from './helpers/Resolver'
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
import { PubSub } from 'apollo-server'
const pubsub = new PubSub()
const POST_ADDED = 'POST_ADDED'
const maintainPinnedPosts = params => {
const pinnedPostFilter = { pinned: true }
if (isEmpty(params.filter)) {

View File

@ -30,3 +30,7 @@ type Query {
type Mutation {
markAsRead(id: ID!): NOTIFIED
}
type Subscription {
notificationAdded(userId: ID!): NOTIFIED
}

View File

@ -245,3 +245,7 @@ type Query {
"""
)
}
type Subscription {
postAdded: Post
}

View File

@ -1,4 +0,0 @@
type Subscription {
postAdded: Post
notificationAdded: NOTIFIED
}

View File

@ -3,7 +3,6 @@ import http from 'http'
import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express'
import CONFIG from './config'
import middleware from './middleware'
import { getNeode, getDriver } from './db/neo4j'
@ -11,23 +10,22 @@ import decode from './jwt/decode'
import schema from './schema'
import webfinger from './activitypub/routes/webfinger'
const driver = getDriver()
const neode = getNeode()
const getContext = async (req) => {
const user = await decode(driver, req.headers.authorization)
return {
driver,
neode,
user,
req,
cypherParams: {
currentUserId: user ? user.id : null,
},
}
const getContext = async req => {
const user = await decode(driver, req.headers.authorization)
return {
driver,
neode,
user,
req,
cypherParams: {
currentUserId: user ? user.id : null,
},
}
}
export const context = async (options) => {
export const context = async options => {
const { connection, req } = options
if (connection) {
return connection.context
@ -63,9 +61,8 @@ const createServer = options => {
app.use('/.well-known/', webfinger())
app.use(express.static('public'))
server.applyMiddleware({ app, path: '/' })
const httpServer = http.createServer(app);
server.installSubscriptionHandlers(httpServer);
const httpServer = http.createServer(app)
server.installSubscriptionHandlers(httpServer)
return { server, httpServer, app }
}

View File

@ -22,13 +22,12 @@
</template>
<script>
import { mapGetters } from 'vuex'
import unionBy from 'lodash/unionBy'
import { NOTIFICATIONS_POLL_INTERVAL } from '~/constants/notifications'
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
import { notificationQuery, markAsReadMutation, notificationAdded } from '~/graphql/User'
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
import Dropdown from '~/components/Dropdown'
import NotificationList from '../NotificationList/NotificationList'
import { notificationAdded } from '~/graphql/User'
export default {
name: 'NotificationMenu',
@ -59,6 +58,9 @@ export default {
},
},
computed: {
...mapGetters({
user: 'auth/user',
}),
unreadNotificationsCount() {
const result = this.notifications.reduce((count, notification) => {
return notification.read ? count : count + 1
@ -77,17 +79,24 @@ export default {
orderBy: 'updatedAt_desc',
}
},
// pollInterval: NOTIFICATIONS_POLL_INTERVAL,
// update({ notifications }) {
// return unionBy(notifications, this.notifications, notification => notification.id).sort(
// (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
// )
// },
subscribeToMore: {
document: notificationAdded(),
variables() {
return {
userId: this.user.id,
}
},
updateQuery: (previousResult, { subscriptionData }) => {
const { data: { notificationAdded: newNotification } } = subscriptionData
return { notifications: [newNotification, ...previousResult.notifications] }
const {
data: { notificationAdded: newNotification },
} = subscriptionData
return {
notifications: unionBy(
[newNotification],
previousResult.notifications,
notification => notification.id,
).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)),
}
},
},
error(error) {

View File

@ -1 +0,0 @@
export const NOTIFICATIONS_POLL_INTERVAL = 60000

View File

@ -70,6 +70,7 @@ export const notificationQuery = i18n => {
read
reason
createdAt
updatedAt
from {
__typename
... on Post {
@ -108,6 +109,7 @@ export const markAsReadMutation = i18n => {
read
reason
createdAt
updatedAt
from {
__typename
... on Post {
@ -137,12 +139,13 @@ export const notificationAdded = () => {
${commentFragment}
${postFragment}
subscription notifications {
notificationAdded {
subscription notifications($userId: ID!) {
notificationAdded(userId: $userId) {
id
read
reason
createdAt
updatedAt
from {
__typename
... on Post {

View File

@ -78,7 +78,6 @@ import gql from 'graphql-tag'
import {
userFragment,
postFragment,
commentFragment,
postCountsFragment,
userCountsFragment,
locationAndBadgesFragment,
@ -225,30 +224,33 @@ export default {
fetchPolicy: 'cache-and-network',
subscribeToMore: {
document: gql`
${userFragment}
${userCountsFragment}
${locationAndBadgesFragment('EN')}
${postFragment}
${postCountsFragment}
${tagsCategoriesAndPinnedFragment}
${userFragment}
${userCountsFragment}
${locationAndBadgesFragment('EN')}
${postFragment}
${postCountsFragment}
${tagsCategoriesAndPinnedFragment}
subscription Post {
postAdded {
...post
...postCounts
...tagsCategoriesAndPinned
author {
...user
...userCounts
...locationAndBadges
subscription Post {
postAdded {
...post
...postCounts
...tagsCategoriesAndPinned
author {
...user
...userCounts
...locationAndBadges
}
}
}
}`,
`,
updateQuery: (previousResult, { subscriptionData }) => {
const { data: { postAdded: newPost } } = subscriptionData
const {
data: { postAdded: newPost },
} = subscriptionData
return { Post: [newPost, ...previousResult.Post] }
},
}
},
},
},
}

View File

@ -17,7 +17,7 @@ export default ({ app }) => {
credentials: true,
tokenName: 'human-connection-token',
persisting: false,
websocketsOnly: true,
websocketsOnly: false,
cache: new InMemoryCache({ fragmentMatcher }),
}
}