Skip to content

Commit

Permalink
SW: bg sync with fallback
Browse files Browse the repository at this point in the history
  • Loading branch information
s-cork committed Nov 27, 2023
1 parent 4793dc1 commit 87dedb7
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 20 deletions.
12 changes: 6 additions & 6 deletions client_code/service_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
# escape hatches
registration = REG
service_worker = SW
sync_manager = REG.sync
periodic_sync_manager = REG.periodicSync
# not supported by all browsers
# sync_manager = REG.sync
# periodic_sync_manager = REG.periodicSync


def _error_handler(err):
Expand Down Expand Up @@ -108,15 +109,14 @@ def _camel(s):
def register_sync(tag, **options):
"""Registers a background sync when the app comes back online - may fail in some browsers"""
READY.wait()
if sync_manager.getTags(tag):
return
options = {_camel(k): v for k, v in options.items()}
sync_manager.register(tag, options)
SW.postMessage({"type": "SYNC", "tag": tag})


def register_periodic_sync(tag, *, min_interval=None, **options):
"""Registers a periodic sync request with the browser with the specified tag and options"""
READY.wait()
# TODO have some sort of fallback
periodic_sync_manager = REG.periodicSync
if periodic_sync_manager.getTags(tag):
return
options["min_interval"] = min_interval
Expand Down
142 changes: 142 additions & 0 deletions js/service-worker/worker/bg-sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/// <reference lib="WebWorker" />

import { Deferred } from "../../worker/types.ts";
import { defer } from "../../worker/utils/worker.ts";
import { MODULE_LOADING } from "./constants.ts";

interface OnSyncCallback {
(): void | Promise<void>;
}

export interface DeferredSync extends Deferred<null> {
$handled?: boolean;
}

const supportsSync = "sync" in self.registration;

const log = (...args: any[]) => {
// console.log("%cBACKGROUND SYNC", "color: hotpink;", ...args);
};

export class BackgroundSync {
static _initSyncs = new Map<string, [SyncEvent, Deferred<null>]>();
static _instances = new Map<string, BackgroundSync>();

_requestsAddedDuringSync = false;
_syncInProgress = false;
_fallbackRegistered = false;

constructor(readonly tag: string, readonly onSync: OnSyncCallback) {
BackgroundSync._instances.set(tag, this);
this._addSyncListener();
const initSync = BackgroundSync._initSyncs.get(this.tag);
if (initSync) {
this._syncListener(...initSync);
}
if (!supportsSync) {
this._registerFallback();
}
// if any active syncs from restarting then call onSync with the event
}

_registerFallback() {
if (this._fallbackRegistered) return;
this._fallbackRegistered = true;
self.addEventListener("online", () => {
this.onSync();
})
}

async _fallbackSync() {
if (self.navigator.onLine) {
this.onSync();
}
}

async registerSync(): Promise<void> {
if (!supportsSync) {
log("BG SYNC not supported calling early");
return this._fallbackSync();
}
// See https://github.com/GoogleChrome/workbox/issues/2393
try {
log("registering sync with tag", this.tag);
await self.registration.sync.register(this.tag);
} catch (err) {
if (err.message.startsWith("Permission denied")) {
// we're probably on Brave or similar
this._registerFallback();
this._fallbackSync();
} else {
throw err;
}
}
}

// use deferred here because we might be a sync event from starting up
// at which point e.waitUntil is already called
async _syncListener(event: SyncEvent, deferred: DeferredSync) {
log("sync listener", event);
this._syncInProgress = true;
deferred.$handled = true;
BackgroundSync._initSyncs.delete(this.tag);
await MODULE_LOADING?.promise;

let syncError;
try {
await this.onSync();
} catch (error) {
if (error instanceof Error) {
syncError = error;

// Rethrow the error. Note: the logic in the finally clause
// will run before this gets rethrown.
deferred.reject(syncError);
// throw syncError;
}
} finally {
// New items may have been added to the queue during the sync,
// so we need to register for a new sync if that's happened...
// Unless there was an error during the sync, in which
// case the browser will automatically retry later, as long
// as `event.lastChance` is not true.
if (this._requestsAddedDuringSync && !(syncError && !event.lastChance)) {
await this.registerSync();
}

this._syncInProgress = false;
this._requestsAddedDuringSync = false;
deferred.resolve(null);
}
}

_addSyncListener() {
if (!supportsSync) {
// If the browser doesn't support background sync
// retry every time the service worker starts up as a fallback.
return this._fallbackSync();
}

self.addEventListener("sync", (event: SyncEvent) => {
log("sync event handler called", event);
if (event.tag === this.tag) {
log("sync listener fired", event);
const deferred = defer();
this._syncListener(event, deferred);
event.waitUntil(deferred.promise);
}
});
log("SYNC LISTENER ADDED");
}

static async register(name: string) {
log("registering sync", name);
await MODULE_LOADING?.promise;
const instance = this._instances.get(name);
if (!instance) {
throw new Error("No Background sync with this name has been created");
}
instance.registerSync();
log("sync registered for", instance.tag);
}
}
8 changes: 8 additions & 0 deletions js/service-worker/worker/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Deferred } from "../../worker/types.ts";
import { defer } from "../../worker/utils/worker.ts";

export let MODULE_LOADING: null | Deferred<boolean> = null;

export function setModuleLoading() {
MODULE_LOADING ??= defer();
}
45 changes: 34 additions & 11 deletions js/service-worker/worker/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
// we'll need to request imports that we don't have
// the main app will need to register a handler for bg syncing

import type { Deferred } from "../../worker/types.ts";
import { configureSkulpt, errHandler, defer } from "../../worker/utils/worker.ts";
import { BackgroundSync, DeferredSync } from "./bg-sync.ts";
import { MODULE_LOADING, setModuleLoading } from "./constants.ts";

declare global {
interface ServiceWorkerGlobalScope {
Expand All @@ -14,6 +15,7 @@ declare global {
anvilAppOrigin: string;
onsync: any;
onperiodicsync: any;
BackgroundSync: typeof BackgroundSync;
}
}

Expand All @@ -24,10 +26,9 @@ declare const localforage: any;
// Skulpt expectes window to exist
self.window = self;
self.anvilAppOrigin = "";
let MODULE_LOADING: null | Deferred<boolean> = null;

async function loadInitModule(name: string) {
MODULE_LOADING = defer();
setModuleLoading();
MODULE_LOADING.promise.then(() => postMessage({ type: "READY" }));
await localVarStore.setItem("__main__", name);
try {
Expand Down Expand Up @@ -59,7 +60,7 @@ function addAPI() {
// can only do this one skulpt has loaded
function raise_event(args: any[], kws: any[] = []) {
if (args.length !== 1) {
throw new Sk.builtin.TypeError("Expeceted one arg to raise_event");
throw new Sk.builtin.TypeError("Expected one arg to raise_event");
}
const objectKws: any = {};
for (let i = 0; i < kws.length; i += 2) {
Expand All @@ -73,6 +74,8 @@ function addAPI() {

self.raise_event = new pyFunc(raise_event);

self.BackgroundSync = BackgroundSync;

self.sync_event_handler = (cb) => (e) => {
const wrapped = async () => {
await MODULE_LOADING?.promise;
Expand All @@ -84,7 +87,10 @@ function addAPI() {
// this can happen if trying to sync close to going offline
if (numTries < 5 && String(err).toLowerCase().includes("failed to fetch")) {
numTries++;
postMessage({ type: "OUT", message: `It looks like we're offline re-registering sync: '${e.tag}'\n` });
postMessage({
type: "OUT",
message: `It looks like we're offline re-registering sync: '${e.tag}'\n`,
});
await wait(500);
return self.registration.sync.register(e.tag);
} else {
Expand Down Expand Up @@ -126,6 +132,16 @@ async function onAppOrigin(e: any) {
}
self.addEventListener("message", onAppOrigin);

function onSyncEvent(e: any) {
const data = e.data;
const { type } = data;
if (type !== "SYNC") return;
const { tag } = data;
BackgroundSync.register(tag);
}

self.addEventListener("message", onSyncEvent);

async function postMessage(data: { type: string; [key: string]: any }) {
// flag for the client
data.ANVIL_LABS = true;
Expand Down Expand Up @@ -176,26 +192,33 @@ function resetHandler(onwhat: string, setter: any) {
*/
function initSyncCall(eventName: "sync" | "periodicsync") {
const onEvent = ("on" + eventName) as "onsync" | "onperiodicsync";
const deferred = defer();
const { set: onSet } = Object.getOwnPropertyDescriptor(self, onEvent) ?? {};
let initEvent: any;
const initEvents: any[] = [];
const initDefers: DeferredSync[] = [];

self[onEvent] = (e: any) => {
initEvent = e;
initEvents.push(e);
const deferred = defer() as DeferredSync;
initDefers.push(deferred);
BackgroundSync._initSyncs.set(e.tag, [e, deferred]);
initModule();
setTimeout(() => {
deferred.resolve("timeout");
if (!deferred.$handled) {
deferred.resolve(null);
}
}, 5000);
e.waitUntil(deferred.promise);
};

resetHandler(onEvent, async (fn: any) => {
resetHandler(onEvent, onSet);
self[onEvent] = fn;
if (!initEvent) return;
const event = initEvents.pop();
const deferred = initDefers.pop();
if (!event || !deferred) return;
try {
await MODULE_LOADING?.promise;
await fn(initEvent);
await fn(event);
deferred.resolve(null);
} catch (e) {
deferred.reject(e);
Expand Down
Loading

0 comments on commit 87dedb7

Please sign in to comment.