aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/push.js
blob: a075aa4f1980cc042d2605ca9207a5db4ca0b2c1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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();
}