Skip to content
Permalink
cf459d473a
Switch branches/tags
Go to file
@FremyCompany
Latest commit 062ecbc Dec 3, 2013 History
3 contributors

Users who have contributed to this file

@slightlyoff @jakearchibald @FremyCompany
632 lines (549 sloc) 20.7 KB
// This API proposal depends on:
// DOM: http://www.w3.org/TR/domcore/
// URLs: http://url.spec.whatwg.org/
// Promises: https://github.com/slightlyoff/DOMPromise/
// Shared Workers:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html#shared-workers
////////////////////////////////////////////////////////////////////////////////
// Document APIs
////////////////////////////////////////////////////////////////////////////////
// extensions to window.navigator
interface NavigatorServiceWorker {
// null if page has no activated worker
serviceWorker: SharedServiceWorker;
registerServiceWorker(scope: string/* or URL */, url: string/* or URL */): Promise;
// If an event worker is in-waiting, and its url & scope matches both
// url & scope
// - resolve the promise
// - abort these steps
// If no worker is in-waiting, and the current active event worker's
// url & scope matches url & scope
// - resolve the promise
// - abort these steps
// If an event worker with the same scope & url is attempting registration
// - resolve the promise depending on the current registration attempt
// - abort these steps
// Reject the promise if:
// - the URL is not same-origin
// - no scope is provided or the scope does not resolve/parse correctly
// - fetching the event worker returns with an error
// - installing the worker (the event the worker is sent) fails
// with an unhandled exception
// TBD: possible error values upon rejection!
//
// Resolves once the install event is triggered without unhandled exceptions
unregisterServiceWorker(scope: string): Promise;
// TODO: if we have a worker-in-waiting & an active worker,
// what happens? Both removed?
// TODO: does removal happen immediately or using the same pattern as
// a worker update?
// called when a new worker becomes in-waiting
onserviceworkerinstall: (ev: Event) => any;
// TODO: needs custom event type?
// TODO: is this actually useful? Can't simply reload due to other tabs
// called when a new worker takes over via InstallEvent#replace
onserviceworkerreplaced: (ev: Event) => any;
// TODO: needs custom event type?
// TODO: is this actually useful? Might want to force a reload at this point
onserviceworkerreloadpage: (ev: ReloadPageEvent) => any;
// TODO: this event name has gotten way too long
}
interface Navigator extends
NavigatorServiceWorker,
EventTarget,
// the rest is just stuff from the default ts definition
NavigatorID,
NavigatorOnLine,
// NavigatorDoNotTrack,
// NavigatorAbilities,
NavigatorGeolocation
// MSNavigatorAbilities
{ }
interface SharedServiceWorker extends Worker {}
declare var SharedServiceWorker: {
prototype: SharedServiceWorker;
}
class ReloadPageEvent extends _Event {
// Delay the page unload to serialise state to storage or get user's permission
// to reload.
waitUntil(f: Promise): void {}
}
///////////////////////////////////////////////////////////////////////////////
// The Event Worker
///////////////////////////////////////////////////////////////////////////////
class InstallPhaseEvent extends _Event {
previousVersion: any = 0;
// Delay treating the installing worker until the passed Promise resolves
// successfully. This is primarily used to ensure that an ServiceWorker is not
// active until all of the "core" Caches it depends on are populated.
// TODO: what does the returned promise do differently to the one passed in?
waitUntil(f: Promise): Promise { return accepted(); }
}
class InstallEvent extends InstallPhaseEvent {
previous: MessagePort = null;
// Ensures that the worker is used in place of existing workers for
// the currently controlled set of window instances.
// TODO: how does this interact with waitUntil? Does it automatically wait?
replace(): void {}
// Assists in restarting all windows with the new worker.
//
// Return a new Promise
// For each attached window:
// Trigger onserviceworkerreloadpage
// If onserviceworkerreloadpage has default prevented:
// Unfreeze any frozen windows
// reject returned promise
// abort these steps
// If waitUntil called on onserviceworkerreloadpage event:
// frozen windows may wish to indicate which window they're blocked on
// yeild until promise passed into waitUntil resolves
// if waitUntil promise is accepted:
// freeze window (ui may wish to grey it out)
// else:
// Unfreeze any frozen windows
// reject returned promise
// abort these steps
// Else:
// freeze window (ui may wish to grey it out)
// Unload all windows
// If any window fails to unload, eg via onbeforeunload:
// Unfreeze any frozen windows
// reject returned promise
// abort these steps
// Close all connections between the old worker and windows
// Activate the new worker
// Reload all windows asynchronously
// Resolve promise
reloadAll(): Promise {
return new Promise(function() {
});
}
}
interface InstallEventHandler { (e:InstallEvent); }
interface ActivateEventHandler { (e:InstallPhaseEvent); }
interface FetchEventHandler { (e:FetchEvent); }
// FIXME: need custom event types!
interface BeforeCacheEvictionEventHandler { (e:_Event); }
interface CacheEvictionEventHandler { (e:_Event); }
interface OnlineEventHandler { (e:_Event); }
interface OfflineEventHandler { (e:_Event); }
// The scope in which worker code is executed
class ServiceWorkerScope extends SharedWorker {
// Mirrors navigator.onLine. We also get network status change events
// (ononline, etc.). The proposed ping() API must be made available here as
// well.
onLine: boolean;
caches: CacheList;
get windows(): WindowList {
return new WindowList();
}
// Set by the worker and used to communicate to newer versions what they
// are replaceing (see InstallEvent::previousVersion)
version: any = 0; // NOTE: versions must be structured-cloneable!
//
// Events
//
// Legacy Events
// These mirror window.online and window.offline in browsing contexts.
online: OnlineEventHandler;
offline: OfflineEventHandler;
// New Events
// Called when a worker is downloaded and being setup to handle
// events (navigations, alerts, etc.)
oninstall: InstallEventHandler;
// Called when a worker becomes the active event worker for a mapping
onactivate: ActivateEventHandler;
// Called whenever this worker is meant to decide the disposition of a
// request.
onfetch: FetchEventHandler;
onbeforeevicted: BeforeCacheEvictionEventHandler;
onevicted: CacheEvictionEventHandler;
// FIXME(slightlyoff): Need to add flags for:
// - custom "accept/reject" handling, perhaps with global config
// - flag to consult the HTTP cache first?
fetch(request:Request);
fetch(request:URL); // a URL
fetch(request:string); // a URL
fetch(request:any) : Promise {
return new Promise(function(r) {
r.resolve(_defaultToBrowserHTTP(request));
});
}
}
///////////////////////////////////////////////////////////////////////////////
// Event Worker APIs
///////////////////////////////////////////////////////////////////////////////
// http://fetch.spec.whatwg.org/#requests
class Request {
constructor(params?) {
if (params) {
if (typeof params.timeout != "undefined") {
this.timeout = params.timeout;
}
if (typeof params.url != "undefined") {
// should be "new URL(params.url)" but TS won't allow it
this.url = params.url;
}
if (typeof params.synchronous != "undefined") {
this.synchronous = params.synchronous;
}
if (typeof params.encoding != "undefined") {
this.encoding = params.encoding;
}
if (typeof params.forcePreflight != "undefined") {
this.forcePreflight = params.forcePreflight;
}
if (typeof params.omitCredentials != "undefined") {
this.omitCredentials = params.omitCredentials;
}
if (typeof params.method != "undefined") {
this.method = params.method;
}
if (typeof params.headers != "undefined") {
this.headers = params.headers;
}
if (typeof params.body != "undefined") {
this.body = params.body;
}
}
}
encoding: string;
// see: http://www.w3.org/TR/XMLHttpRequest/#the-timeout-attribute
timeout: Number = 0;
url: URL;
method: string = "GET";
origin: string;
// FIXME: mode doesn't seem useful here.
mode: string; // Can be one of "same origin", "tainted x-origin", "CORS"
// FIXME: we don't provide anything but async fetching...
synchronous: boolean = false;
redirectCount: Number = 0;
forcePreflight: boolean = false;
omitCredentials: boolean = false;
referrer: URL;
headers: Map<string, string>; // Needs filtering!
body: any; /*TypedArray? String?*/
}
// http://fetch.spec.whatwg.org/#responses
class Response {
constructor() {}
}
class CrossOriginResponse extends Response {
// This class represents the result of cross-origin fetched resources that are
// tainted, e.g. <img src="http://cross-origin.example/test.png">
// TODO: slightlyoff: make CORS headers readable but not setable?
}
class SameOriginResponse extends Response {
constructor(params?) {
if (params) {
if (typeof params.statusCode != "undefined") {
this.statusCode = params.statusCode;
}
if (typeof params.statusText != "undefined") {
this.statusText = params.statusText;
}
if (typeof params.encoding != "undefined") {
this.encoding = params.encoding;
}
if (typeof params.method != "undefined") {
this.method = params.method;
}
if (typeof params.headers != "undefined") {
this.headers = params.headers;
}
if (typeof params.body != "undefined") {
this.body = params.body;
}
}
super();
}
// This class represents the result of all other fetched resources, including
// cross-origin fetched resources using the CORS fetching mode.
statusCode: Number;
statusText: string;
// Explicitly omitting httpVersion
encoding: string;
method: string;
// NOTE: the internal "_headers" is not meant to be exposed. Only here for
// pseudo-impl purposes.
_headers: Map<string, string>; // FIXME: Needs filtering!
get headers() {
return this._headers;
}
set headers(items) {
if (items instanceof Map) {
items.forEach((value, key, map) => this._headers.set(key, value))
} else {
// Enumerate own properties and treat them as key/value pairs
for (var x in items) {
(function(x) {
if (items.hasOwnProperty(x)) { this._headers.set(x, items[x]); }
}).call(this, x);
}
}
}
body: any; /*TypedArray? String?*/
}
class ResponsePromise extends Promise {}
class RequestPromise extends Promise {}
class FetchEvent extends _Event {
// The body of the request.
request: Request;
// Can be one of:
// "navigate"
// "fetch"
type: string = "navigate";
// The window issuing the request.
window: any; // FIXME: should this also have an ID for easier use in ES5?
// Does the request correspond to navigation of the top-level window, e.g.
// reloading a tab or typing a URL into the URL bar?
isTopLevel: boolean = false;
// Does the navigation or fetch come from a document that has been "soft
// reloaded"? That is to say, the reload button in the URL bar or the
// back/forward buttons in browser chrome? If true, some apps may choose not
// to work so hard to get "fresh" content to display.
isReload: boolean = false;
// Promise must resolve with a Response. A Network Error is thrown for other
// resolution types/values.
respondWith(r: Promise) : void;
respondWith(r: Response) : void;
// "any" to make the TS compiler happy:
respondWith(r: any) : void {
if (!(r instanceof Response) || !(r instanceof Promise)) {
throw new Error("Faux NetworkError because DOM is currently b0rken");
}
this.stopImmediatePropagation();
if (r instanceof Response) {
r = new Promise(function(resolver) { resolver.resolve(r); });
}
r.then(_useWorkerResponse,
_defaultToBrowserHTTP);
}
forwardTo(url: URL) : Promise;
forwardTo(url: string) : Promise;
// "any" to make the TS compiler happy:
forwardTo(url: any) : Promise {
if (!(url instanceof _URL) || typeof url != "string") {
// Should *actually* be a DOMException.NETWORK_ERR
// Can't be today because DOMException isn't currently constructable
throw new Error("Faux NetworkError because DOM is currently b0rken");
}
this.stopImmediatePropagation();
return new Promise(function(resolver){
resolver.resolve(new SameOriginResponse({
statusCode: 302,
headers: { "Location": url.toString() }
}));
});
}
constructor() {
super("fetch", { cancelable: true, bubbles: false });
// This is the meat of the API for most use-cases.
// If preventDefault() is not called on the event, the request is sent to
// the default browser worker. That is to say, to respond with something
// from the cache, you must preventDefault() and respond with it manually,
// digging the resource out of the cache and calling
// evt.respondWith(cachedItem).
//
// Note:
// while preventDefault() must be called synchronously to cancel the
// default, responding does not need to be synchronous. That is to say,
// you can do something async (like fetch contents, go to IDB, whatever)
// within whatever the network time out is and as long as you still have
// the FetchEvent instance, you can fulfill the request later.
this.window = null; // to allow postMessage, window.topLevel, etc
}
}
// Design notes:
// - Caches are atomic: they are not complete until all of their resources are
// fetched
// - Updates are also atomic: the old contents are visible until all new
// contents are fetched/installed.
// - Caches should have version numbers and "update" should set/replace it
// This largely describes the current Application Cache API. It's only available
// inside worker instances (not in regular documents), meaning that caching is a
// feature of the event worker. This is likely to change!
class Cache {
items: AsyncMap;
// Allow arrays of URLs or strings
constructor(...urls:URL[]);
constructor(...urls:string[]);
// "any" to make the TS compiler happy:
constructor(...urls:any[]) {
// Note that items may ONLY contain Response instasnces
if (urls.length) {
// Begin fetching on the URLs and storing them in this.items
}
}
// Match a URL or a string
match(name:URL) : Promise;
match(name:string) : Promise;
// "any" to make the TS compiler happy:
match(name:any) : Promise {
// name matches something in items
if (name) {
return this.items.get(name.toString());
}
}
// TODO: define type-restricting getters/setters
// Cribbed from Mozilla's proposal, but with sane returns
add(...response:string[]) : Promise;
add(...response:URL[]) : Promise;
// "any" to make the TS compiler happy:
add(...response:any[]) : Promise {
// If a URL (or URL string) is passed, a new CachedResponse is added to
// items upon successful fetching
return accepted();
}
// Needed because Response objects don't have URLs.
addResponse(url, response:Response) : Promise {
return accepted();
}
remove(...response:string[]) : Promise;
remove(...response:URL[]) : Promise;
// "any" to make the TS compiler happy:
remove(...response:any[]) : Promise {
// FIXME: does this need to be async?
return accepted();
}
// For the below, see current AppCache, although we extend with sane returns
// Update has the effect of checking the HTTP cache validity of all items
// currently in the cache and updating with new versions if the current item
// is expired. New items may be added to the cache with the urls that can be
// passed. The HTTP cache is currently used for these resources but no
// heuristic caching is applied for these requests.
update(...urls:URL[]) : Promise;
update(...urls:string[]) : Promise { return accepted(); }
ready(): Promise { return accepted(); }
}
class CacheList implements Map<string, any> {
constructor(iterable: Array) { }
// Convenience method to get ResponsePromise from named cache.
match(cacheName: String, url: URL) : ResponsePromise;
match(cacheName: String, url: String) : ResponsePromise;
// "any" to make the TS compiler happy
match(cacheName: any, url: any) : ResponsePromise {
return new ResponsePromise(function(){});
}
// interface Map<any, any>
get(key: any): any {}
has(key: any): boolean { return true; }
set(key: any, val: any): Map<any, any> { return this; }
clear(): void {}
delete(key: any): boolean { return true; }
forEach(callback: Function, thisArg?: Object): void {}
items(): any[] { return []; }
keys(): any[] { return []; }
values(): any[] { return []; }
get size(): number { return 0; }
}
////////////////////////////////////////////////////////////////////////////////
// Utility Decls to make the TypeScript compiler happy
////////////////////////////////////////////////////////////////////////////////
// Cause, you know, the stock definition claims that URL isn't a class. FML.
class _URL {
constructor(url, base) {}
}
// http://tc39wiki.calculist.org/es6/map-set/
// http://wiki.ecmascript.org/doku.php?id=harmony:simple_maps_and_sets
// http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts
// http://people.mozilla.org/~jorendorff/es6-draft.html#sec-15.14.4
/*
class Map {
constructor(iterable?:any[]) {}
get(key: any): any {}
has(key: any): boolean { return true; }
set(key: any, val: any): void {}
clear(): void {}
delete(key: any): boolean { return true; }
forEach(callback: Function, thisArg?: Object): void {}
items(): any[] { return []; }
keys(): any[] { return []; }
values(): any[] { return []; }
}
*/
// https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#interface-event
interface EventHandler { (e:_Event); }
interface _EventInit {
bubbles: boolean;
cancelable: boolean;
}
// the TS compiler is unhappy *both* with re-defining DOM types and with direct
// sublassing of most of them. This is sane (from a regular TS pespective), if
// frustrating. As a result, we describe the built-in Event type with a prefixed
// name so that we can subclass it later.
class _Event {
type: string;
target: any;
currentTarget: any;
eventPhase: Number;
bubbles: boolean = false;
cancelable: boolean = true;
defaultPrevented: boolean = false;
isTrusted: boolean = false;
timeStamp: Number;
stopPropagation(): void {}
stopImmediatePropagation(): void {}
preventDefault(): void {}
constructor(type : string, eventInitDict?: _EventInit) {}
}
class _CustomEvent extends _Event {
// Constructor(DOMString type, optional EventInit eventInitDict
constructor(type : string, eventInitDict?: _EventInit) {
super(type, eventInitDict);
}
}
class _EventTarget {
dispatchEvent(e:_Event): boolean {
return true;
}
}
// https://github.com/slightlyoff/DOMPromise/blob/master/DOMPromise.idl
class Resolver {
public accept(v:any): void {}
public reject(v:any): void {}
public resolve(v:any): void {}
}
class Promise {
// Callback type decl:
// callback : (n : number) => number
constructor(init : (r:Resolver) => void ) {
}
}
function accepted() : Promise {
return new Promise(function(r) {
r.accept(true);
});
}
function acceptedResponse() : ResponsePromise {
return new ResponsePromise(function(r) {
r.accept(new Response());
});
}
interface ConnectEventHandler { (e:_Event); }
// http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.html#shared-workers-and-the-sharedworker-interface
class SharedWorker extends _EventTarget {
// To make it clear where onconnect comes from
onconnect: ConnectEventHandler;
constructor(url: string, name?: string) {
super();
}
}
////////////////////////////////////////////////////////////////////////////////
// Not part of any public standard but used above:
////////////////////////////////////////////////////////////////////////////////
class WindowList /* extends Array */ {}
class AsyncMap {
constructor(iterable?:any[]) {}
get(key: any): Promise { return accepted(); }
has(key: any): Promise { return accepted(); }
set(key: any, val: any): Promise { return accepted(); }
clear(): Promise { return accepted(); }
delete(key: any): Promise { return accepted(); }
forEach(callback: Function, thisArg?: Object): void {}
items(): Promise { return accepted(); }
keys(): Promise { return accepted(); }
values(): Promise { return accepted(); }
}
var _useWorkerResponse = function() : Promise { return accepted(); };
var _defaultToBrowserHTTP = function(url?) : Promise { return accepted(); };