Skip to content
This repository was archived by the owner on Sep 23, 2022. It is now read-only.

Commit a3e087f

Browse files
committed
Move app business logic to workers, add WorkerProxy (#32)
1 parent ba28d5f commit a3e087f

File tree

5 files changed

+242
-172
lines changed

5 files changed

+242
-172
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
# FlareTail.js
22

3-
A JavaScript library for Firefox OS application development, consisting of WAI-ARIA-driven accessible widgets, a lightweight app framework, and convenient utility functions.
3+
A JavaScript library for Firefox OS application development, consisting of WAI-ARIA-driven accessible widgets, a lightweight app framework featuring a multithreaded MVC pattern, and convenient utility functions.
44

55
This is the core of [BzDeck](https://github.com/bzdeck/bzdeck) and not intended for general use at this time.
66

77
* **widgets.js**: A collection of [WAI-ARIA](http://www.w3.org/TR/wai-aria/)-driven accessible widgets. This will use [Custom Elements](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Custom_Elements) once the feature is implemented by Firefox.
8-
* **app.js**: A lightweight app framework featuring a MVC pattern, router and events.
8+
* **app.main.js**: App frontend implementation on the main thread containing View, Helper, Controller, Router, Events and WorkerProxy.
9+
* **app.worker.js**: App business logic implementation on the service worker containing DataSource, Model and Collection.
910
* **helpers.js**: Convenient utility functions handling a variety of stuff for an application, including a [Microdata](http://www.w3.org/TR/microdata/)-based HTML5 template engine, keybinding support and date formatter.

scripts/app.main.js

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
/**
6+
* Declare the FlareTail.js namespace.
7+
* @namespace
8+
*/
9+
var FlareTail = FlareTail || {};
10+
11+
/**
12+
* Provide a lightweight application framework.
13+
*/
14+
FlareTail.app = {};
15+
16+
/*
17+
* Provide app router functionalities. The routes can be defined on the app's controllers using regular expressions,
18+
* e.g. BzDeck.controllers.DetailsPage.route = '/bug/(\\d+)';
19+
*/
20+
FlareTail.app.Router = class Router {
21+
/**
22+
* Get a Router instance.
23+
* @constructor
24+
* @argument {Object} app - App namespace containing configurations and controllers.
25+
* @return {Object} router
26+
*/
27+
constructor (app) {
28+
// Specify the base URL of the app, without a trailing slash
29+
this.root = app.config.app.root.match(/(.*)\/$/)[1] || '';
30+
// Specify the launch path
31+
this.launch_path = app.config.app.launch_path || app.config.app.root || '/';
32+
// Specify the routes
33+
this.routes = new Map();
34+
35+
// Retrieve the routes from app controllers
36+
for (let [name, component] of Object.entries(app)) {
37+
if (name.match(/.+Controller$/) && 'route' in component.prototype) {
38+
this.routes.set(new RegExp(`^${this.root}${component.prototype.route}$`), component);
39+
}
40+
}
41+
42+
window.addEventListener('popstate', event => this.locate());
43+
}
44+
45+
/**
46+
* Find a route usually by the URL. If found, create a new instance of the corresponding controller. If not found, the
47+
* specified pathname is invalid, so nativate to the app's launch path instead.
48+
* @argument {String} [path=location.pathname] - URL pathname used to find a route.
49+
* @return {Boolean} result - Whether a route is found.
50+
*/
51+
locate (path = location.pathname) {
52+
for (let [re, constructor] of this.routes) {
53+
let match = path.match(re);
54+
55+
if (match) {
56+
// Call the constructor when a route is found
57+
// Pass arguments based on the RegExp pattern, taking numeric arguments into account
58+
new constructor(...match.slice(1).map(arg => isNaN(arg) ? arg : Number(arg)));
59+
60+
return true;
61+
}
62+
}
63+
64+
// Couldn't find a route; go to the launch path
65+
this.navigate(this.launch_path);
66+
67+
return false;
68+
}
69+
70+
/**
71+
* Navigate to the specified URL pathname by manipulating the browser history.
72+
* @argument {String} path - URL pathname to go.
73+
* @argument {Object} [state={}] - History state object.
74+
* @argument {Boolean} [replace=false] - If true, the current history state will be replaced, otherwise appended.
75+
* @return {undefined}
76+
*/
77+
navigate (path, state = {}, replace = false) {
78+
state.previous = replace && history.state && history.state.previous ? history.state.previous : location.pathname;
79+
80+
let args = [state, 'Loading...', this.root + path]; // l10n
81+
82+
replace ? history.replaceState(...args) : history.pushState(...args);
83+
window.dispatchEvent(new PopStateEvent('popstate'));
84+
85+
if (FlareTail.debug) {
86+
console.info(replace ? 'History replaced:' : 'History added:', path, state);
87+
}
88+
}
89+
}
90+
91+
/**
92+
* Provide app event functionalities.
93+
*/
94+
FlareTail.app.Events = class Events {
95+
/**
96+
* Publish an event asynchronously on a separate thread.
97+
* @argument {String} topic - An event name. Shorthand syntax is supported: :Updated in BugModel means
98+
* BugModel:Updated, :Error in SessionController means SessionController:Error, and so on.
99+
* @argument {Object} [data={}] - Data to pass the subscribers. If the instance has set the id property, that id will
100+
* be automatically appended to the data.
101+
* @return {undefined}
102+
*/
103+
trigger (topic, data = {}) {
104+
if (topic.match(/^:/)) {
105+
topic = this.constructor.name + topic;
106+
}
107+
108+
let id = this.id;
109+
110+
if (FlareTail.debug) {
111+
console.info('Event triggered:', topic, id || '(global)', data);
112+
}
113+
114+
this.helpers.event.trigger(window, topic, { detail: { id, data }});
115+
}
116+
117+
/**
118+
* Subscribe an event.
119+
* @argument {String} topic - Event name. Shorthand syntax is supported: M:Updated in BugView means BugModel:Updated,
120+
* V:AppMenuItemSelected in ToolbarController means ToolbarView:AppMenuItemSelected, and so on.
121+
* @argument {Function} callback - Function called whenever the specified event is fired.
122+
* @argument {Boolean} [global=false] - If true, the callback function will be fired even when the event detail object
123+
* and the instance have different id properties. Otherwise, the identity will be respected.
124+
* @return {undefined}
125+
*/
126+
on (topic, callback, global = false) {
127+
topic = topic.replace(/^([MVC]):/, (match, prefix) => {
128+
return this.constructor.name.match(/(.*)(Model|View|Controller)$/)[1]
129+
+ { M: 'Model', V: 'View', C: 'Controller' }[prefix] + ':';
130+
});
131+
132+
window.addEventListener(topic, event => {
133+
if (!global && event.detail && event.detail.id && this.id && event.detail.id !== this.id) {
134+
return false;
135+
}
136+
137+
callback(event.detail.data);
138+
139+
return true;
140+
});
141+
}
142+
143+
/**
144+
* Subscribe an event with an automatically determined callback. So this is the 'on' function's shorthand. For
145+
* example, if the topic is 'V:NavigationRequested', on_navigation_requested will be set as the callback function.
146+
* @argument {String} topic - See the 'on' function above for details.
147+
* @argument {Boolean} [global=false] - See the 'on' function above for details.
148+
* @return {undefined}
149+
*/
150+
subscribe (topic, global = false) {
151+
this.on(topic, data => this[topic.replace(/^.+?\:/, 'on').replace(/([A-Z])/g, '_$1').toLowerCase()](data), global);
152+
}
153+
}
154+
155+
FlareTail.app.Events.prototype.helpers = FlareTail.helpers,
156+
157+
/**
158+
* Provide app view functionalities.
159+
* @extends FlareTail.app.Events
160+
*/
161+
FlareTail.app.View = class View extends FlareTail.app.Events {}
162+
163+
FlareTail.app.View.prototype.get_fragment = FlareTail.helpers.content.get_fragment;
164+
FlareTail.app.View.prototype.get_template = FlareTail.helpers.content.get_template;
165+
FlareTail.app.View.prototype.fill = FlareTail.helpers.content.fill;
166+
FlareTail.app.View.prototype.widgets = FlareTail.widgets;
167+
168+
/**
169+
* Provide app helper functionalities.
170+
* @extends FlareTail.app.View
171+
*/
172+
FlareTail.app.Helper = class Helper extends FlareTail.app.View {}
173+
174+
/**
175+
* Provide app controller functionalities.
176+
* @extends FlareTail.app.Events
177+
*/
178+
FlareTail.app.Controller = class Controller extends FlareTail.app.Events {}
179+
180+
/**
181+
* Provide worker proxy functionalities. This typically offers the frontend catch-all mechanism for backend Collections,
182+
* forwarding all method calls from the main thread to the service worker and receiving the results from the worker.
183+
* This allows Controllers to seamlessly access Collections.
184+
*/
185+
FlareTail.app.WorkerProxy = class WorkerProxy {
186+
/**
187+
* Get a WorkerProxy instance.
188+
* @constructor
189+
* @argument {String} class_name - Name of the target class on the worker.
190+
* @return {Proxy} proxy - The catch-all magic mechanism. Functions on this proxy will return a Promise.
191+
*/
192+
constructor (class_name) {
193+
return new Proxy({}, {
194+
get: (obj, func_name) => new Proxy(() => {}, {
195+
apply: (_obj, _this, args) => new Promise(resolve => {
196+
let listener = event => {
197+
let [ service, type, detail ] = event.data;
198+
199+
if (service === class_name && type === func_name) {
200+
if (FlareTail.debug) {
201+
console.info('[WorkerProxy] received message:', class_name, func_name, detail);
202+
}
203+
204+
navigator.serviceWorker.removeEventListener('message', listener);
205+
resolve(detail);
206+
}
207+
};
208+
209+
if (FlareTail.debug) {
210+
console.info('[WorkerProxy] sent message:', class_name, func_name, args);
211+
}
212+
213+
navigator.serviceWorker.addEventListener('message', listener);
214+
navigator.serviceWorker.ready.then(reg => reg.active.postMessage([class_name, func_name, args]));
215+
}),
216+
}),
217+
});
218+
}
219+
}

0 commit comments

Comments
 (0)