rpc
This commit is contained in:
1 parent
111f690482
commit
2c66c47163
14 files changed
+168
-58
No files matched your search
@@ -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
@@ -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
@@ -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 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||||
|
};
|
||||||
Generated
+1
-20
@@ -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
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user