Skip to content

Commit

Permalink
Official event Dexie.on('storagemutated') (#1384)
Browse files Browse the repository at this point in the history
Official event Dexie.on('storagemutated').

This change is a step to making dexie@3.2.0 an officially supported stable release where the observability communication channel can be official and documented.

The event was previously undocumented and had the name Dexie.on('txcommitted'). The event is the channel that communicates changes on indexeddb databases to enable `liveQuery()` to detect changes across service worker, windows and tabs. Event is propagated across peers using BroadcastChannel on browsers supporting it, and "storage" event on IE and Safari.

The commit does not break any officially documented Dexie API but as the rename of the "txcommitted" event to "storagemutated" and its corresponding event names in BroadcastChannel land localStorage ("x-storagemutated-1") are changed, code that would have been using those undocumented event names will need to be updated.

This commit also improves observability in a rare use case when "dexie" module is imported multiple times (possibly with different versions of the package) as it also propagates on window/self event target and not just Dexie static.
  • Loading branch information
dfahlander authored Sep 3, 2021
1 parent b6a1236 commit 0f2c780
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 53 deletions.
2 changes: 1 addition & 1 deletion src/classes/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class Transaction implements ITransaction {
this.active = false;
this._resolve();
if ('mutatedParts' in idbtrans) {
globalEvents.txcommitted.fire(idbtrans["mutatedParts"]);
globalEvents.storagemutated.fire(idbtrans["mutatedParts"]);
}
});
return this;
Expand Down
17 changes: 16 additions & 1 deletion src/globals/global-events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
import Events from '../helpers/Events';
import { GlobalDexieEvents } from '../public/types/db-events';

export const globalEvents = Events(null, "txcommitted") as GlobalDexieEvents;
export const DEXIE_STORAGE_MUTATED_EVENT_NAME = 'storagemutated' as 'storagemutated';

// Name of the global event fired using DOM dispatchEvent (if not in node).
// Reason for propagating this as a DOM event is for getting reactivity across
// multiple versions of Dexie within the same app (as long as they are
// compatible with regards to the event data).
// If the ObservabilitySet protocol change in a way that would not be backward
// compatible, make sure also update the event name to a new number at the end
// so that two Dexie instances of different versions continue to work together
// - maybe not able to communicate but won't fail due to unexpected data in
// the detail property of the CustomEvent. If so, also make sure to udpate
// docs and explain at which Dexie version the new name and format of the event
// is being used.
export const STORAGE_MUTATED_DOM_EVENT_NAME = 'x-storagemutated-1';

export const globalEvents = Events(null, DEXIE_STORAGE_MUTATED_EVENT_NAME) as GlobalDexieEvents;
41 changes: 22 additions & 19 deletions src/live-query/enable-broadcast.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { globalEvents } from "../globals/global-events";
import { propagateLocally, propagatingLocally } from "./propagate-locally";
import {
globalEvents,
STORAGE_MUTATED_DOM_EVENT_NAME,
DEXIE_STORAGE_MUTATED_EVENT_NAME,
} from '../globals/global-events';
import { propagateLocally, propagatingLocally } from './propagate-locally';

if (typeof BroadcastChannel !== "undefined") {
const bc = new BroadcastChannel("dexie-txcommitted");
if (typeof BroadcastChannel !== 'undefined') {
const bc = new BroadcastChannel(STORAGE_MUTATED_DOM_EVENT_NAME);

//
// Propagate local changes to remote tabs, windows and workers via BroadcastChannel
//
globalEvents("txcommitted", (changedParts) => {
globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, (changedParts) => {
if (!propagatingLocally) {
bc.postMessage(changedParts);
}
Expand All @@ -19,51 +23,50 @@ if (typeof BroadcastChannel !== "undefined") {
bc.onmessage = (ev) => {
if (ev.data) propagateLocally(ev.data);
};
} else if (typeof self !== "undefined" && typeof navigator !== "undefined") {
} else if (typeof self !== 'undefined' && typeof navigator !== 'undefined') {
// DOM verified - when typeof self !== "undefined", we are a window or worker. Not a Node process.

//
// Propagate local changes to remote tabs/windows via storage event and service worker
// via messages. We have this code here because of https://bugs.webkit.org/show_bug.cgi?id=161472.
//
globalEvents("txcommitted", (changedParts) => {
globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, (changedParts) => {
try {
if (!propagatingLocally) {
if (typeof localStorage !== "undefined") {
if (typeof localStorage !== 'undefined') {
// We're a browsing window or tab. Propagate to other windows/tabs via storage event:
localStorage.setItem(
"dexie-txcommitted",
STORAGE_MUTATED_DOM_EVENT_NAME,
JSON.stringify({
trig: Math.random(),
changedParts,
})
);
}
if (typeof self["clients"] === "object") {
if (typeof self['clients'] === 'object') {
// We're a service worker. Propagate to our browser clients.
[...self["clients"].matchAll({ includeUncontrolled: true })].forEach(
[...self['clients'].matchAll({ includeUncontrolled: true })].forEach(
(client) =>
client.postMessage({
type: "dexie-txcommitted",
type: STORAGE_MUTATED_DOM_EVENT_NAME,
changedParts,
})
);
}
}
}
} catch {}
});

//
// Propagate remote changes locally via storage event:
//
addEventListener("storage", (ev: StorageEvent) => {
if (ev.key === "dexie-txcommitted") {
addEventListener('storage', (ev: StorageEvent) => {
if (ev.key === STORAGE_MUTATED_DOM_EVENT_NAME) {
const data = JSON.parse(ev.newValue);
if (data) propagateLocally(data.changedParts);
}
});


//
// Propagate messages from service worker
//
Expand All @@ -74,8 +77,8 @@ if (typeof BroadcastChannel !== "undefined") {
}
}

function propagateMessageLocally({data}: MessageEvent) {
if (data && data.type === "dexie-txcommitted") {
function propagateMessageLocally({ data }: MessageEvent) {
if (data && data.type === STORAGE_MUTATED_DOM_EVENT_NAME) {
propagateLocally(data.changedParts);
}
}
6 changes: 3 additions & 3 deletions src/live-query/live-query.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isAsyncFunction, keys } from "../functions/utils";
import { globalEvents } from "../globals/global-events";
import { globalEvents, DEXIE_STORAGE_MUTATED_EVENT_NAME } from "../globals/global-events";
import {
decrementExpectedAwaits,
incrementExpectedAwaits,
Expand Down Expand Up @@ -48,7 +48,7 @@ export function liveQuery<T>(querier: () => T | Promise<T>): IObservable<T> {
},
unsubscribe: () => {
closed = true;
globalEvents.txcommitted.unsubscribe(mutationListener);
globalEvents.storagemutated.unsubscribe(mutationListener);
},
};

Expand Down Expand Up @@ -77,7 +77,7 @@ export function liveQuery<T>(querier: () => T | Promise<T>): IObservable<T> {
const subscr: ObservabilitySet = {};
const ret = execute(subscr);
if (!startedListening) {
globalEvents("txcommitted", mutationListener);
globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, mutationListener);
startedListening = true;
}
querying = true;
Expand Down
54 changes: 28 additions & 26 deletions src/live-query/propagate-locally.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,39 @@
import { globalEvents } from '../globals/global-events';
import { isIEOrEdge } from '../globals/constants';
import { globalEvents, DEXIE_STORAGE_MUTATED_EVENT_NAME, STORAGE_MUTATED_DOM_EVENT_NAME } from '../globals/global-events';
import { ObservabilitySet } from "../public/types/db-events";
import { extendObservabilitySet } from './extend-observability-set';

function fireLocally(updateParts: ObservabilitySet) {
if (typeof dispatchEvent !== 'undefined' && typeof addEventListener !== 'undefined') {
globalEvents(DEXIE_STORAGE_MUTATED_EVENT_NAME, updatedParts => {
if (!propagatingLocally) {
let event: CustomEvent<ObservabilitySet>;
if (isIEOrEdge) {
event = document.createEvent('CustomEvent');
event.initCustomEvent(STORAGE_MUTATED_DOM_EVENT_NAME, true, true, updatedParts);
} else {
event = new CustomEvent(STORAGE_MUTATED_DOM_EVENT_NAME, {
detail: updatedParts
});
}
propagatingLocally = true;
dispatchEvent(event);
propagatingLocally = false;
}
});
addEventListener(STORAGE_MUTATED_DOM_EVENT_NAME, ({detail}: CustomEvent<ObservabilitySet>) => {
if (!propagatingLocally) {
propagateLocally(detail);
}
});
}

export function propagateLocally(updateParts: ObservabilitySet) {
let wasMe = propagatingLocally;
try {
propagatingLocally = true;
globalEvents.txcommitted.fire(updateParts);
globalEvents.storagemutated.fire(updateParts);
} finally {
propagatingLocally = wasMe;
}
}

export let propagateLocally = fireLocally;
export let propagatingLocally = false;
let accumulatedParts: ObservabilitySet = {};

if (typeof document !== 'undefined' && document.addEventListener) {
// If our tab becomes open, trigger all the collected changes
const fireIfVisible = () => {
// Only trigger the event if our tab is open:
if (document.visibilityState === "visible") {
if (Object.keys(accumulatedParts).length > 0) {
fireLocally(accumulatedParts);
}
accumulatedParts = {};
}
};

document.addEventListener("visibilitychange", fireIfVisible);

propagateLocally = (changedParts: ObservabilitySet) => {
extendObservabilitySet(accumulatedParts, changedParts);
fireIfVisible();
}
}
6 changes: 3 additions & 3 deletions src/public/types/db-events.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ export type ObservabilitySet = {
[part: string]: IntervalTree;
};

export interface DexieOnTxCommittedEvent {
export interface DexieOnStorageMutatedEvent {
subscribe(fn: (parts: ObservabilitySet) => any): void;
unsubscribe(fn: (parts: ObservabilitySet) => any): void;
fire(parts: ObservabilitySet): any;
}

export interface GlobalDexieEvents extends DexieEventSet {
(eventName: 'txcommitted', subscriber: (parts: ObservabilitySet) => any): void;
txcommitted: DexieOnTxCommittedEvent;
(eventName: 'storagemutated', subscriber: (parts: ObservabilitySet) => any): void;
storagemutated: DexieOnStorageMutatedEvent;
}

0 comments on commit 0f2c780

Please sign in to comment.