Skip to content

Commit

Permalink
feat: phoenix persistant storage browse impl
Browse files Browse the repository at this point in the history
  • Loading branch information
abose committed Dec 19, 2023
1 parent e3f0832 commit efbc036
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 11 deletions.
2 changes: 2 additions & 0 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,8 @@
<script src="thirdparty/bugsnag.min.js"></script>
<script src="appConfig.js"></script>
<script src="loggerSetup.js"></script>
<script src="utils/EventDispatcher.js"></script>
<script src="storage.js"></script>

<script>
window.splashScreenPresent = true;
Expand Down
152 changes: 152 additions & 0 deletions src/storage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
* Original work Copyright (c) 2012 - 2021 Adobe Systems Incorporated. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

// The main persistent storage apis used to store preferences, setting etc. This is synced across all running phoenix
// tabs in the browser and processes in the desktop app.
// Data is eventually consistent with sync delays up to a few seconds.

/*global EventDispatcher*/

function setupGlobalStorageBrowser() {

const PH_LOCAL_STORE_PREFIX = "Ph_";
const PHOENIX_STORAGE_BROADCAST_CHANNEL_NAME = "ph-storage";
const EXTERNAL_CHANGE_BROADCAST_INTERVAL = 500;
const EXTERNAL_CACHE_CLEAR_INTERVAL = 2000;
// Since this is a sync API, performance is critical. So we use a memory map in cache. This limits the size
// of what can be stored in this storage as if fully in memory.
const CHANGE_TYPE_EXTERNAL = "External",
CHANGE_TYPE_INTERNAL = "Internal";
const MGS_CHANGE = 'change';
const cache = {};
let pendingBroadcast = {},
watchExternalKeys = {},
externalWatchKeyList = [];

const storageChannel = new BroadcastChannel(PHOENIX_STORAGE_BROADCAST_CHANNEL_NAME);
function _broadcastPendingChanges() {
storageChannel.postMessage({type: MGS_CHANGE, keys: Object.keys(pendingBroadcast)});
pendingBroadcast = {};
}
setInterval(_broadcastPendingChanges, EXTERNAL_CHANGE_BROADCAST_INTERVAL);
// Listen for messages on the channel
storageChannel.onmessage = (event) => {
const message = event.data;
if(message.type === MGS_CHANGE){
for(let key of message.keys){
PhStore.trigger(key, CHANGE_TYPE_EXTERNAL);
delete cache[key]; // clear cache for changed data
}
}
};

setInterval(()=>{
for(let key of externalWatchKeyList){
delete cache[key];
}
}, EXTERNAL_CACHE_CLEAR_INTERVAL);

/**
* Retrieves the value associated with the specified key from the browser's local storage.
*
* @param {string} key - The key to retrieve the value for.
* @returns {object|null} - The value associated with the specified key. Returns null if the key does not exist.
*/
function getItem(key) {
let cachedResult = cache[key];
if(cachedResult){
return cachedResult;
}
const jsonStr = localStorage.getItem(PH_LOCAL_STORE_PREFIX + key);
if(jsonStr === null){
return null;
}
try {
cachedResult = JSON.parse(jsonStr);
cache[key] = cachedResult;
return cachedResult;
} catch (e) {
return null;
}
}

/**
* Sets the value of a specified key in the localStorage.
*
* @param {string} key - The key to set the value for.
* @param {*} value - The value to be stored. Can be any valid JSON serializable data type.
*
*/
function setItem(key, value) {
localStorage.setItem(PH_LOCAL_STORE_PREFIX + key, JSON.stringify(value));
cache[key] = value;
if(watchExternalKeys[key]){
pendingBroadcast[key] = true;
}
PhStore.trigger(key, CHANGE_TYPE_INTERNAL);
}

/**
* Enables monitoring of external changes to the specified key when there are multiple Phoenix windows/instances.
* By default, PhStore.on(<key name>, fn) only triggers for key value changes within the current instance.
* Calling this function will activate change events when changes occur in other instances as well.
* Note that there may be eventual consistent delays of up to 1 second.
*
* @param {string} key - The key to observe changes for.
*/

function watchExternalChanges(key) {
watchExternalKeys[key] = true;
externalWatchKeyList = Object.keys(watchExternalKeys);
}

/**
* If there are multiple phoenix windows/instances, This function will stop watching changes
* made by other phoenix instances for this key.
*
* @param {string} key - The key for which changes are being observed.
*/
function unwatchExternalChanges(key) {
delete watchExternalKeys[key];
externalWatchKeyList = Object.keys(watchExternalKeys);
}


const PhStore = {
getItem,
setItem,
watchExternalChanges,
unwatchExternalChanges
};
EventDispatcher.makeEventDispatcher(PhStore);
window.PhStore = PhStore;
}

function setupGlobalStorageTauri(){
// todo passing in browser storage for now. we should change this for tauri.
setupGlobalStorageBrowser();
}

if(Phoenix.browser.isTauri){
setupGlobalStorageTauri();
} else {
setupGlobalStorageBrowser();
}
32 changes: 21 additions & 11 deletions src/utils/EventDispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,24 @@
globalObject = global; //nodejs
}

function defineRequire() {
globalObject.EventDispatcher.requireDefined = true;
define(function (require, exports, module) {
exports.makeEventDispatcher = globalObject.EventDispatcher.makeEventDispatcher;
exports.triggerWithArray = globalObject.EventDispatcher.triggerWithArray;
exports.on_duringInit = globalObject.EventDispatcher.on_duringInit;
exports.markDeprecated = globalObject.EventDispatcher.markDeprecated;
exports.setLeakThresholdForEvent = globalObject.EventDispatcher.setLeakThresholdForEvent;
});
}

if(globalObject.EventDispatcher){
// already created
if(globalObject.define && !globalObject.EventDispatcher.requireDefined){
// requirejs is present, but this module is not defined. This may happen if the module was loaded twice
// - once as a normal script and once as a require js lib.
defineRequire();
}
return;
}

Expand Down Expand Up @@ -291,14 +307,14 @@
* @type {function}
*/
var trigger = function (eventName) {
var event = { type: eventName, target: this },
handlerList = this._eventHandlers && this._eventHandlers[eventName],
i;

let handlerList = this._eventHandlers && this._eventHandlers[eventName];
if (!handlerList) {
return;
}

let event = { type: eventName, target: this },
i;

// Use a clone of the list in case handlers call on()/off() while we're still in the loop
handlerList = handlerList.slice();

Expand Down Expand Up @@ -397,13 +413,7 @@

if(globalObject.define){
// for requirejs support
define(function (require, exports, module) {
exports.makeEventDispatcher = globalObject.EventDispatcher.makeEventDispatcher;
exports.triggerWithArray = globalObject.EventDispatcher.triggerWithArray;
exports.on_duringInit = globalObject.EventDispatcher.on_duringInit;
exports.markDeprecated = globalObject.EventDispatcher.markDeprecated;
exports.setLeakThresholdForEvent = globalObject.EventDispatcher.setLeakThresholdForEvent;
});
defineRequire();
}
}());

2 changes: 2 additions & 0 deletions test/SpecRunner.html
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@

<!-- Import the phoenix browser virtual file system -->
<script src="../src/phoenix/virtualfs.js"></script>
<script src="../src/utils/EventDispatcher.js"></script>
<script src="../src/storage.js"></script>

<script>
// environment setup for boot. do not move out of index html!!
Expand Down

0 comments on commit efbc036

Please sign in to comment.