mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-04 13:03:37 +00:00
fix(puter-js): subscribe once the worker is active, and retry a subscription the push service hasn't settled
This commit is contained in:
1 parent
eb1ad240f2
commit
d587ffa428
2 files changed
+119
-20
No files matched your search
@@ -122,6 +122,19 @@ const subscriptionBody = (subscription) => {
|
||||
return { endpoint, keys: { p256dh: keys?.p256dh, auth: keys?.auth } };
|
||||
};
|
||||
|
||||
const subscribeWith = (registration, key) =>
|
||||
registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: key,
|
||||
});
|
||||
|
||||
/** Tells the server about `subscription`; `active` or `pending`. */
|
||||
const announce = (puter, subscription, label) =>
|
||||
request(puter, 'POST', '/push/web/subscriptions', {
|
||||
subscription: subscriptionBody(subscription),
|
||||
...(label ? { label } : {}),
|
||||
});
|
||||
|
||||
/**
|
||||
* Subscribes this browser and registers it with the server. A new subscription
|
||||
* is confirmed by a push the worker hands back; a known one is only refreshed.
|
||||
@@ -151,31 +164,28 @@ export async function syncWebPush(
|
||||
await subscription.unsubscribe();
|
||||
subscription = null;
|
||||
}
|
||||
subscription ??= await registration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: key,
|
||||
});
|
||||
subscription ??= await subscribeWith(registration, key);
|
||||
|
||||
const body = {
|
||||
subscription: subscriptionBody(subscription),
|
||||
...(label ? { label } : {}),
|
||||
};
|
||||
// Listening first: the confirmation can arrive before the POST returns.
|
||||
const challenge = nextChallenge(nav);
|
||||
try {
|
||||
const result = await request(
|
||||
puter,
|
||||
'POST',
|
||||
'/push/web/subscriptions',
|
||||
body,
|
||||
);
|
||||
let result;
|
||||
try {
|
||||
result = await announce(puter, subscription, label);
|
||||
} catch (error) {
|
||||
if (error?.code !== 'push_subscription_invalid') throw error;
|
||||
// The push service may not know a brand-new subscription yet; one fresh one is tried.
|
||||
await subscription.unsubscribe();
|
||||
subscription = await subscribeWith(registration, key);
|
||||
result = await announce(puter, subscription, label);
|
||||
}
|
||||
if (result.status === 'active') return result.device;
|
||||
const { device } = await request(
|
||||
puter,
|
||||
'POST',
|
||||
'/push/web/subscriptions/verify',
|
||||
{
|
||||
subscription: body.subscription,
|
||||
subscription: subscriptionBody(subscription),
|
||||
challenge: await challenge.promise,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -16,29 +16,49 @@ vi.mock('./lib/pushStore.js', () => ({
|
||||
const { syncWebPush, removeWebPush, fromBase64Url, workerFor } =
|
||||
await import('./lib/webPush.js');
|
||||
const { NotificationsModule } = await import('./index.js');
|
||||
const { PuterJSError } = await import('../../lib/PuterJSError.js');
|
||||
const { shownFrom } = await import('./onPush.js');
|
||||
|
||||
// 65 bytes, 0x04 first, as base64url.
|
||||
const KEY = Buffer.from([4, ...Array(64).fill(7)]).toString('base64url');
|
||||
|
||||
const makeSubscription = (key = fromBase64Url(KEY)) => ({
|
||||
endpoint: 'https://push.example/send/abc',
|
||||
const makeSubscription = (
|
||||
key = fromBase64Url(KEY),
|
||||
endpoint = 'https://push.example/send/abc',
|
||||
) => ({
|
||||
endpoint,
|
||||
options: { applicationServerKey: key.buffer },
|
||||
toJSON: () => ({
|
||||
endpoint: 'https://push.example/send/abc',
|
||||
endpoint,
|
||||
expirationTime: null,
|
||||
keys: { p256dh: 'p', auth: 'a' },
|
||||
}),
|
||||
unsubscribe: vi.fn(async () => true),
|
||||
});
|
||||
|
||||
const makeNav = ({ existing = null } = {}) => {
|
||||
/** A worker still installing; `activate()` moves it to `activated`. */
|
||||
const makeWorker = () => {
|
||||
const listeners = new Set();
|
||||
return {
|
||||
state: 'installing',
|
||||
addEventListener: (type, fn) => listeners.add(fn),
|
||||
removeEventListener: (type, fn) => listeners.delete(fn),
|
||||
activate() {
|
||||
this.state = 'activated';
|
||||
for (const fn of [...listeners]) fn();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const makeNav = ({ existing = null, installing = null } = {}) => {
|
||||
const listeners = new Set();
|
||||
const pushManager = {
|
||||
getSubscription: vi.fn(async () => existing),
|
||||
subscribe: vi.fn(async () => makeSubscription()),
|
||||
};
|
||||
const registration = { active: {}, pushManager };
|
||||
const registration = installing
|
||||
? { active: null, installing, pushManager }
|
||||
: { active: {}, pushManager };
|
||||
return {
|
||||
pushManager,
|
||||
registration,
|
||||
@@ -159,6 +179,75 @@ describe('syncWebPush', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('subscribes once the worker is active', async () => {
|
||||
const installing = makeWorker();
|
||||
const nav = makeNav({ installing });
|
||||
mockRequest.mockImplementation(async (_p, method, route) =>
|
||||
route === '/push/web/key'
|
||||
? { publicKey: KEY }
|
||||
: { status: 'active', device: { uid: 'd4' } },
|
||||
);
|
||||
const pending = syncWebPush(puter, { worker, nav });
|
||||
await flush();
|
||||
expect(nav.serviceWorker.register).toHaveBeenCalled();
|
||||
expect(nav.pushManager.subscribe).not.toHaveBeenCalled();
|
||||
installing.activate();
|
||||
await expect(pending).resolves.toEqual({ uid: 'd4' });
|
||||
expect(nav.pushManager.subscribe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('replaces a subscription the push service refused, once', async () => {
|
||||
const first = makeSubscription(undefined, 'https://push.example/1');
|
||||
const second = makeSubscription(undefined, 'https://push.example/2');
|
||||
const nav = makeNav();
|
||||
nav.pushManager.subscribe
|
||||
.mockResolvedValueOnce(first)
|
||||
.mockResolvedValueOnce(second);
|
||||
let announced = 0;
|
||||
mockRequest.mockImplementation(async (_p, method, route, body) => {
|
||||
if (route === '/push/web/key') return { publicKey: KEY };
|
||||
if (route === '/push/web/subscriptions') {
|
||||
if (announced++ === 0) {
|
||||
throw new PuterJSError(
|
||||
'refused',
|
||||
'push_subscription_invalid',
|
||||
);
|
||||
}
|
||||
queueMicrotask(() =>
|
||||
nav.post({
|
||||
type: 'puter-push-challenge',
|
||||
challenge: 'c.sig',
|
||||
}),
|
||||
);
|
||||
return { status: 'pending' };
|
||||
}
|
||||
return {
|
||||
device: { uid: 'd5', endpoint: body.subscription.endpoint },
|
||||
};
|
||||
});
|
||||
await expect(syncWebPush(puter, { worker, nav })).resolves.toEqual({
|
||||
uid: 'd5',
|
||||
endpoint: 'https://push.example/2',
|
||||
});
|
||||
expect(first.unsubscribe).toHaveBeenCalledTimes(1);
|
||||
expect(second.unsubscribe).not.toHaveBeenCalled();
|
||||
expect(nav.pushManager.subscribe).toHaveBeenCalledTimes(2);
|
||||
expect(announced).toBe(2);
|
||||
});
|
||||
|
||||
it('rejects when the fresh subscription is refused too', async () => {
|
||||
const nav = makeNav();
|
||||
mockRequest.mockImplementation(async (_p, method, route) => {
|
||||
if (route === '/push/web/key') return { publicKey: KEY };
|
||||
throw new PuterJSError('refused', 'push_subscription_invalid');
|
||||
});
|
||||
await expect(syncWebPush(puter, { worker, nav })).rejects.toMatchObject(
|
||||
{ code: 'push_subscription_invalid' },
|
||||
);
|
||||
expect(nav.pushManager.subscribe).toHaveBeenCalledTimes(2);
|
||||
expect(mockRequest).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeWebPush', () => {
|
||||
|
||||
Reference in new issue
Block a user