This commit is contained in:
ntr committed 2022-09-11 21:16:33 +10:00
1 parent 111f690482
commit 2c66c47163
14 files changed
+168 -58

No files matched your search

+2 -5
View File
@@ -1,9 +1,6 @@
# TODO # TODO
push offers push offers
redux that bitch cover off admin routes and commands
do we keep historical offers / wants?
seller
eg min 6 items
shipping is 101aud
+1 -1
View File
@@ -28,7 +28,7 @@ async function start() {
events, events,
} = createServices(log); } = createServices(log);
events.emit(Events.StartupSync); // events.emit(Events.StartupSync);
// events.emit(Events.ListingsUpdated); // events.emit(Events.ListingsUpdated);
api.listen(); api.listen();
+13 -7
View File
@@ -16,6 +16,7 @@ import type { Listing, User, Want, Prisma } from '@prisma/client';
import { Pg } from './pg'; import { Pg } from './pg';
import routerAuth from './routers/router.auth'; import routerAuth from './routers/router.auth';
import { Events } from './events'; import { Events } from './events';
import createRpc, { RpcMessageType } from './rpc';
export default function createApi(log: Log, pg: Pg, discogs, offers, events) { export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
const PORT = 41337; const PORT = 41337;
@@ -153,22 +154,27 @@ export default function createApi(log: Log, pg: Pg, discogs, offers, events) {
app.ws.use(passport.session()); app.ws.use(passport.session());
app.ws.use(async (ctx, next) => { app.ws.use(async (ctx, next) => {
log.info('new ws connection', { user: ctx.state.user }); log.info('new ws connection', { user: ctx.state.user });
ctx.websocket.on('message', async function incoming(message) {
log.info(message.toString()); // log.child...
const ws = ctx.websocket;
const rpc = createRpc(log, pg, ctx.state.user, ws, discogs, offers);
if (ctx.state.user) events.emit(Events.Connection, { ws, id: ctx.state.user.id });
ws.on('message', async function incoming(message) {
try { try {
const parsed = JSON.parse(message.toString()); const parsed = JSON.parse(message.toString());
log.info(parsed); const response = await rpc.receive(parsed);
// const response = await rpc.receive(parsed); rpc.send(response);
// rpc.send(ws, response);
} catch (e) { } catch (e) {
log.warn(e); log.warn(e);
log.warn(message.toString()); log.warn(message.toString());
// rpc.send(ws, [RpcMessageType.Error, e.message || e.msg]) rpc.send([RpcMessageType.Error, e.message || e.msg]);
} }
return true; return true;
}); });
ctx.websocket.on('close', function onClose() { ws.on('close', function onClose() {
log.info('ws disconnected', { user: ctx.state.user }); log.info('ws disconnected', { user: ctx.state.user });
}); });
}); });
+6
View File
@@ -12,10 +12,12 @@ export enum Events {
WantAdded = 'WantAdded', WantAdded = 'WantAdded',
StartupSync = 'StartupSync', StartupSync = 'StartupSync',
ListingsUpdated = 'ListingsUpdated', ListingsUpdated = 'ListingsUpdated',
Connection = 'Connection',
} }
export default function createEvents(log: Log, pg: Pg, discogs, offers) { export default function createEvents(log: Log, pg: Pg, discogs, offers) {
const events = new EventEmitter({ captureRejections: true }); const events = new EventEmitter({ captureRejections: true });
const connections = new Map();
async function populateListings(want: Want) { async function populateListings(want: Want) {
try { try {
@@ -82,5 +84,9 @@ export default function createEvents(log: Log, pg: Pg, discogs, offers) {
log.info('offers up to date'); log.info('offers up to date');
}); });
events.on(Events.Connection, async function onConnection({ ws, id }) {
connections.set(id, ws);
})
return events; return events;
} }
+14 -6
View File
@@ -1,5 +1,6 @@
import type Log from 'bunyan'; import type Log from 'bunyan';
import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client'; import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client';
import assert from 'assert/strict';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import { Context } from 'koa'; import { Context } from 'koa';
@@ -8,11 +9,11 @@ import { logAxiosError } from './utils';
import { Pg } from './pg'; import { Pg } from './pg';
export enum Status { export enum Status {
Pending = 'pending', Pending = 'Pending',
Accepted = 'accepted', Accepted = 'Accepted',
Declined = 'declined', Declined = 'Declined',
Ordered = 'ordered', Ordered = 'Ordered',
Complete = 'complete', Complete = 'Complete',
} }
export default function createOffers(log: Log, pg: Pg) { export default function createOffers(log: Log, pg: Pg) {
@@ -60,7 +61,13 @@ export default function createOffers(log: Log, pg: Pg) {
// check the user actually has this offer // check the user actually has this offer
const offer = await pg.userOfferFind(ctx.state.user, id); const offer = await pg.userOfferFind(ctx.state.user, id);
return pg.offerSetStatus(offer.id, Status.Accepted); return pg.offerStatusSet(offer.id, Status.Accepted);
}
async function statusSet(user: User, id: UUID, status: Status) {
// check the user actually has this offer
const offer = await pg.userOfferFind(user, id);
return pg.offerStatusSet(offer.id, status);
} }
async function bundles() { async function bundles() {
@@ -108,5 +115,6 @@ export default function createOffers(log: Log, pg: Pg) {
generate, generate,
accept, accept,
bundles, bundles,
statusSet,
}; };
} }
+3 -3
View File
@@ -31,7 +31,7 @@ export interface Pg {
bundleCandidates: () => Promise<Listing[][]>, bundleCandidates: () => Promise<Listing[][]>,
offersCreate: (offers: Offer[]) => Promise<Offer[]>, offersCreate: (offers: Offer[]) => Promise<Offer[]>,
offerSetStatus: (id: UUID, status: Status) => Promise<Offer>, offerStatusSet: (id: UUID, status: Status) => Promise<Offer>,
offersAdmin: () => Promise<Offer[]>, offersAdmin: () => Promise<Offer[]>,
} }
@@ -258,7 +258,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
); );
} }
async function offerSetStatus(id: UUID, status: Status) { async function offerStatusSet(id: UUID, status: Status) {
return prisma.offer.update({ return prisma.offer.update({
where: { id: id }, where: { id: id },
data: { status }, data: { status },
@@ -294,7 +294,7 @@ export default function createPg(log: Log, prisma: PrismaClient): Pg {
bundleCandidates, bundleCandidates,
offersCreate, offersCreate,
offerSetStatus, offerStatusSet,
offersAdmin, offersAdmin,
}; };
} }
+100
View File
@@ -0,0 +1,100 @@
import { EventEmitter } from 'node:events';
import { setTimeout } from 'timers/promises';
import assert from 'assert/strict';
import type Log from 'bunyan';
import type { Want, Offer, User } from '@prisma/client';
import { Pg } from './pg';
import { Status } from './offers';
function checkMsgType(msgType: RpcMessageType): msgType is RpcMessageType {
if (RpcMessageType[msgType]) {
return true;
}
return false;
}
// rpc is created per connection from the api
export default function createRpc(log: Log, pg: Pg, user: User, ws, discogs, offers) {
async function ping(): Promise<PongMessage> {
return [RpcMessageType.Pong, []];
}
async function offerStatusSet(params: OfferStatusSetParams): Promise<OffersMessage> {
log.info(params);
const { id, status } = params;
assert(id, 'no id supplied');
assert(Status[status], 'unknown status');
const offer = await offers.statusSet(user, id, status);
return [RpcMessageType.Offers, []]
}
async function receive(message: any): Promise<RpcMessage> {
const [msgType, params] = message;
if (msgType !== RpcMessageType.Ping) log.info({ msgType, params }, 'rpc message');
// // this sucks but can't figure out how to convince typescript to agree yet
if (checkMsgType(msgType)) {
switch (msgType) {
case RpcMessageType.Ping:
return ping();
case RpcMessageType.OfferStatusSet:
return offerStatusSet(params);
default:
throw new Error(`Message type ${msgType} is known but unhandled`);
}
} else {
throw new Error(`Unknown message type ${msgType}`);
}
throw new Error(`Invalid parameters for message type ${msgType}: ${JSON.stringify(message)}`);
}
function send(msg: RpcMessage) {
ws.send(JSON.stringify(msg, (key, value) =>
typeof value === 'bigint'
? value.toString()
: value // return everything else unchanged
))
}
return {
receive,
send,
}
}
export enum RpcMessageType {
Ping = 'Ping',
Pong = 'Pong',
Bundles = 'Bundles',
Offers = 'Offers',
List = 'List',
Error = 'Error',
OfferStatusSet = 'OfferStatusSet',
}
type ErrorMessage = [RpcMessageType.Error, string];
type PongMessage = [RpcMessageType.Pong, []];
type PingMessage = [RpcMessageType.Ping, []];
type OffersMessage = [RpcMessageType.Offers, Offer[]];
type OfferStatusSetMessage = [RpcMessageType.OfferStatusSet, Offer[]];
export type RpcMessage =
PingMessage |
PongMessage |
OffersMessage |
OfferStatusSetMessage |
ErrorMessage;
type OfferStatusSetParams = {
id: UUID,
status: Status,
};
+1 -20
View File
@@ -16,8 +16,7 @@
"preact-router": "^3.2.1", "preact-router": "^3.2.1",
"react-feather": "^2.0.10", "react-feather": "^2.0.10",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"redux": "^4.2.0", "redux": "^4.2.0"
"unistore": "^3.5.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-transform-react-jsx-source": "^7.18.6", "@babel/plugin-transform-react-jsx-source": "^7.18.6",
@@ -19059,19 +19058,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/unistore": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/unistore/-/unistore-3.5.2.tgz",
"integrity": "sha512-2Aa4eX0Ua1umyiI3Eai6Li+wXYOHgaDBGOPB3Hvw7PAVuD30TAyh5kS4yNKb2fLDbQgizvPhKQRcYnOdfsm4VQ==",
"peerDependenciesMeta": {
"preact": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/universalify": { "node_modules/universalify": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
@@ -35978,11 +35964,6 @@
"crypto-random-string": "^2.0.0" "crypto-random-string": "^2.0.0"
} }
}, },
"unistore": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/unistore/-/unistore-3.5.2.tgz",
"integrity": "sha512-2Aa4eX0Ua1umyiI3Eai6Li+wXYOHgaDBGOPB3Hvw7PAVuD30TAyh5kS4yNKb2fLDbQgizvPhKQRcYnOdfsm4VQ=="
},
"universalify": { "universalify": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
+1 -1
View File
@@ -20,6 +20,7 @@ import store, { userState, wsState } from './store'
const ws = createWs(store); const ws = createWs(store);
ws.connect(); ws.connect();
// dodgy, doesn't cause the header to rerender
async function init() { async function init() {
const [error, user] = await http.getLogin(); const [error, user] = await http.getLogin();
if (error) return route('/login'); if (error) return route('/login');
@@ -27,7 +28,6 @@ async function init() {
store.dispatch(userState.actions.set(user)); store.dispatch(userState.actions.set(user));
store.dispatch(wsState.actions.set(ws)); store.dispatch(wsState.actions.set(ws));
} }
init(); init();
const App = () => { const App = () => {
+1 -1
View File
@@ -7,7 +7,7 @@ import http from '../http';
export default function Offer(offer) { export default function Offer(offer) {
const { want: { data: details } } = offer; const { want: { data: details } } = offer;
const [accepted, setAccepted] = useState(offer.status !== 'pending'); const [accepted, setAccepted] = useState(offer.status !== 'Pending');
async function onClick() { async function onClick() {
const [error] = await http.postOfferAccept(offer.id); const [error] = await http.postOfferAccept(offer.id);
+15 -6
View File
@@ -1,21 +1,30 @@
import { h } from 'preact'; import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks'; import { useState, useEffect } from 'preact/hooks';
import { connect } from 'unistore/preact'
import http from './../http'; import http from './../http';
import AcpOffer from './../components/acp.offer'; import AcpOffer from './../components/acp.offer';
// hmmm import { useSelector, useDispatch } from 'react-redux'
// bundle... import { State } from './../store';
import { Status } from './../../../api/src/offers'
enum Status {
Pending = 'Pending',
Accepted = 'Accepted',
Declined = 'Declined',
Ordered = 'Ordered',
Complete = 'Complete',
}
function Item({ item }) { function Item({ item }) {
const ws = useSelector((state: State) => state.ws);
console.log(item);
const statuses = Object.entries(Status).map(([label, value]) => ( const statuses = Object.entries(Status).map(([label, value]) => (
<option value={value}>{label}</option> <option selected={item.status === value} value={value}>{value}</option>
)); ));
function onChange(e) { function onChange(e) {
console.log(item.id, e.target.value); ws.sendOfferStatusSet(item.id, e.target.value);
} }
return ( return (
+4 -1
View File
@@ -31,7 +31,10 @@ const reducer = combineReducers({
ws: wsState.reducer, ws: wsState.reducer,
}); });
const store = createStore(reducer); const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store; export default store;
+3 -3
View File
@@ -227,11 +227,11 @@ article {
} }
.list { .list {
display: flex; display: grid;
flex-flow: row wrap; grid-template-columns:
1fr 1fr 1fr 1fr;
article { article {
flex: 1 1 20%;
padding: var(--base-spacing); padding: var(--base-spacing);
margin: var(--base-spacing-half); margin: var(--base-spacing-half);
} }
+4 -4
View File
@@ -24,13 +24,13 @@ export default function createSocket(store) {
// setTimeout(sendPing, 50); // setTimeout(sendPing, 50);
} }
function sendSourcesAvailable() { function sendOfferStatusSet(id, status) {
send(['SourcesAvailable', {}]); send(['OfferStatusSet', { id, status }]);
} }
let pongTimeout; let pongTimeout;
function onPong() { function onPong() {
pongTimeout = setTimeout(sendPing, 1000); pongTimeout = setTimeout(sendPing, 5000);
} }
// ------------- // -------------
@@ -108,6 +108,6 @@ export default function createSocket(store) {
return { return {
connect, connect,
sendSourcesAvailable, sendOfferStatusSet,
}; };
} }