Account sync in action
This commit is contained in:
parent
3dd8dd4288
commit
fdee54f921
8 changed files with 118 additions and 32 deletions
|
@ -46,6 +46,7 @@ func (s *Server) handleAccountGet(w http.ResponseWriter, _ *http.Request, v *vis
|
|||
return err
|
||||
}
|
||||
limits, stats := info.Limits, info.Stats
|
||||
|
||||
response := &apiAccountResponse{
|
||||
Limits: &apiAccountLimits{
|
||||
Basis: string(limits.Basis),
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
accountSubscriptionSingleUrl,
|
||||
accountSubscriptionUrl,
|
||||
accountTokenUrl,
|
||||
accountUrl,
|
||||
accountUrl, maybeWithAuth, topicUrl,
|
||||
withBasicAuth,
|
||||
withBearerAuth
|
||||
} from "./utils";
|
||||
|
@ -15,6 +15,7 @@ import subscriptionManager from "./SubscriptionManager";
|
|||
import i18n from "i18next";
|
||||
import prefs from "./Prefs";
|
||||
import routes from "../components/routes";
|
||||
import userManager from "./UserManager";
|
||||
|
||||
const delayMillis = 45000; // 45 seconds
|
||||
const intervalMillis = 900000; // 15 minutes
|
||||
|
@ -23,6 +24,11 @@ class AccountApi {
|
|||
constructor() {
|
||||
this.timer = null;
|
||||
this.listener = null; // Fired when account is fetched from remote
|
||||
|
||||
// Random ID used to identify this client when sending/receiving "sync" events
|
||||
// to the sync topic of an account. This ID doesn't matter much, but it will prevent
|
||||
// a client from reacting to its own message.
|
||||
this.identity = Math.floor(Math.random() * 2586000);
|
||||
}
|
||||
|
||||
registerListener(listener) {
|
||||
|
@ -164,6 +170,7 @@ class AccountApi {
|
|||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
this.triggerChange(); // Dangle!
|
||||
}
|
||||
|
||||
async addSubscription(payload) {
|
||||
|
@ -182,6 +189,7 @@ class AccountApi {
|
|||
}
|
||||
const subscription = await response.json();
|
||||
console.log(`[AccountApi] Subscription`, subscription);
|
||||
this.triggerChange(); // Dangle!
|
||||
return subscription;
|
||||
}
|
||||
|
||||
|
@ -201,6 +209,7 @@ class AccountApi {
|
|||
}
|
||||
const subscription = await response.json();
|
||||
console.log(`[AccountApi] Subscription`, subscription);
|
||||
this.triggerChange(); // Dangle!
|
||||
return subscription;
|
||||
}
|
||||
|
||||
|
@ -216,6 +225,7 @@ class AccountApi {
|
|||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
this.triggerChange(); // Dangle!
|
||||
}
|
||||
|
||||
async upsertAccess(topic, everyone) {
|
||||
|
@ -236,6 +246,7 @@ class AccountApi {
|
|||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
this.triggerChange(); // Dangle!
|
||||
}
|
||||
|
||||
async deleteAccess(topic) {
|
||||
|
@ -250,6 +261,7 @@ class AccountApi {
|
|||
} else if (response.status !== 200) {
|
||||
throw new Error(`Unexpected server response ${response.status}`);
|
||||
}
|
||||
this.triggerChange(); // Dangle!
|
||||
}
|
||||
|
||||
async sync() {
|
||||
|
@ -285,6 +297,34 @@ class AccountApi {
|
|||
}
|
||||
}
|
||||
|
||||
async triggerChange() {
|
||||
const account = await this.get();
|
||||
if (!account.sync_topic) {
|
||||
return;
|
||||
}
|
||||
const url = topicUrl(config.base_url, account.sync_topic);
|
||||
console.log(`[AccountApi] Triggering account change to ${url}`);
|
||||
const user = await userManager.get(config.base_url);
|
||||
const headers = {
|
||||
Cache: "no" // We really don't need to store this!
|
||||
};
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
event: "sync",
|
||||
source: this.identity
|
||||
}),
|
||||
headers: maybeWithAuth(headers, user)
|
||||
});
|
||||
if (response.status < 200 || response.status > 299) {
|
||||
throw new Error(`Unexpected response: ${response.status}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[AccountApi] Publishing to sync topic failed`, e);
|
||||
}
|
||||
}
|
||||
|
||||
startWorker() {
|
||||
if (this.timer !== null) {
|
||||
return;
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
import {
|
||||
accountPasswordUrl,
|
||||
accountSettingsUrl,
|
||||
accountSubscriptionSingleUrl,
|
||||
accountSubscriptionUrl,
|
||||
accountTokenUrl,
|
||||
accountUrl,
|
||||
fetchLinesIterator, maybeWithAuth,
|
||||
withBasicAuth,
|
||||
withBearerAuth,
|
||||
fetchLinesIterator,
|
||||
maybeWithAuth,
|
||||
topicShortUrl,
|
||||
topicUrl,
|
||||
topicUrlAuth,
|
||||
|
|
|
@ -11,7 +11,7 @@ class ConnectionManager {
|
|||
constructor() {
|
||||
this.connections = new Map(); // ConnectionId -> Connection (hash, see below)
|
||||
this.stateListener = null; // Fired when connection state changes
|
||||
this.notificationListener = null; // Fired when new notifications arrive
|
||||
this.messageListener = null; // Fired when new notifications arrive
|
||||
}
|
||||
|
||||
registerStateListener(listener) {
|
||||
|
@ -22,12 +22,12 @@ class ConnectionManager {
|
|||
this.stateListener = null;
|
||||
}
|
||||
|
||||
registerNotificationListener(listener) {
|
||||
this.notificationListener = listener;
|
||||
registerMessageListener(listener) {
|
||||
this.messageListener = listener;
|
||||
}
|
||||
|
||||
resetNotificationListener() {
|
||||
this.notificationListener = null;
|
||||
resetMessageListener() {
|
||||
this.messageListener = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -97,9 +97,9 @@ class ConnectionManager {
|
|||
}
|
||||
|
||||
notificationReceived(subscriptionId, notification) {
|
||||
if (this.notificationListener) {
|
||||
if (this.messageListener) {
|
||||
try {
|
||||
this.notificationListener(subscriptionId, notification);
|
||||
this.messageListener(subscriptionId, notification);
|
||||
} catch (e) {
|
||||
console.error(`[ConnectionManager] Error handling notification for ${subscriptionId}`, e);
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@ class SubscriptionManager {
|
|||
topic: topic,
|
||||
mutedUntil: 0,
|
||||
last: null,
|
||||
remoteId: null
|
||||
remoteId: null,
|
||||
internal: false
|
||||
};
|
||||
await db.subscriptions.put(subscription);
|
||||
return subscription;
|
||||
|
@ -44,9 +45,11 @@ class SubscriptionManager {
|
|||
const remote = remoteSubscriptions[i];
|
||||
const local = await this.add(remote.base_url, remote.topic);
|
||||
const reservation = remoteReservations?.find(r => remote.base_url === config.base_url && remote.topic === r.topic) || null;
|
||||
await this.setRemoteId(local.id, remote.id);
|
||||
await this.setDisplayName(local.id, remote.display_name);
|
||||
await this.setReservation(local.id, reservation); // May be null!
|
||||
await this.update(local.id, {
|
||||
remoteId: remote.id,
|
||||
displayName: remote.display_name,
|
||||
reservation: reservation // May be null!
|
||||
});
|
||||
remoteIds.push(remote.id);
|
||||
}
|
||||
|
||||
|
@ -54,7 +57,8 @@ class SubscriptionManager {
|
|||
const localSubscriptions = await db.subscriptions.toArray();
|
||||
for (let i = 0; i < localSubscriptions.length; i++) {
|
||||
const local = localSubscriptions[i];
|
||||
if (!local.remoteId || !remoteIds.includes(local.remoteId)) {
|
||||
const remoteExists = local.remoteId && remoteIds.includes(local.remoteId);
|
||||
if (!local.internal && !remoteExists) {
|
||||
await this.remove(local.id);
|
||||
}
|
||||
}
|
||||
|
@ -182,6 +186,10 @@ class SubscriptionManager {
|
|||
});
|
||||
}
|
||||
|
||||
async update(subscriptionId, params) {
|
||||
await db.subscriptions.update(subscriptionId, params);
|
||||
}
|
||||
|
||||
async pruneNotifications(thresholdTimestamp) {
|
||||
await db.notifications
|
||||
.where("time").below(thresholdTimestamp)
|
||||
|
|
|
@ -27,6 +27,7 @@ import Login from "./Login";
|
|||
import Pricing from "./Pricing";
|
||||
import Signup from "./Signup";
|
||||
import Account from "./Account";
|
||||
import accountApi from "../app/AccountApi";
|
||||
|
||||
export const AccountContext = createContext(null);
|
||||
|
||||
|
@ -79,6 +80,20 @@ const Layout = () => {
|
|||
useBackgroundProcesses();
|
||||
useEffect(() => updateTitle(newNotificationsCount), [newNotificationsCount]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!account || !account.sync_topic) {
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
const subscription = await subscriptionManager.add(config.base_url, account.sync_topic);
|
||||
if (!subscription.hidden) {
|
||||
await subscriptionManager.update(subscription.id, {
|
||||
internal: true
|
||||
});
|
||||
}
|
||||
})();
|
||||
}, [account]);
|
||||
|
||||
return (
|
||||
<Box sx={{display: 'flex'}}>
|
||||
<ActionBar
|
||||
|
|
|
@ -215,9 +215,11 @@ const UpgradeBanner = () => {
|
|||
};
|
||||
|
||||
const SubscriptionList = (props) => {
|
||||
const sortedSubscriptions = props.subscriptions.sort( (a, b) => {
|
||||
return (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic)) ? -1 : 1;
|
||||
});
|
||||
const sortedSubscriptions = props.subscriptions
|
||||
.filter(s => !s.internal)
|
||||
.sort((a, b) => {
|
||||
return (topicUrl(a.baseUrl, a.topic) < topicUrl(b.baseUrl, b.topic)) ? -1 : 1;
|
||||
});
|
||||
return (
|
||||
<>
|
||||
{sortedSubscriptions.map(subscription =>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {useNavigate, useParams} from "react-router-dom";
|
||||
import {useEffect, useState} from "react";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import subscriptionManager from "../app/SubscriptionManager";
|
||||
import {disallowedTopic, expandSecureUrl, topicUrl} from "../app/utils";
|
||||
import notifier from "../app/Notifier";
|
||||
|
@ -10,6 +10,7 @@ import pruner from "../app/Pruner";
|
|||
import session from "../app/Session";
|
||||
import {UnauthorizedError} from "../app/AccountApi";
|
||||
import accountApi from "../app/AccountApi";
|
||||
import {AccountContext} from "./App";
|
||||
|
||||
/**
|
||||
* Wire connectionManager and subscriptionManager so that subscriptions are updated when the connection
|
||||
|
@ -20,6 +21,34 @@ export const useConnectionListeners = (subscriptions, users) => {
|
|||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const handleMessage = async (subscriptionId, message) => {
|
||||
const subscription = await subscriptionManager.get(subscriptionId);
|
||||
if (subscription.internal) {
|
||||
await handleInternalMessage(message);
|
||||
} else {
|
||||
await handleNotification(subscriptionId, message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInternalMessage = async (message) => {
|
||||
console.log(`[ConnectionListener] Received message on sync topic`, message.message);
|
||||
try {
|
||||
const data = JSON.parse(message.message);
|
||||
if (data.event === "sync") {
|
||||
if (data.source !== accountApi.identity) {
|
||||
console.log(`[ConnectionListener] Triggering account sync`);
|
||||
await accountApi.sync();
|
||||
} else {
|
||||
console.log(`[ConnectionListener] I triggered the account sync, ignoring message`);
|
||||
}
|
||||
} else {
|
||||
console.log(`[ConnectionListener] Unknown message type. Doing nothing.`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[ConnectionListener] Error parsing sync topic message`, e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNotification = async (subscriptionId, notification) => {
|
||||
const added = await subscriptionManager.addNotification(subscriptionId, notification);
|
||||
if (added) {
|
||||
|
@ -28,10 +57,10 @@ export const useConnectionListeners = (subscriptions, users) => {
|
|||
}
|
||||
};
|
||||
connectionManager.registerStateListener(subscriptionManager.updateState);
|
||||
connectionManager.registerNotificationListener(handleNotification);
|
||||
connectionManager.registerMessageListener(handleMessage);
|
||||
return () => {
|
||||
connectionManager.resetStateListener();
|
||||
connectionManager.resetNotificationListener();
|
||||
connectionManager.resetMessageListener();
|
||||
}
|
||||
},
|
||||
// We have to disable dep checking for "navigate". This is fine, it never changes.
|
||||
|
@ -100,11 +129,9 @@ export const useBackgroundProcesses = () => {
|
|||
export const useAccountListener = (setAccount) => {
|
||||
useEffect(() => {
|
||||
accountApi.registerListener(setAccount);
|
||||
(async () => {
|
||||
await accountApi.sync();
|
||||
})();
|
||||
accountApi.sync(); // Dangle
|
||||
return () => {
|
||||
accountApi.registerListener();
|
||||
accountApi.resetListener();
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue