Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove ServiceWorker Page Control Requirement #745

Merged
merged 26 commits into from
Feb 19, 2021

Conversation

jkasten2
Copy link
Member

@jkasten2 jkasten2 commented Feb 12, 2021

Description

1 Line Summary

Remove ServiceWorker page control requirement.

Details

Summary

This is a quick 2 minute summary, recommend also seeing the full spike work here after reading this summary.
Removes the requirement of the service worker scope to be at or higher than the page you are viewing. (AKA the ServiceWorker has control of the page)

APIs that assumed only one ServiceWorker is on a domain has been replaced with navigator.serviceWorker.getRegistration(scope).

  • Examples; navigator.serviceWorker.controller & navigator.serviceWorker.ready

Dev Sandbox has been updated to a non-root scope to demonstrate these changes work.

What this means

  • No need to merge ServiceWorker files per the "Integrating Multiple Service Workers" guide to support the following.
  • Compatibility with AMP, when using your own domain (AKA not Google proxied)
  • Compatibility with PWA
  • And any other service worker that requires page control

What is PR does NOT do

  • Does not change the default scope
    • This is intended so there is no breaking changes to those who have already setup this SDK. A change in the default will be done in the future.
  • Does not allow more than one OneSignal ServiceWorker per domain. Example en/push/onesignal and es/push/onesignal
    • This is because the same index.db, sessionStorage, and others would be shared.

Full Details

See issue #615 for the full research done before this PR was created.

Systems Affected

  • WebSDK
  • Backend
  • Dashboard

Validation

Tests

Tests have been updated and pass. Need to evaluate changes if we some new tests.

Info

Checklist

  • All the automated tests pass or I explained why that is not possible
  • I have personally tested this on my machine or explained why that is not possible
    • Tested Edge(Chromium), and Firefox on Windows 10.
    • Tested Chrome and Safari on macOS
    • Tested both HTTP and HTTPS sites
    • Tested ServiceWorker upgrading versions
    • Tested on_focus and on_session works
    • Tested notification open focuses an existing tab when it should
    • Tested with and without another SW at the root scope along with OneSignal being sub-scoped.
  • I have included test coverage for these changes or explained why they are not needed
    • Added ServiceWorkerUtilHelper tests
    • Added throws to a new mock for service worker APIs that require page control which we should never use again.

Programming Checklist
Interfaces:

  • Don't use default export
  • New interfaces are in model files

Functions:

  • Don't use default export
  • All function signatures have return types
  • Helpers should not access any data but rather be given the data to operate on.

Typescript:

  • No Typescript warnings
  • Avoid silencing null/undefined warnings with the exclamation point

Other:

  • Iteration: refrain from using elem of array syntax. Prefer forEach or use map
  • Avoid using global OneSignal accessor for context if possible. Instead, we can pass it to function/constructor so that we don't call OneSignal.context
    • Some new OneSignal.context were introduced into this PR as injection of the serviceWorkerManager would have required a number of changes. I recommend instead we come up with a strategy on removing this in another unrelated PR.

Browser behavior

Page to SW Message race condition

I had a concern about how soon you can send a message to a ServiceWorker after registering it. It seems right away as long as you await on navigator.serviceWorker.getRegistration() and check all 3 possible state the worker can be in.

Proof of this behavior:

Page:

async function testThis() {
  await navigator.serviceWorker.register("pwa-sw.js");
  var getReg = await navigator.serviceWorker.getRegistration();
  var curSw = getReg.installing || getReg.waiting || getReg.active;
  curSw.postMessage("[Page -> SW] Hello from page!");
  console.log("getReg.installing: " + getReg.installing + " waiting: " + getReg.waiting + " active: " + getReg.active);
}
testThis();

pwa-sw.js:

self.addEventListener('message', function(event) {
    console.log("[SW Context] Got Messsage: ", event);
});

Results:

02:26:22.019 getReg.installing: [object ServiceWorker] waiting: null active: null debugger eval code:9:18
02:26:22.019 [SW Context] Got Messsage:  [object ExtendableMessageEvent] pwa-sw.js:3:13
02:26:22.020 undefined

image
Confirmed this on Edge(Chromium) 88.0.705.63 , Firefox 85.0.2, and Safari 13.1.2.

Subscribe to push race condition

I had a concern about how soon you can subscribe for push after the service worker is registered. As long as you await on navigator.serviceWorker.getRegistration() this isn't an issue in my testing.
Page:

/**
 * urlBase64ToUint8Array
 * 
 * @param {string} base64String a public vavid key
 */
function urlBase64ToUint8Array(base64String) {
    var padding = '='.repeat((4 - base64String.length % 4) % 4);
    var base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

    var rawData = window.atob(base64);
    var outputArray = new Uint8Array(rawData.length);

    for (var i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

async function testThis() {
  await navigator.serviceWorker.register("pwa-sw.js");
  var getReg = await navigator.serviceWorker.getRegistration();  
  var pushOptions = {
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(
        'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
      )
  };
  var pushSub = await getReg.pushManager.subscribe(pushOptions);
  console.log("pushSub: " + JSON.stringify(pushSub.toJSON()));
}
testThis();

Results:

00:00:35.533
/**
 * urlBase64ToUint8Array
 * 
 * @param {string} base64String a public vavid key
 */…
00:00:35.570
Promise { "pending" }
​
<state>: "fulfilled"
​
<value>: undefined

00:00:35.696 pushSub: {"endpoint":"https://updates.push.services.mozilla.com/wpush/v2/gAAAAABgL…wt29fu6xANwYULvF8u26_o5kNRL18yaTtkUjUkLZBDyguWSaJJWHsr95J87E","keys":{"auth":"HoHFMcrC6byl7BxPpXNF0A","p256dh":"BFT49CnizDrRFdzqQRHSmdRUjKDWvwKco6jzzmKOTFgfbSRjEuBokz0CZxgE6J0bwKnxSCapQKqIp7W7a3tJP0o"}}

Confirmed this on Edge(Chromium) 88.0.705.63 and Firefox 85.0.2.

Checklist

  • I have included screenshots/recordings of the intended results or explained why they are not needed

Related Tickets


This change is Reviewable

* Setup the WebSDK in the dev sandbox under a sub directory scope.
   - This is to confirm the current limitation that this SDK does not
   work if the page currently viewing isn't under control of the sw.
   - The following last message is shown in this example
   - installWorker - Awaiting on navigator.serviceWorker's 'controllerchange' event
* Added OneSignalWorker.js under push/onesignal/ as a possible sub-dir
someone may want to use onesignal under.
* Modified the expresss example's server.js to support the new
subfoldered files.
* No need to wait for page control for any of the sw features
OneSignal uses.
* We do NOT need to wait to setup a listner to get messages sent from
a ServiceWorker
   - In fact it can be setup before we even install one.
* It isn't clear if we need to wait for a specific ServiceWorker state
before we use `postMessage`.
   - We can't use "controllerchange" as this is the specific requirment
   we want to drop.
   - This didn't have an issue while testing this SDK as a whole at this
   point but doesn't mean there could be an issue.
   - TODO to confirm in before considering this PR complete.
* This is no longer being used any more and is dead code
* We also never need to know this as we don't want to depend on
our SW being in control of the page.
* Moved getRegistration to ServicWorkerManager
* getRegistration requires a scope to get the correct sw that only the
ServiceWorkerManager instance can provide
* We don't want to use location.href any more as this is the
ServiceWorker that is in control of the page which may not be
OneSIgnal's.
* Changed `navigator.serviceWorker.controller` to `this.context.serviceWorkerManager.getRegistration()`
* This ensure we get the correct OneSignal ServiceWorker.
* Added error handling that was missing before
   - This was also required to get some tests to pass
* getRegistration is now only defined on ServiceWorkerManager. This
requires a few things now:
   - An instance to ServiceWorkerManager
   - A OneSignal.context, due to above.
* Removed location.href check as we dont' call this any more
* postMessage does now get called in tests due the usage of
getRegistration, this is ok but had to remove NotImplementedError
from the mock so tests do not break.
* This corrects which sw we use to subscribe and unsubscribe from push
* navigator.serviceWorker.controller gets the service worker in control
of the page which isn't what we want.
    - We want to use the serviceWorkerManager.getRegistration() so we
    get the sw OneSignal registered.
* Clean up tests using serviceWorker.ready
* Bypassed isn't something we care about any more as it only matters if
you require page control which our service worker no longer requires.
* This means hard refreashes don't break the SDK either now 🙌!
* We had includeUncontrolled: true in 2 of 3 spots already this was an
outlier without any reason given.
* This is requried to track on_session and on_focus correctly.
* Confirmed this changed was required on an HTTPS setup
* self.location.origin works in the service worker context and won't be
wrong when the scope of the service worker isn't root.
@jkasten2 jkasten2 added the WIP label Feb 12, 2021
@jkasten2 jkasten2 requested review from rgomezp and itrush February 12, 2021 08:52
* Fixed bug where sometime in Firefox `serviceWorker.active` is `null`
because it take a few secounds to become active for postMessage to use.
* See "Page to SW Message race condition" in PR for more details on how
I validated this as a stable solution.
* Since we no longer need to control the page for OneSignal to work
there is no longer a need to claim or skipWaiting for the service worker
* Since the scope of the ServiceWorker can be a path where no HTML pages
are served it is safer to use the origin instead as a default
the notification open URL.
* `Installing` is not a state we care about since we don't need control
of the page so removing the logic for this.
   - Both `postMessage` and `pushManager.subscribe` work in any state.
* `.active` will be covered in a different commit.
* Since we are no longer useing `skipWaiting` and `clients.claim()`
`workerRegistration.active` can't be used to detect if the SW is
`ThirdParty` or not.
* This signal also is unnecessary as the filename is later used with the
call to `swActiveStateByFileName` which should be the source of turth
for this.
* Removed `listenIfPageUncontrolled` flag on listen.
* This is dead code as we never passing in this flag.
* Also controlled vs uncontrolled is no longer relevent with this PR.
* Page control isn't a factor in the PR so cleaned up all comments
referencing to ServiceWorker page control.
* renamed helpers/page/ServiceWorkerHelper to helpers/page/ServiceWorkerUtilHelper.
* There was another `ServiceWorkerHelper` class under just /helpers/
and this was creating some confussion.
* Replaced all `ServiceWorker.active` with a new getAvailableServiceWorker helper method.
* Updated in place `availableWorker` to use this.
Copy link
Contributor

@itrush itrush left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 21 of 21 files at r1, 1 of 1 files at r2.
Reviewable status: 15 of 23 files reviewed, 2 unresolved discussions (waiting on @itrush, @jkasten2, and @rgomezp)


src/helpers/SubscriptionHelper.ts, line 165 at r3 (raw file):

OneSignal.context.serviceWorkerManager

possibly replace with function argument to keep helper clean of dependencies


src/helpers/page/ServiceWorkerUtilHelper.ts, line 20 at r3 (raw file):

throw new Error("Could not find an available ServiceWorker instance!");

just be careful about this error, it may break people's sites

Copy link
Contributor

@itrush itrush left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 8 of 8 files at r3.
Reviewable status: all files reviewed, 2 unresolved discussions (waiting on @jkasten2 and @rgomezp)

* There are cases where getAvailableServiceWorker returning `null` is ok
and it safe to move on so the following was done.
   - Removed the throw
   - Added a warning log etnry
   - Added null handling where this method is being called.
@jkasten2 jkasten2 force-pushed the feat/remove_sw_page_control_requirement branch from 5c0ccfc to 57f60b2 Compare February 18, 2021 11:36
* Created a MockServiceWorkerContainerWithAPIBan that throws on the APIs
we want to ban
* MockServiceWorkerContainerWithAPIBan instance is created by default
for all test's env setup.
   - This means any test that hits these APIs will throw, even for new
   tests
* Also added self test (meta tests) for the new
MockServiceWorkerContainerWithAPIBan class
* Clean up - Moved access to OneSignal.context.serviceWorkerManager
down one call
stack from getRawPushSubscriptionFromServiceWorkerRegistration
to getRawPushSubscription
Copy link
Member Author

@jkasten2 jkasten2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 18 of 30 files reviewed, 2 unresolved discussions (waiting on @itrush and @rgomezp)


src/helpers/SubscriptionHelper.ts, line 165 at r3 (raw file):

Previously, itrush (Iryna Trush) wrote…
OneSignal.context.serviceWorkerManager

possibly replace with function argument to keep helper clean of dependencies

I move the access to OneSignal.context.serviceWorkerManager down one level to SubscriptionHelper.getRawPushSubscription to make things a bit more separated. However the call stack for this goes through MainHelper.createDeviceRecord which is also static and has 7 entry points. This helper layering isn't the best design and adding a 3 param to each seems like a side step.

This points to a larger issues of using state through statics instead of creating instance where we can do dependency injection.


src/helpers/page/ServiceWorkerUtilHelper.ts, line 20 at r3 (raw file):

Previously, itrush (Iryna Trush) wrote…
throw new Error("Could not find an available ServiceWorker instance!");

just be careful about this error, it may break people's sites

Fixed! Turned this into a warning. On the consuming end added a null check.

@jkasten2 jkasten2 requested a review from itrush February 19, 2021 03:09
@jkasten2 jkasten2 removed the WIP label Feb 19, 2021
* establishServiceWorkerChannel already internally handles if the
ServiceWorker exists already and doesn't require active status.
jkasten2 added a commit that referenced this pull request Feb 19, 2021
* Added a new changedServiceWorkerParams method which check if;
   - OneSignal ServiceWorker path, filename, or query params changed.
   - OneSignal ServierWorker scope changed
* This is a new check that was added to shouldInstallWorker
* A developer might change the OneSignal service worker params to take
advantage of the PR #745 "Remove ServiceWorker Page Control Requirement"
  - This way the new OneSignal SW scope can be setup while their new
  root scoped SW can be installed at the same time.
Copy link
Contributor

@itrush itrush left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 14 of 14 files at r4.
Reviewable status: all files reviewed, 6 unresolved discussions (waiting on @jkasten2 and @rgomezp)


src/helpers/SubscriptionHelper.ts, line 165 at r3 (raw file):

Previously, jkasten2 (Josh Kasten) wrote…

I move the access to OneSignal.context.serviceWorkerManager down one level to SubscriptionHelper.getRawPushSubscription to make things a bit more separated. However the call stack for this goes through MainHelper.createDeviceRecord which is also static and has 7 entry points. This helper layering isn't the best design and adding a 3 param to each seems like a side step.

This points to a larger issues of using state through statics instead of creating instance where we can do dependency injection.

yeah, it's asking for a nice refactor :) but if it's too involved, let's keep it simple for now


src/managers/ServiceWorkerManager.ts, line 89 at r4 (raw file):

····

nit: extra spaces


test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts, line 7 at r4 (raw file):

·

nit: trailing whitespace


test/support/mocks/service-workers/models/MockServiceWorkerContainerWithAPIBan.ts, line 13 at r4 (raw file):

··

nit: trailing spaces


test/support/mocks/service-workers/models/MockServiceWorkerContainerWithAPIBan.ts, line 46 at r4 (raw file):

}

nit: empty line at the end of the file


test/unit/helpers/page/ServiceWorkerUtilHelper.ts, line 34 at r4 (raw file):

});

// See test/unit/manager/ServiceWorkerManager.ts for other tests that cover this class

nit: new line at the end of the file


test/unit/managers/ServiceWorkerManager.ts, line 187 at r3 (raw file):

navigator.serviceWorker.controller

just to double-check: if someone writes it like this in tests, it would throw too?


test/unit/meta/mockPushManager.ts, line 57 at r4 (raw file):

subscribedSubscription

subscribed subscription, buttery butter 🙃

Copy link
Member Author

@jkasten2 jkasten2 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewable status: 25 of 30 files reviewed, 6 unresolved discussions (waiting on @itrush and @rgomezp)


src/helpers/SubscriptionHelper.ts, line 165 at r3 (raw file):

Previously, itrush (Iryna Trush) wrote…

yeah, it's asking for a nice refactor :) but if it's too involved, let's keep it simple for now

Thanks!


src/managers/ServiceWorkerManager.ts, line 89 at r4 (raw file):

Previously, itrush (Iryna Trush) wrote…
····

nit: extra spaces

Fixed!


test/support/mocks/service-workers/models/MockServiceWorkerContainer.ts, line 7 at r4 (raw file):

Previously, itrush (Iryna Trush) wrote…
·

nit: trailing whitespace

Fixed!


test/support/mocks/service-workers/models/MockServiceWorkerContainerWithAPIBan.ts, line 13 at r4 (raw file):

Previously, itrush (Iryna Trush) wrote…
··

nit: trailing spaces

Fixed!


test/support/mocks/service-workers/models/MockServiceWorkerContainerWithAPIBan.ts, line 46 at r4 (raw file):

Previously, itrush (Iryna Trush) wrote…
}

nit: empty line at the end of the file

Fixed!


test/unit/helpers/page/ServiceWorkerUtilHelper.ts, line 34 at r4 (raw file):

Previously, itrush (Iryna Trush) wrote…

nit: new line at the end of the file

Fixed!


test/unit/managers/ServiceWorkerManager.ts, line 187 at r3 (raw file):

Previously, itrush (Iryna Trush) wrote…
navigator.serviceWorker.controller

just to double-check: if someone writes it like this in tests, it would throw too?

Yes! Also will throw if someone adds this to something in src /and it runs in any test.


test/unit/meta/mockPushManager.ts, line 57 at r4 (raw file):

Previously, itrush (Iryna Trush) wrote…
subscribedSubscription

subscribed subscription, buttery butter 🙃

haha, just making sure someone doesn't think it is I can't believe it's not butter! 😆

Anyway I picked a better name.

@jkasten2 jkasten2 merged commit 76a7d5d into master Feb 19, 2021
@jkasten2 jkasten2 deleted the feat/remove_sw_page_control_requirement branch February 19, 2021 20:03
jkasten2 added a commit that referenced this pull request Feb 20, 2021
* Added a new changedServiceWorkerParams method which check if;
   - OneSignal ServiceWorker path, filename, or query params changed.
   - OneSignal ServierWorker scope changed
* This is a new check that was added to shouldInstallWorker
* A developer might change the OneSignal service worker params to take
advantage of the PR #745 "Remove ServiceWorker Page Control Requirement"
  - This way the new OneSignal SW scope can be setup while their new
  root scoped SW can be installed at the same time.
jkasten2 added a commit that referenced this pull request Feb 23, 2021
* Added a new changedServiceWorkerParams method which check if;
   - OneSignal ServiceWorker path, filename, or query params changed.
   - OneSignal ServierWorker scope changed
* This is a new check that was added to shouldInstallWorker
* A developer might change the OneSignal service worker params to take
advantage of the PR #745 "Remove ServiceWorker Page Control Requirement"
  - This way the new OneSignal SW scope can be setup while their new
  root scoped SW can be installed at the same time.
jkasten2 added a commit that referenced this pull request Feb 24, 2021
* Added a new changedServiceWorkerParams method which check if;
   - OneSignal ServiceWorker path, filename, or query params changed.
   - OneSignal ServierWorker scope changed
* This is a new check that was added to shouldInstallWorker
* A developer might change the OneSignal service worker params to take
advantage of the PR #745 "Remove ServiceWorker Page Control Requirement"
  - This way the new OneSignal SW scope can be setup while their new
  root scoped SW can be installed at the same time.
rgomezp pushed a commit that referenced this pull request Mar 3, 2021
* Added a new changedServiceWorkerParams method which check if;
   - OneSignal ServiceWorker path, filename, or query params changed.
   - OneSignal ServierWorker scope changed
* This is a new check that was added to shouldInstallWorker
* A developer might change the OneSignal service worker params to take
advantage of the PR #745 "Remove ServiceWorker Page Control Requirement"
  - This way the new OneSignal SW scope can be setup while their new
  root scoped SW can be installed at the same time.
@jkasten2
Copy link
Member Author

Subscribe to push race condition

I had a concern about how soon you can subscribe for push after the service worker is registered. As long as you await on navigator.serviceWorker.getRegistration() this isn't an issue in my testing.
Page:

/**
 * urlBase64ToUint8Array
 * 
 * @param {string} base64String a public vavid key
 */
function urlBase64ToUint8Array(base64String) {
    var padding = '='.repeat((4 - base64String.length % 4) % 4);
    var base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

    var rawData = window.atob(base64);
    var outputArray = new Uint8Array(rawData.length);

    for (var i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

async function testThis() {
  await navigator.serviceWorker.register("pwa-sw.js");
  var getReg = await navigator.serviceWorker.getRegistration();  
  var pushOptions = {
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(
        'BEl62iUYgUivxIkv69yViEuiBIa-Ib9-SkvMeAtA3LFgDzkrxZJjSgSnfckjBJuBkr3qBUYIHBQFLXYp5Nksh8U'
      )
  };
  var pushSub = await getReg.pushManager.subscribe(pushOptions);
  console.log("pushSub: " + JSON.stringify(pushSub.toJSON()));
}
testThis();

It turned out this wasn't always true. If another service worker is registered already under a scope of the newly registered one you may get the first one. Example first service worker scope could be / and the 2nd one /my-scope you may sometimes get the service worker at / and something the one at /my-scope.

This part of the logic from this PR was corrected in PR #1136

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants