notify
This commit is contained in:
1 parent
b86a4216cf
commit
b91f877045
11 files changed
+118
-130
No files matched your search
@@ -1,10 +1,7 @@
|
|||||||
# TODO
|
# TODO
|
||||||
push offers
|
push offers
|
||||||
|
|
||||||
listen / notify
|
add last synced time to want
|
||||||
on offers
|
|
||||||
notify admins
|
|
||||||
|
|
||||||
|
|
||||||
contact options on landing page
|
contact options on landing page
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ export default function createDiscogs(log: Log, pg: Pg) {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const additions = results.filter(r => r.status === 'fulfilled').map(r => r.value);
|
const additions = results.filter(r => r.status === 'fulfilled').map(r => r.value);
|
||||||
|
|
||||||
log.info('want list additions', additions);
|
log.info('want list additions', { additions: additions.length });
|
||||||
|
|
||||||
return additions;
|
return additions;
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-5
@@ -15,7 +15,8 @@ export enum Events {
|
|||||||
ListingsUpdated = 'ListingsUpdated',
|
ListingsUpdated = 'ListingsUpdated',
|
||||||
Connection = 'Connection',
|
Connection = 'Connection',
|
||||||
|
|
||||||
OffersUpdated = 'OffersUpdated'
|
OfferUpdated = 'OfferUpdated',
|
||||||
|
WantUpdated = 'WantUpdated',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
||||||
@@ -43,7 +44,7 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
|||||||
async function updateWantListings(wants: Want[]) {
|
async function updateWantListings(wants: Want[]) {
|
||||||
for (const want of wants) {
|
for (const want of wants) {
|
||||||
const listings = await populateListings(want);
|
const listings = await populateListings(want);
|
||||||
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings added');
|
log.info({ listings: listings.length, ...wantDetails(want) }, 'listings found');
|
||||||
events.emit(Events.ListingsUpdated);
|
events.emit(Events.ListingsUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,11 +75,18 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
|
|||||||
connections.set(id, rpc);
|
connections.set(id, rpc);
|
||||||
})
|
})
|
||||||
|
|
||||||
events.on(Events.OffersUpdated, async function onOffersUpdated(offer: Offer) {
|
events.on(Events.OfferUpdated, async function onOfferUpdated(offer: Offer) {
|
||||||
const rpc = connections.get(offer.userId);
|
const rpc = connections.get(offer.userId);
|
||||||
if (!rpc) return null;
|
if (!rpc) return null;
|
||||||
log.debug({ event: Events.OffersUpdated, rpc, offer });
|
log.debug({ event: Events.OfferUpdated, rpc, offer });
|
||||||
return rpc.sendUserOffers();
|
return rpc.pushUserOffers();
|
||||||
|
});
|
||||||
|
|
||||||
|
events.on(Events.WantUpdated, async function onWantUpdated(want: Want) {
|
||||||
|
const rpc = connections.get(want.userId);
|
||||||
|
if (!rpc) return null;
|
||||||
|
log.debug({ event: Events.WantUpdated, rpc, want });
|
||||||
|
return rpc.pushUserWants();
|
||||||
});
|
});
|
||||||
|
|
||||||
return events;
|
return events;
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import createSubscriber from 'pg-listen'
|
|
||||||
import type Log from 'bunyan';
|
|
||||||
|
|
||||||
import { Events } from './events';
|
|
||||||
|
|
||||||
export default function createNotify(log: Log, events) {
|
|
||||||
const CHANNEL = 'events';
|
|
||||||
const subscriber = createSubscriber({ connectionString: process.env.DATABASE_URL });
|
|
||||||
|
|
||||||
subscriber.notifications.on(CHANNEL, ({ table, action, record }) => {
|
|
||||||
if (table === 'offers') return events.emit(Events.OffersUpdated, record);
|
|
||||||
})
|
|
||||||
|
|
||||||
async function connect() {
|
|
||||||
await subscriber.connect();
|
|
||||||
return subscriber.listenTo(CHANNEL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { connect };
|
|
||||||
}
|
|
||||||
+8
-4
@@ -18,22 +18,26 @@ export default function createNotify(log: Log, pg: Pg, events) {
|
|||||||
|
|
||||||
async function handleOffers(payload) {
|
async function handleOffers(payload) {
|
||||||
const { action, id } = payload;
|
const { action, id } = payload;
|
||||||
log.info({ channel: OFFERS_CHANNEL, action, id });
|
log.debug({ channel: OFFERS_CHANNEL, action, id });
|
||||||
|
if (action === 'DELETE') return null;
|
||||||
|
|
||||||
|
const offer = await pg.offerFindId(id);
|
||||||
|
// events.emit(Events.OfferUpdated, offer);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUsers(payload) {
|
async function handleUsers(payload) {
|
||||||
const { action, id } = payload;
|
const { action, id } = payload;
|
||||||
log.info({ channel: USERS_CHANNEL, action, id });
|
log.debug({ channel: USERS_CHANNEL, action, id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleListings(payload) {
|
async function handleListings(payload) {
|
||||||
const { action, id } = payload;
|
const { action, id } = payload;
|
||||||
log.info({ channel: LISTINGS_CHANNEL, action, id });
|
log.debug({ channel: LISTINGS_CHANNEL, action, id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleWants(payload) {
|
async function handleWants(payload) {
|
||||||
const { action, id } = payload;
|
const { action, id } = payload;
|
||||||
log.info({ channel: WANTS_CHANNEL, action, id });
|
log.debug({ channel: WANTS_CHANNEL, action, id });
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriber.notifications.on(OFFERS_CHANNEL, handleOffers);
|
subscriber.notifications.on(OFFERS_CHANNEL, handleOffers);
|
||||||
|
|||||||
+80
-80
@@ -12,6 +12,76 @@ import { Status } from './offers';
|
|||||||
import { isAdmin } from './utils';
|
import { isAdmin } from './utils';
|
||||||
import { Events } from './events';
|
import { Events } from './events';
|
||||||
|
|
||||||
|
export type Rpc = ReturnType<typeof createRpc>;
|
||||||
|
|
||||||
|
export enum RpcMessageType {
|
||||||
|
Ping = 'Ping',
|
||||||
|
Pong = 'Pong',
|
||||||
|
|
||||||
|
// points out user is anon
|
||||||
|
WhoDis = 'WhoDis',
|
||||||
|
|
||||||
|
Bundles = 'Bundles',
|
||||||
|
Offers = 'Offers',
|
||||||
|
List = 'List',
|
||||||
|
Wants = 'Wants',
|
||||||
|
User = 'User',
|
||||||
|
|
||||||
|
Error = 'Error',
|
||||||
|
|
||||||
|
OfferSamples = 'OfferSamples',
|
||||||
|
OfferStatusSet = 'OfferStatusSet',
|
||||||
|
|
||||||
|
AcpOfferStatusSet = 'AcpOfferStatusSet',
|
||||||
|
AcpOffers = 'AcpOffers',
|
||||||
|
AcpListingsSync = 'AcpListingsSync',
|
||||||
|
|
||||||
|
WantRm = 'WantRm',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrorMessage = [RpcMessageType.Error, string];
|
||||||
|
type PongMessage = [RpcMessageType.Pong, []];
|
||||||
|
type PingMessage = [RpcMessageType.Ping, []];
|
||||||
|
type WhoDisMessage = [RpcMessageType.WhoDis, []];
|
||||||
|
type UserMessage = [RpcMessageType.User, User];
|
||||||
|
type WantsMessage = [RpcMessageType.Wants, Want[]];
|
||||||
|
type OffersMessage = [RpcMessageType.Offers, Offer[]];
|
||||||
|
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
|
||||||
|
type OfferSamplesMessage = [RpcMessageType.OfferSamples, Offer[]];
|
||||||
|
|
||||||
|
type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]];
|
||||||
|
type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]];
|
||||||
|
type AcpListingsSyncMessage = [RpcMessageType.AcpListingsSync, []];
|
||||||
|
|
||||||
|
export type RpcMessage =
|
||||||
|
PingMessage |
|
||||||
|
PongMessage |
|
||||||
|
WhoDisMessage |
|
||||||
|
|
||||||
|
UserMessage |
|
||||||
|
|
||||||
|
WantsMessage |
|
||||||
|
|
||||||
|
OffersMessage |
|
||||||
|
OfferSamplesMessage |
|
||||||
|
OfferStatusSetMessage |
|
||||||
|
|
||||||
|
AcpOfferStatusSetMessage |
|
||||||
|
AcpOffersMessage |
|
||||||
|
AcpListingsSyncMessage |
|
||||||
|
|
||||||
|
ErrorMessage;
|
||||||
|
|
||||||
|
|
||||||
|
type OfferStatusSetParams = {
|
||||||
|
id: UUID,
|
||||||
|
status: Status,
|
||||||
|
};
|
||||||
|
|
||||||
|
type WantRmParams = {
|
||||||
|
id: UUID,
|
||||||
|
};
|
||||||
|
|
||||||
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
|
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
|
||||||
if (RpcMessageType[msgType]) {
|
if (RpcMessageType[msgType]) {
|
||||||
return true;
|
return true;
|
||||||
@@ -134,11 +204,8 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
|
|||||||
const userMsg: UserMessage = [RpcMessageType.User, user];
|
const userMsg: UserMessage = [RpcMessageType.User, user];
|
||||||
send(userMsg);
|
send(userMsg);
|
||||||
|
|
||||||
await sendUserOffers();
|
await pushUserOffers();
|
||||||
|
await pushUserWants();
|
||||||
const wants = await pg.userWants(user);
|
|
||||||
const wantsMsg: WantsMessage = [RpcMessageType.Wants, wants];
|
|
||||||
send(wantsMsg);
|
|
||||||
|
|
||||||
if (isAdmin(user)) return adminConnection();
|
if (isAdmin(user)) return adminConnection();
|
||||||
}
|
}
|
||||||
@@ -151,10 +218,12 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendUserOffers() {
|
async function pushUserOffers() {
|
||||||
const userOffers = await offers.userOffers(user);
|
send(await userOffers());
|
||||||
const offersMsg: OffersMessage = [RpcMessageType.Offers, userOffers];
|
}
|
||||||
send(offersMsg);
|
|
||||||
|
async function pushUserWants() {
|
||||||
|
send(await userWants());
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -162,76 +231,7 @@ export default function createRpc(log: Log, pg: Pg, ws, discogs: Discogs, offers
|
|||||||
send,
|
send,
|
||||||
onConnection,
|
onConnection,
|
||||||
|
|
||||||
sendUserOffers,
|
pushUserOffers,
|
||||||
|
pushUserWants,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Rpc = ReturnType<typeof createRpc>;
|
|
||||||
|
|
||||||
export enum RpcMessageType {
|
|
||||||
Ping = 'Ping',
|
|
||||||
Pong = 'Pong',
|
|
||||||
|
|
||||||
// points out user is anon
|
|
||||||
WhoDis = 'WhoDis',
|
|
||||||
|
|
||||||
Bundles = 'Bundles',
|
|
||||||
Offers = 'Offers',
|
|
||||||
List = 'List',
|
|
||||||
Wants = 'Wants',
|
|
||||||
User = 'User',
|
|
||||||
|
|
||||||
Error = 'Error',
|
|
||||||
|
|
||||||
OfferSamples = 'OfferSamples',
|
|
||||||
OfferStatusSet = 'OfferStatusSet',
|
|
||||||
|
|
||||||
AcpOfferStatusSet = 'AcpOfferStatusSet',
|
|
||||||
AcpOffers = 'AcpOffers',
|
|
||||||
AcpListingsSync = 'AcpListingsSync',
|
|
||||||
|
|
||||||
WantRm = 'WantRm',
|
|
||||||
}
|
|
||||||
|
|
||||||
type ErrorMessage = [RpcMessageType.Error, string];
|
|
||||||
type PongMessage = [RpcMessageType.Pong, []];
|
|
||||||
type PingMessage = [RpcMessageType.Ping, []];
|
|
||||||
type WhoDisMessage = [RpcMessageType.WhoDis, []];
|
|
||||||
type UserMessage = [RpcMessageType.User, User];
|
|
||||||
type WantsMessage = [RpcMessageType.Wants, Want[]];
|
|
||||||
type OffersMessage = [RpcMessageType.Offers, Offer[]];
|
|
||||||
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
|
|
||||||
type OfferSamplesMessage = [RpcMessageType.OfferSamples, Offer[]];
|
|
||||||
|
|
||||||
type AcpOfferStatusSetMessage = [RpcMessageType.AcpOfferStatusSet, Offer[]];
|
|
||||||
type AcpOffersMessage = [RpcMessageType.AcpOffers, Offer[]];
|
|
||||||
type AcpListingsSyncMessage = [RpcMessageType.AcpListingsSync, []];
|
|
||||||
|
|
||||||
export type RpcMessage =
|
|
||||||
PingMessage |
|
|
||||||
PongMessage |
|
|
||||||
WhoDisMessage |
|
|
||||||
|
|
||||||
UserMessage |
|
|
||||||
|
|
||||||
WantsMessage |
|
|
||||||
|
|
||||||
OffersMessage |
|
|
||||||
OfferSamplesMessage |
|
|
||||||
OfferStatusSetMessage |
|
|
||||||
|
|
||||||
AcpOfferStatusSetMessage |
|
|
||||||
AcpOffersMessage |
|
|
||||||
AcpListingsSyncMessage |
|
|
||||||
|
|
||||||
ErrorMessage;
|
|
||||||
|
|
||||||
|
|
||||||
type OfferStatusSetParams = {
|
|
||||||
id: UUID,
|
|
||||||
status: Status,
|
|
||||||
};
|
|
||||||
|
|
||||||
type WantRmParams = {
|
|
||||||
id: UUID,
|
|
||||||
};
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { useState, useCallback } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { ExternalLink } from 'react-feather';
|
import { ExternalLink } from 'react-feather';
|
||||||
|
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { State } from './../store'
|
import { State } from './../store'
|
||||||
|
|
||||||
import http from './../http';
|
|
||||||
|
|
||||||
function Want(want) {
|
function Want(want) {
|
||||||
const r = want.data;
|
const r = want.data;
|
||||||
|
|
||||||
@@ -14,10 +12,13 @@ function Want(want) {
|
|||||||
const [confirm, setConfirm] = useState(false);
|
const [confirm, setConfirm] = useState(false);
|
||||||
|
|
||||||
function onClick() {
|
function onClick() {
|
||||||
if (confirm) return ws.sendWantRm(want.id);
|
if (confirm) {
|
||||||
|
setConfirm(false);
|
||||||
|
return ws.sendWantRm(want.id);
|
||||||
|
}
|
||||||
setTimeout(() => setConfirm(false), 2500);
|
setTimeout(() => setConfirm(false), 2500);
|
||||||
return setConfirm(true);
|
return setConfirm(true);
|
||||||
};
|
}
|
||||||
|
|
||||||
const text = confirm
|
const text = confirm
|
||||||
? 'confirm'
|
? 'confirm'
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import Want from './../components/want';
|
|||||||
|
|
||||||
const List = () => {
|
const List = () => {
|
||||||
const wants = useSelector((state: State) => state.wants);
|
const wants = useSelector((state: State) => state.wants);
|
||||||
const user = useSelector((state: State) => state.user);
|
|
||||||
|
|
||||||
if (!wants) {
|
if (!wants) {
|
||||||
return <div> loading... </div>;
|
return <div> loading... </div>;
|
||||||
|
|||||||
@@ -378,19 +378,16 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
|
||||||
a {
|
|
||||||
font-size: var(--small-font-size);
|
|
||||||
margin-right: var(--spacing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
header {
|
header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr min-content;
|
grid-template-columns: 1fr 1fr min-content;
|
||||||
padding-bottom: var(--spacing);
|
padding-bottom: var(--spacing);
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-size: var(--small-font-size);
|
||||||
|
margin-right: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import toast from 'izitoast';
|
|||||||
export function info(title, message) {
|
export function info(title, message) {
|
||||||
toast.info({
|
toast.info({
|
||||||
title,
|
title,
|
||||||
message,
|
message: message || '',
|
||||||
position: 'bottomCenter',
|
position: 'bottomCenter',
|
||||||
color: '#1f2128',
|
color: '#1f2128',
|
||||||
titleColor: '#F4F1D0',
|
titleColor: '#F4F1D0',
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,6 @@
|
|||||||
import { wantsState, wsState, userState, offersState, acpOffersState } from './store'
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
|
import { wantsState, userState, offersState, acpOffersState } from './store'
|
||||||
import * as toast from './toast';
|
import * as toast from './toast';
|
||||||
|
|
||||||
const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`;
|
const SOCKET_URL = `${window.location.protocol === 'https:' ? 'wss://' : 'ws://'}${window.location.host}/api/ws`;
|
||||||
@@ -116,6 +117,7 @@ export default function createSocket(dispatch) {
|
|||||||
// Connection opened
|
// Connection opened
|
||||||
function onOpen() {
|
function onOpen() {
|
||||||
console.log('websocket connected', SOCKET_URL)
|
console.log('websocket connected', SOCKET_URL)
|
||||||
|
toast.info('connected', 'connected to server');
|
||||||
sendPing();
|
sendPing();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user