UNPKG

4.38 kBJavaScriptView Raw
1// import signalhub from "signalhub";
2import * as gameActions from "@cardcore/game";
3import { CLIENT_LOAD_STATE_DONE, clientPoll } from "@cardcore/client";
4import { hashState, serverFetch } from "@cardcore/util";
5import ssbKeys from "@streamplace/ssb-keys";
6import stringify from "json-stable-stringify";
7
8export const REMOTE_ACTION = Symbol("REMOTE_ACTION");
9
10export function gameMiddleware(store) {
11 const server = `${document.location.protocol}//${document.location.host}`;
12 const channelName = document.location.pathname.slice(1);
13 // const hub = signalhub("butt-card", [server]);
14 // hub.subscribe(channelName).on("data", async action => {
15 // const me = store.getState().client.keys;
16 // if (action._sender === me.id) {
17 // return;
18 // }
19 // action = {
20 // ...action,
21 // [REMOTE_ACTION]: true
22 // };
23 // store.dispatch(action);
24 // });
25
26 const promises = new WeakMap();
27
28 return next => {
29 const queue = [];
30 let running = false;
31 let sync = true;
32 let prevHash = null;
33
34 const runNext = async () => {
35 if (running) {
36 return;
37 }
38 if (queue.length === 0) {
39 store.dispatch(clientPoll());
40 return;
41 }
42 const action = queue.shift();
43 if (!sync && action.type !== gameActions.DESYNC) {
44 // if we lost sync, the only thing we accept is desync reports
45 running = false;
46 return;
47 }
48 running = true;
49 const me = store.getState().client.keys;
50 const isGameAction = !!gameActions[action.type];
51 const ret = await next({
52 ...action,
53 _me: me && me.id,
54 prev: isGameAction ? prevHash : undefined
55 });
56 const hash = await hashState(store.getState().game);
57 if (action[REMOTE_ACTION]) {
58 prevHash = hash;
59 // we just completed a remote action, assert states match
60 if (sync && hash !== action.next) {
61 // very bad and extremely fatal for now - perhaps someday we recover
62 sync = false;
63 store.dispatch(gameActions.desync(me.id, store.getState().game));
64 }
65 } else if (
66 gameActions[action.type] &&
67 action.type !== gameActions.DESYNC
68 ) {
69 const prev = prevHash;
70 prevHash = hash;
71 // tell everyone else the action happened and the resulting hash
72 const next = hash;
73 const signedAction = ssbKeys.signObj(me, {
74 ...action,
75 prev,
76 next
77 });
78 const res = await serverFetch(`/${encodeURIComponent(next)}`, {
79 method: "POST",
80 body: stringify(signedAction),
81 headers: {
82 "content-type": "application/json"
83 }
84 });
85 if (res.status === 409) {
86 sync = false;
87 store.dispatch(gameActions.desync("client", store.getState().game));
88 const server = await res.json();
89 store.dispatch(gameActions.desync("server", server.state.game));
90 }
91 }
92 const [resolve] = promises.get(action); // hack, maybe should reject?
93 running = false;
94 const state = store.getState();
95 const nextActions = state.game && state.game.nextActions;
96 // only dequeue if a game action just happened - client actions don't count
97 if (
98 nextActions &&
99 nextActions.length > 0 &&
100 (gameActions[action.type] || action.type === CLIENT_LOAD_STATE_DONE) &&
101 !state.client.loadingState
102 ) {
103 const { playerId, notPlayerId, action } = nextActions[0];
104 if (!gameActions[action.type]) {
105 throw new Error(
106 `${action.type} is queued but we don't have a definition`
107 );
108 }
109 if (
110 (playerId && playerId === me.id) ||
111 (notPlayerId && notPlayerId !== me.id) // hack hack hack
112 ) {
113 await store.dispatch({
114 ...action,
115 _fromQueue: true,
116 _needsCreator: true,
117 _sender: me.id
118 });
119 }
120 }
121 resolve(ret);
122 runNext();
123 };
124
125 return action => {
126 if (!action[REMOTE_ACTION]) {
127 action = { ...action, _sender: store.getState().client.keys.id };
128 }
129 queue.push(action);
130 const prom = new Promise((resolve, reject) => {
131 promises.set(action, [resolve, reject]);
132 });
133 setTimeout(runNext, 0);
134 return prom;
135 };
136 };
137}