aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/push.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/push.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/push.js68
1 files changed, 68 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/push.js b/packages/meshbay-hub/src/meshbay_hub/static/push.js
new file mode 100644
index 0000000..a075aa4
--- /dev/null
+++ b/packages/meshbay-hub/src/meshbay_hub/static/push.js
@@ -0,0 +1,68 @@
+/**
+ * Notifications on this phone: the page's half.
+ *
+ * Nothing to install: the shell fetches what is new on its own every quarter
+ * of an hour, and when the phone already has a UnifiedPush distributor it is
+ * used too, so they arrive at once. The page gives the hub whatever the shell
+ * ends up with, because the page holds the session. Whether a notification is
+ * wanted — every one turned off, or a group muted — is decided on the hub,
+ * which then creates nothing (docs/MESHBAY_DESIGN.md §11.3).
+ */
+import * as platform from './platform.js';
+import { hubFetch } from './hub-client.js';
+
+// How long turning it on waits for a distributor before registering without
+// one; when the endpoint comes later, the next sync hands it over.
+const WAIT_MS = 10_000;
+const POLL_MS = 500;
+
+/**
+ * Make the hub's row match what this phone has: at every start and sign-in,
+ * and after turning it on. A distributor may hand out a new endpoint, or go
+ * away, at any time, page running or not; this is where the hub hears it.
+ */
+export async function syncPush(user) {
+ if (!platform.push.available || !user) return null;
+ const s = await platform.push.status();
+ if (!s.enabled) return s;
+ const mine = s.subscription && s.account === user.userId;
+ if (mine && s.registered === s.endpoint) return s;
+ const body = s.endpoint
+ ? { endpoint: s.endpoint, p256dh: s.p256dh, auth: s.auth }
+ : {};
+ if (mine) body.id = s.subscription;
+ const r = await hubFetch('/v1/push/subscriptions', {
+ method: 'POST', token: user.token, body,
+ });
+ return platform.push.remember(r.id, user.userId, r.poll_secret, r.now);
+}
+
+/** Turn it on: a moment for a distributor to answer, then register either way. */
+export async function enablePush(user, onProgress) {
+ let s = await platform.push.enable();
+ const until = Date.now() + WAIT_MS;
+ while (s.state === 'pending' && Date.now() < until) {
+ if (onProgress) onProgress(s);
+ await new Promise((resolve) => setTimeout(resolve, POLL_MS));
+ s = await platform.push.status();
+ }
+ return syncPush(user).then((synced) => synced || s);
+}
+
+/**
+ * Turn it off: the hub forgets this phone first, then the phone stops. The hub
+ * half is the one that can fail; the phone half happens regardless, so a
+ * refused request never leaves it on here.
+ */
+export async function disablePush(user) {
+ if (!platform.push.available) return null;
+ const s = await platform.push.status();
+ if (s.subscription && user && s.account === user.userId) {
+ try {
+ await hubFetch(`/v1/push/subscriptions/${s.subscription}`, {
+ method: 'DELETE', token: user.token,
+ });
+ } catch (e) { /* already gone, or unreachable: the phone half still happens */ }
+ }
+ return platform.push.disable();
+}