@webqit/webflo 0.8.47 → 0.8.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docker/Dockerfile +25 -0
- package/docker/README.md +71 -0
- package/package.json +9 -8
- package/src/cmd/client.js +68 -97
- package/src/cmd/origins.js +2 -2
- package/src/cmd/server.js +1 -1
- package/src/modules/Router.js +130 -0
- package/src/modules/_FormData.js +60 -0
- package/src/modules/_Headers.js +88 -0
- package/src/modules/_MessageStream.js +191 -0
- package/src/modules/_NavigationEvent.js +89 -0
- package/src/modules/_Request.js +61 -0
- package/src/modules/_RequestHeaders.js +72 -0
- package/src/modules/_Response.js +56 -0
- package/src/modules/_ResponseHeaders.js +81 -0
- package/src/modules/_URL.js +111 -0
- package/src/modules/client/Cache.js +38 -0
- package/src/modules/client/Client.js +49 -20
- package/src/modules/client/Http.js +26 -11
- package/src/modules/client/NavigationEvent.js +20 -0
- package/src/modules/client/Router.js +30 -106
- package/src/modules/client/StdRequest.js +13 -15
- package/src/modules/client/Storage.js +56 -0
- package/src/modules/client/Url.js +1 -1
- package/src/modules/client/Worker.js +70 -66
- package/src/modules/client/WorkerClient.js +102 -0
- package/src/modules/client/WorkerComm.js +183 -0
- package/src/modules/client/effects/sounds.js +64 -0
- package/src/modules/server/NavigationEvent.js +38 -0
- package/src/modules/server/Router.js +53 -126
- package/src/modules/server/Server.js +192 -84
- package/src/modules/util.js +7 -7
- package/src/modules/NavigationEvent.js +0 -46
- package/src/modules/Response.js +0 -98
- package/src/modules/XURL.js +0 -125
- package/src/modules/client/ClientNavigationEvent.js +0 -10
- package/src/modules/client/Push.js +0 -84
- package/src/modules/server/ServerNavigationEvent.js +0 -10
- package/src/modules/server/StdIncomingMessage.js +0 -73
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @imports
|
|
5
|
+
*/
|
|
6
|
+
import { Observer } from '@webqit/pseudo-browser/index2.js';
|
|
7
|
+
import { _isString, _isUndefined } from '@webqit/util/js/index.js';
|
|
8
|
+
|
|
9
|
+
export default function(persistent = false) {
|
|
10
|
+
|
|
11
|
+
const storeType = persistent ? 'localStorage' : 'sessionStorage';
|
|
12
|
+
if (!window[storeType]) {
|
|
13
|
+
throw new Error(`The specified Web Storage API ${storeType} is invalid or not supported`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const _storage = {};
|
|
17
|
+
Observer.intercept(_storage, (event, received, next) => {
|
|
18
|
+
if (event.type === 'get' && _isString(event.name)) {
|
|
19
|
+
const value = window[storeType].getItem(event.name);
|
|
20
|
+
return !_isUndefined(value) ? JSON.parse(value) : value;
|
|
21
|
+
}
|
|
22
|
+
if (event.type === 'set') {
|
|
23
|
+
window[storeType].setItem(event.name, !_isUndefined(event.value) ? JSON.stringify(event.value) : event.value);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
if (event.type === 'deleteProperty') {
|
|
27
|
+
window[storeType].removeItem(event.name);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (event.type === 'has') {
|
|
31
|
+
for(var i = 0; i < window[storeType].length; i ++){
|
|
32
|
+
if (window[storeType].key(i) === event.name) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
if (event.type === 'ownKeys') {
|
|
39
|
+
var keys = [];
|
|
40
|
+
for(var i = 0; i < window[storeType].length; i ++){
|
|
41
|
+
keys.push(window[storeType].key(i));
|
|
42
|
+
};
|
|
43
|
+
return keys;
|
|
44
|
+
}
|
|
45
|
+
if (event.type === 'getOwnPropertyDescriptor') {
|
|
46
|
+
return { enumerable: true, configurable: true };
|
|
47
|
+
}
|
|
48
|
+
return next();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return Observer.proxy(_storage);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
Observer,
|
|
56
|
+
}
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
import Router from './Router.js';
|
|
6
6
|
import _isGlobe from 'is-glob';
|
|
7
7
|
import Minimatch from 'minimatch';
|
|
8
|
+
import { Observer } from '@webqit/pseudo-browser/index2.js';
|
|
8
9
|
import _isArray from '@webqit/util/js/isArray.js';
|
|
9
10
|
import _afterLast from '@webqit/util/str/afterLast.js';
|
|
10
11
|
import _after from '@webqit/util/str/after.js';
|
|
11
12
|
import _before from '@webqit/util/str/before.js';
|
|
12
13
|
import _any from '@webqit/util/arr/any.js';
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
|
|
14
|
+
import _copy from '@webqit/util/obj/copy.js';
|
|
15
|
+
import NavigationEvent from './NavigationEvent.js';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* ---------------------------
|
|
@@ -23,8 +23,10 @@ import ClientNavigationEvent from './ClientNavigationEvent.js';
|
|
|
23
23
|
export default function(layout, params) {
|
|
24
24
|
|
|
25
25
|
// Copy...
|
|
26
|
-
layout = {...layout};
|
|
27
|
-
params = {...params};
|
|
26
|
+
layout = { ...layout };
|
|
27
|
+
params = { ...params };
|
|
28
|
+
const sessionStores = Object.create(null);
|
|
29
|
+
const localStores = Object.create(null);
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* -------------
|
|
@@ -91,45 +93,55 @@ export default function(layout, params) {
|
|
|
91
93
|
* ONFETCH
|
|
92
94
|
* -------------
|
|
93
95
|
*/
|
|
94
|
-
|
|
96
|
+
|
|
95
97
|
// Listen now...
|
|
96
98
|
self.addEventListener('fetch', async evt => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
// URL schemes that might arrive here but not supported; e.g.: chrome-extension://
|
|
100
|
+
if (!evt.request.url.startsWith('http')) return;
|
|
101
|
+
// Fetches request
|
|
102
|
+
const handleFetch = async evt => {
|
|
103
|
+
|
|
104
|
+
if (evt.request.url.startsWith(self.origin) && (evt.request.mode === 'navigate' || evt.request.headers.get('X-Powered-By') === '@webqit/webflo')) {
|
|
105
|
+
// -----------------
|
|
106
|
+
// Sync session data to cache to be available to service-worker routers
|
|
107
|
+
// Sync only takes for requests that actually do send the "$session" cookie
|
|
108
|
+
const sessionData = Observer.proxy(sessionStores[evt.clientId] || {});
|
|
109
|
+
const clientNavigationEvent = new NavigationEvent(new NavigationEvent.Request(evt.request), sessionData);
|
|
110
|
+
// -----------------
|
|
111
|
+
// The app router
|
|
112
|
+
const router = new Router(_before(evt.request.url, '?'), layout, { layout });
|
|
113
|
+
const httpMethodName = evt.request.method.toLowerCase();
|
|
114
|
+
const _response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], clientNavigationEvent, null, (event, arg) => defaultFetch(evt));
|
|
115
|
+
if (!(_response instanceof Response)/* _response being a native Response instance is fine */) {
|
|
116
|
+
return new NavigationEvent.Response(_response);
|
|
117
|
+
}
|
|
118
|
+
return _response;
|
|
119
|
+
}
|
|
104
120
|
|
|
105
|
-
|
|
106
|
-
layout,
|
|
121
|
+
return defaultFetch(evt);
|
|
107
122
|
};
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const clientNavigationEvent = new ClientNavigationEvent(evt.request, evt.request.url);
|
|
111
|
-
const httpMethodName = evt.request.method.toLowerCase();
|
|
112
|
-
return await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], clientNavigationEvent, null, async function() {
|
|
113
|
-
if (_any((params.cache_only_url_list || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(evt.request.url, pattern))) {
|
|
114
|
-
return cache_fetch(evt);
|
|
115
|
-
}
|
|
116
|
-
// Now, the following is key:
|
|
117
|
-
// The browser likes to use "force-cache" for "navigate" requests
|
|
118
|
-
// when, for example, the back button was used.
|
|
119
|
-
// Thus the origin server would still not be contacted by the self.fetch() below, leading to inconsistencies in responses.
|
|
120
|
-
// So, we detect this scenerio and avoid it.
|
|
121
|
-
if (evt.request.mode === 'navigate' && evt.request.cache === 'force-cache' && evt.request.destination === 'document') {
|
|
122
|
-
return cache_fetch(evt, true/** cacheRefresh */);
|
|
123
|
-
}
|
|
124
|
-
if (_any((params.cache_first_url_list || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(evt.request.url, pattern))) {
|
|
125
|
-
return cache_fetch(evt, true/** cacheRefresh */);
|
|
126
|
-
}
|
|
127
|
-
if (_any((params.network_first_url_list || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(evt.request.url, pattern))) {
|
|
128
|
-
return network_fetch(evt, true/** cacheFallback */);
|
|
129
|
-
}
|
|
130
|
-
return network_fetch(evt);
|
|
131
|
-
});
|
|
123
|
+
evt.respondWith(handleFetch(evt));
|
|
124
|
+
});
|
|
132
125
|
|
|
126
|
+
const defaultFetch = function(evt) {
|
|
127
|
+
if (_any((params.cache_only_url_list || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(evt.request.url, pattern))) {
|
|
128
|
+
return cache_fetch(evt);
|
|
129
|
+
}
|
|
130
|
+
// Now, the following is key:
|
|
131
|
+
// The browser likes to use "force-cache" for "navigate" requests
|
|
132
|
+
// when, for example, the back button was used.
|
|
133
|
+
// Thus the origin server would still not be contacted by the self.fetch() below, leading to inconsistencies in responses.
|
|
134
|
+
// So, we detect this scenerio and avoid it.
|
|
135
|
+
if (evt.request.mode === 'navigate' && evt.request.cache === 'force-cache' && evt.request.destination === 'document') {
|
|
136
|
+
return cache_fetch(evt, true/** cacheRefresh */);
|
|
137
|
+
}
|
|
138
|
+
if (_any((params.cache_first_url_list || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(evt.request.url, pattern))) {
|
|
139
|
+
return cache_fetch(evt, true/** cacheRefresh */);
|
|
140
|
+
}
|
|
141
|
+
if (_any((params.network_first_url_list || []).map(c => c.trim()).filter(c => c), pattern => Minimatch.Minimatch(evt.request.url, pattern))) {
|
|
142
|
+
return network_fetch(evt, true/** cacheFallback */);
|
|
143
|
+
}
|
|
144
|
+
return network_fetch(evt);
|
|
133
145
|
};
|
|
134
146
|
|
|
135
147
|
const getCacheName = request => request.headers.get('Accept') === 'application/json'
|
|
@@ -170,7 +182,7 @@ export default function(layout, params) {
|
|
|
170
182
|
const refreshCache = (request, response) => {
|
|
171
183
|
|
|
172
184
|
// Check if we received a valid response
|
|
173
|
-
if (request.method !== 'GET' || !response || response.status !== 200 || (response.type !== 'basic' && response.type !== 'cors')) {
|
|
185
|
+
if ((request._method || request.method) !== 'GET' || !response || response.status !== 200 || (response.type !== 'basic' && response.type !== 'cors')) {
|
|
174
186
|
return response;
|
|
175
187
|
}
|
|
176
188
|
|
|
@@ -189,25 +201,33 @@ export default function(layout, params) {
|
|
|
189
201
|
return response;
|
|
190
202
|
};
|
|
191
203
|
|
|
204
|
+
const relay = function(evt, messageData) {
|
|
205
|
+
return self.clients.matchAll().then(clientList => {
|
|
206
|
+
clientList.forEach(client => {
|
|
207
|
+
if (client.id === evt.source.id) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
client.postMessage(messageData);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
192
215
|
// -----------------------------
|
|
193
216
|
|
|
194
217
|
self.addEventListener('message', evt => {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
};
|
|
198
|
-
const router = new Router('/', layout, $context);
|
|
218
|
+
|
|
219
|
+
// Handle normally
|
|
220
|
+
const router = new Router('/', layout, { layout });
|
|
199
221
|
evt.waitUntil(
|
|
200
222
|
router.route('postmessage', evt, null, function() {
|
|
201
223
|
return self;
|
|
202
224
|
})
|
|
203
225
|
);
|
|
226
|
+
|
|
204
227
|
});
|
|
205
228
|
|
|
206
229
|
self.addEventListener('push', evt => {
|
|
207
|
-
const
|
|
208
|
-
layout,
|
|
209
|
-
};
|
|
210
|
-
const router = new Router('/', layout, $context);
|
|
230
|
+
const router = new Router('/', layout, { layout });
|
|
211
231
|
evt.waitUntil(
|
|
212
232
|
router.route('push', evt, null, function() {
|
|
213
233
|
return self;
|
|
@@ -216,10 +236,7 @@ export default function(layout, params) {
|
|
|
216
236
|
});
|
|
217
237
|
|
|
218
238
|
self.addEventListener('notificationclick', evt => {
|
|
219
|
-
const
|
|
220
|
-
layout,
|
|
221
|
-
};
|
|
222
|
-
const router = new Router('/', layout, $context);
|
|
239
|
+
const router = new Router('/', layout, { layout });
|
|
223
240
|
evt.waitUntil(
|
|
224
241
|
router.route('notificationclick', evt, null, function() {
|
|
225
242
|
return self;
|
|
@@ -228,10 +245,7 @@ export default function(layout, params) {
|
|
|
228
245
|
});
|
|
229
246
|
|
|
230
247
|
self.addEventListener('notificationclose', evt => {
|
|
231
|
-
const
|
|
232
|
-
layout,
|
|
233
|
-
};
|
|
234
|
-
const router = new Router('/', layout, $context);
|
|
248
|
+
const router = new Router('/', layout, { layout });
|
|
235
249
|
evt.waitUntil(
|
|
236
250
|
router.route('notificationclose', evt, null, function() {
|
|
237
251
|
return self;
|
|
@@ -247,16 +261,6 @@ export default function(layout, params) {
|
|
|
247
261
|
const matchClientUrl = (client, url) => '/' + _after(_after(client.url, '//'), '/') === url;
|
|
248
262
|
|
|
249
263
|
/**
|
|
250
|
-
relay(messageData) {
|
|
251
|
-
return self.clients.matchAll().then(clientList => {
|
|
252
|
-
clientList.forEach(client => {
|
|
253
|
-
if (client.id === evt.source.id) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
client.postMessage(messageData);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
};
|
|
260
264
|
|
|
261
265
|
if (notificationData) {
|
|
262
266
|
var title = params.NOTIFICATION_TITLE || '';
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @imports
|
|
5
|
+
*/
|
|
6
|
+
import { Observer } from '@webqit/pseudo-browser/index2.js';
|
|
7
|
+
import { _isFunction } from '@webqit/util/js/index.js';
|
|
8
|
+
|
|
9
|
+
export default class WorkerClient {
|
|
10
|
+
|
|
11
|
+
constructor(file, params = {}) {
|
|
12
|
+
this.ready = navigator.serviceWorker.ready;
|
|
13
|
+
// --------
|
|
14
|
+
// Registration and lifecycle
|
|
15
|
+
// --------
|
|
16
|
+
this.registration = new Promise((resolve, reject) => {
|
|
17
|
+
const register = () => {
|
|
18
|
+
navigator.serviceWorker.register(file, { scope: params.scope || '/' }).then(async registration => {
|
|
19
|
+
|
|
20
|
+
// Helper that updates instance's state
|
|
21
|
+
const state = target => {
|
|
22
|
+
// instance2.state can be any of: "installing", "installed", "activating", "activated", "redundant"
|
|
23
|
+
const equivState = target.state === 'installed' ? 'waiting' :
|
|
24
|
+
(target.state === 'activating' || target.state === 'activated' ? 'active' : target.state)
|
|
25
|
+
Observer.set(this, equivState, target);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// We're always installing at first for a new service worker.
|
|
29
|
+
// An existing service would immediately be active
|
|
30
|
+
const worker = registration.active || registration.waiting || registration.installing;
|
|
31
|
+
state(worker);
|
|
32
|
+
worker.addEventListener('statechange', e => state(e.target));
|
|
33
|
+
|
|
34
|
+
// "updatefound" event - a new worker that will control
|
|
35
|
+
// this page is installing somewhere
|
|
36
|
+
registration.addEventListener('updatefound', () => {
|
|
37
|
+
// If updatefound is fired, it means that there's
|
|
38
|
+
// a new service worker being installed.
|
|
39
|
+
state(registration.installing);
|
|
40
|
+
registration.installing.addEventListener('statechange', e => state(e.target));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
resolve(registration);
|
|
44
|
+
}).catch(e => reject(e));
|
|
45
|
+
};
|
|
46
|
+
if (params.onWondowLoad) {
|
|
47
|
+
window.addEventListener('load', register);
|
|
48
|
+
} else {
|
|
49
|
+
register();
|
|
50
|
+
}
|
|
51
|
+
if (params.startMessages) {
|
|
52
|
+
navigator.serviceWorker.startMessages();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// --------
|
|
56
|
+
// Post messaging
|
|
57
|
+
// --------
|
|
58
|
+
this.post = {
|
|
59
|
+
send: (message, onAvailability = 1) => {
|
|
60
|
+
if (this.active) {
|
|
61
|
+
if (_isFunction(message)) message = message();
|
|
62
|
+
this.active.postMessage(message);
|
|
63
|
+
} else if (onAvailability) {
|
|
64
|
+
// Availability Handling
|
|
65
|
+
const availabilityHandler = entry => {
|
|
66
|
+
if (_isFunction(message)) message = message();
|
|
67
|
+
entry.value.postMessage(message);
|
|
68
|
+
if (onAvailability !== 2) {
|
|
69
|
+
Observer.unobserve(this, 'active', availabilityHandler);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
Observer.observe(this, 'active', availabilityHandler);
|
|
73
|
+
}
|
|
74
|
+
return this.post;
|
|
75
|
+
},
|
|
76
|
+
receive: (callback, onAvailability = 1) => {
|
|
77
|
+
navigator.serviceWorker.addEventListener('message', callback);
|
|
78
|
+
return this.post;
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
// --------
|
|
82
|
+
// Push notifications
|
|
83
|
+
// --------
|
|
84
|
+
this.push = {
|
|
85
|
+
getSubscription: async () => {
|
|
86
|
+
return (await this.registration).pushManager.getSubscription();
|
|
87
|
+
},
|
|
88
|
+
subscribe: async (publicKey, params = {}) => {
|
|
89
|
+
var subscription = await this.push.getSubscription();
|
|
90
|
+
return subscription ? subscription : (await this.registration).pushManager.subscribe({
|
|
91
|
+
applicationServerKey: urlBase64ToUint8Array(publicKey),
|
|
92
|
+
...params,
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
unsubscribe: async () => {
|
|
96
|
+
var subscription = await this.push.getSubscription();
|
|
97
|
+
return !subscription ? null : subscription.unsubscribe();
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @imports
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
var _sab;
|
|
8
|
+
export function request(context, data = {}) {
|
|
9
|
+
var response;
|
|
10
|
+
const channel = new MessageChannel();
|
|
11
|
+
channel.port1.onmessage = e => (console.log('-----receiving'), response = e.data);
|
|
12
|
+
console.log('------waiting 11111', self.crossOriginIsolated);
|
|
13
|
+
try {
|
|
14
|
+
_sab = _sab || new ArrayBuffer(4); Atomics;
|
|
15
|
+
context.postMessage({ _sab, ...data, }, [ channel.port2 ]);
|
|
16
|
+
Atomics.wait(new Int32Array(_sab), 0, 0, 400);
|
|
17
|
+
return channel;
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.error('error1', e);
|
|
20
|
+
var x = new XMLHttpRequest();
|
|
21
|
+
x.timeout = 400;
|
|
22
|
+
x.open('get', '/@sleep@/t.js?t=400', false);
|
|
23
|
+
x.setRequestHeader('cache-control', 'no-cache, no-store, max-age=0');
|
|
24
|
+
try { x.send() } catch(e) { console.error('error2', e, response) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return response;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function listen(context, handler) {
|
|
31
|
+
console.log('::::::::::::::::::', crossOriginIsolated)
|
|
32
|
+
context.addEventListener('message', e => {
|
|
33
|
+
if (e.data._sab instanceof SharedArrayBuffer && e.ports.length) {
|
|
34
|
+
console.log('--------handling', e.data)
|
|
35
|
+
var response = handler(e.data);
|
|
36
|
+
if (response !== undefined) {
|
|
37
|
+
e.ports[0].postMessage(response);
|
|
38
|
+
console.log('--------sent');
|
|
39
|
+
Atomics.notify(new Int32Array(e.data._sab), 0, 1);
|
|
40
|
+
console.log('--------notified');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* ------------------
|
|
48
|
+
* https://www.sitepen.com/blog/the-return-of-sharedarraybuffers-and-atomics
|
|
49
|
+
* ------------------
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
export function sharedArrayBufferToUtf16String(buf) {
|
|
53
|
+
const array = new Uint16Array(buf);
|
|
54
|
+
return String.fromCharCode.apply(null, array);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function utf16StringToSharedArrayBuffer(str) {
|
|
58
|
+
// 2 bytes for each char
|
|
59
|
+
const bytes = str.length *2;
|
|
60
|
+
const buffer = new SharedArrayBuffer(bytes);
|
|
61
|
+
const arrayBuffer = new Uint16Array(buffer);
|
|
62
|
+
for (let i = 0, strLen = str.length; i < strLen; i++) {
|
|
63
|
+
arrayBuffer[i] = str.charCodeAt(i);
|
|
64
|
+
}
|
|
65
|
+
return { array: arrayBuffer, buffer: buffer };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function encodeUf8StringToSharedArrayBuffer(string) {
|
|
69
|
+
// Calculate the byte size of the UTF-8 string
|
|
70
|
+
let bytes = string.length;
|
|
71
|
+
for (let i = string.length -1; i <= 0; i--) {
|
|
72
|
+
const code = string.charCodeAt(i);
|
|
73
|
+
if (code > 0x7f && code <= 0x7ff) {
|
|
74
|
+
bytes++;
|
|
75
|
+
} else if (code > 0x7ff && code <= 0xffff) {
|
|
76
|
+
bytes+=2;
|
|
77
|
+
}
|
|
78
|
+
if (code >= 0xdc00 && code <= 0xdfff) {
|
|
79
|
+
i--; // trail surrogate
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const buffer = new SharedArrayBuffer(bytes);
|
|
83
|
+
const arrayBuffer = new Uint8Array(buffer);
|
|
84
|
+
const encoded = unescape(encodeURIComponent(string));
|
|
85
|
+
for (var i = 0; i < encoded.length; i++) {
|
|
86
|
+
arrayBuffer[i] = encoded[i].charCodeAt(0);
|
|
87
|
+
}
|
|
88
|
+
return { array: arrayBuffer, buffer: buffer };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function decodeUtf8StringFromSharedArrayBuffer(array) {
|
|
92
|
+
var encodedString = String.fromCharCode.apply(null, array);
|
|
93
|
+
var decodedString = decodeURIComponent(escape(encodedString));
|
|
94
|
+
return decodedString;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* // main.js
|
|
99
|
+
worker.postMessage(sharedBuffer);
|
|
100
|
+
// worker.js
|
|
101
|
+
constsharedArray = new Int32Array(m.data);
|
|
102
|
+
|
|
103
|
+
const exampleString = "Hello world, this is an example string!";
|
|
104
|
+
const sharedArrayBuffer = utf16StringToSharedArrayBuffer(exampleString).buffer;
|
|
105
|
+
const backToString = sharedArrayBufferToUtf16String(sharedArrayBuffer);
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
// Sync Local Storage
|
|
109
|
+
function sharedStore(store, persistent = false, onAvailability = 1) {
|
|
110
|
+
const storeData = () => Object.keys(store).reduce((_store, key) => (_store[key] = store[key], _store), {});
|
|
111
|
+
this.post.send(() => ({ _type: 'WHOLE_STORAGE_SYNC', _persistent: persistent, store: storeData() }), onAvailability);
|
|
112
|
+
window.addEventListener('beforeunload', e => {
|
|
113
|
+
this.post.send({ _type: 'WHOLE_STORAGE_SYNC', _persistent: persistent });
|
|
114
|
+
});
|
|
115
|
+
// --------
|
|
116
|
+
Observer.observe(store, changes => {
|
|
117
|
+
changes.forEach(change => {
|
|
118
|
+
if (change.type === 'set') {
|
|
119
|
+
if (!(change.detail || {}).noSync) {
|
|
120
|
+
this.post.send({ _type: 'STORAGE_SYNC', _persistent: persistent, ..._copy(change, [ 'type', 'name', 'path', 'value', 'oldValue', 'isUpdate', 'related', ]) });
|
|
121
|
+
}
|
|
122
|
+
} else if (change.type === 'deletion') {
|
|
123
|
+
if (!(change.detail || {}).noSync) {
|
|
124
|
+
this.post.send({ _type: 'STORAGE_SYNC', _persistent: persistent, ..._copy(change, [ 'type', 'name', 'path', 'value', 'oldValue', 'isUpdate', 'related', ]) });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
// --------
|
|
130
|
+
this.post.receive(e => {
|
|
131
|
+
if (e.data && e.data._type === 'STORAGE_SYNC' && e.data._persistent === persistent) {
|
|
132
|
+
if (e.data.type === 'set') {
|
|
133
|
+
Observer.set(store, e.data.name, e.data.value, { detail: { noSync: true } });
|
|
134
|
+
} else if (e.data.type === 'deletion') {
|
|
135
|
+
Observer.deleteProperty(store, e.data.name, { detail: { noSync: true } });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}, onAvailability);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
self.addEventListener('message', evt => {
|
|
142
|
+
|
|
143
|
+
// SESSION_SYNC
|
|
144
|
+
var clientId = evt.source.id;
|
|
145
|
+
if (evt.data && evt.data._type === 'WHOLE_STORAGE_SYNC') {
|
|
146
|
+
const storage = evt.data._persistent ? localStores : sessionStores;
|
|
147
|
+
if (evt.data.store) {
|
|
148
|
+
storage[clientId] = evt.data.store;
|
|
149
|
+
// --------------------------
|
|
150
|
+
// Get mutations synced TO client
|
|
151
|
+
Observer.observe(storage[clientId], changes => {
|
|
152
|
+
changes.forEach(change => {
|
|
153
|
+
if (!(change.detail || {}).noSync) {
|
|
154
|
+
self.clients.get(clientId).then(client => {
|
|
155
|
+
client.postMessage({ _type: 'STORAGE_SYNC', _persistent: evt.data._persistent, ..._copy(change, [ 'type', 'name', 'path', 'value', 'oldValue', 'isUpdate', 'related', ]), });
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
// --------------------------
|
|
161
|
+
} else {
|
|
162
|
+
delete storage[clientId];
|
|
163
|
+
}
|
|
164
|
+
} else if (evt.data && evt.data._type === 'STORAGE_SYNC') {
|
|
165
|
+
// --------------------------
|
|
166
|
+
// Get mutations synced FROM client
|
|
167
|
+
const storage = evt.data._persistent ? localStores : sessionStores;
|
|
168
|
+
if (evt.data.type === 'set') {
|
|
169
|
+
if (storage[clientId]) Observer.set(storage[clientId], evt.data.name, evt.data.value, { detail: { noSync: true } });
|
|
170
|
+
} else if (evt.data.type === 'deletion') {
|
|
171
|
+
if (storage[clientId]) Observer.deleteProperty(storage[clientId], evt.data.name, { detail: { noSync: true } });
|
|
172
|
+
}
|
|
173
|
+
// --------------------------
|
|
174
|
+
|
|
175
|
+
// --------------------------
|
|
176
|
+
// Relay to other clients
|
|
177
|
+
if (evt.data._persistent) {
|
|
178
|
+
relay(evt, evt.data);
|
|
179
|
+
}
|
|
180
|
+
// --------------------------
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const context = new window.AudioContext();
|
|
2
|
+
|
|
3
|
+
export function playFile(filepath) {
|
|
4
|
+
// see https://jakearchibald.com/2016/sounds-fun/
|
|
5
|
+
// Fetch the file
|
|
6
|
+
fetch(filepath)
|
|
7
|
+
// Read it into memory as an arrayBuffer
|
|
8
|
+
.then(response => response.arrayBuffer())
|
|
9
|
+
// Turn it from mp3/aac/whatever into raw audio data
|
|
10
|
+
.then(arrayBuffer => context.decodeAudioData(arrayBuffer))
|
|
11
|
+
.then(audioBuffer => {
|
|
12
|
+
// Now we're ready to play!
|
|
13
|
+
const soundSource = context.createBufferSource();
|
|
14
|
+
soundSource.buffer = audioBuffer;
|
|
15
|
+
soundSource.connect(context.destination);
|
|
16
|
+
soundSource.start();
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function playSuccess() {
|
|
21
|
+
const successNoise = context.createOscillator();
|
|
22
|
+
successNoise.frequency = "600";
|
|
23
|
+
successNoise.type = "sine";
|
|
24
|
+
successNoise.frequency.exponentialRampToValueAtTime(800, context.currentTime + 0.05);
|
|
25
|
+
successNoise.frequency.exponentialRampToValueAtTime(1000, context.currentTime + 0.15);
|
|
26
|
+
|
|
27
|
+
successGain = context.createGain();
|
|
28
|
+
successGain.gain.exponentialRampToValueAtTime(0.01, context.currentTime + 0.3);
|
|
29
|
+
|
|
30
|
+
successFilter = context.createBiquadFilter("bandpass");
|
|
31
|
+
successFilter.Q = 0.01;
|
|
32
|
+
|
|
33
|
+
successNoise
|
|
34
|
+
.connect(successFilter)
|
|
35
|
+
.connect(successGain)
|
|
36
|
+
.connect(context.destination);
|
|
37
|
+
successNoise.start();
|
|
38
|
+
successNoise.stop(context.currentTime + 0.2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function playError() {
|
|
42
|
+
const errorNoise = context.createOscillator();
|
|
43
|
+
errorNoise.frequency = "400";
|
|
44
|
+
errorNoise.type = "sine";
|
|
45
|
+
errorNoise.frequency.exponentialRampToValueAtTime(200, context.currentTime + 0.05);
|
|
46
|
+
errorNoise.frequency.exponentialRampToValueAtTime(100, context.currentTime + 0.2);
|
|
47
|
+
|
|
48
|
+
errorGain = context.createGain();
|
|
49
|
+
errorGain.gain.exponentialRampToValueAtTime(0.01, context.currentTime + 0.3);
|
|
50
|
+
|
|
51
|
+
errorNoise.connect(errorGain).connect(context.destination);
|
|
52
|
+
errorNoise.start();
|
|
53
|
+
errorNoise.stop(context.currentTime + 0.3);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let successButton = document.querySelector("#success");
|
|
57
|
+
successButton.addEventListener("click", function() {
|
|
58
|
+
playFile('https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/success.mp3');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
let errorButton = document.querySelector("#error");
|
|
62
|
+
errorButton.addEventListener("click", function() {
|
|
63
|
+
playFile('https://s3-us-west-2.amazonaws.com/s.cdpn.io/3/error.mp3');
|
|
64
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @imports
|
|
4
|
+
*/
|
|
5
|
+
import { URL } from 'url';
|
|
6
|
+
import { Readable } from "stream";
|
|
7
|
+
import { FormData, File, Blob } from 'formdata-node';
|
|
8
|
+
import { FormDataEncoder } from 'form-data-encoder';
|
|
9
|
+
import { Request, Response, Headers } from 'node-fetch';
|
|
10
|
+
import _NavigationEvent from '../_NavigationEvent.js';
|
|
11
|
+
import _FormData from '../_FormData.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Patch MessageStream with formData()
|
|
15
|
+
*/
|
|
16
|
+
const _streamFormDataPatch = MessageStream => class extends MessageStream {
|
|
17
|
+
|
|
18
|
+
// formData() polyfill
|
|
19
|
+
async formData() {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The NavigationEvent class
|
|
27
|
+
*/
|
|
28
|
+
export default _NavigationEvent({
|
|
29
|
+
URL,
|
|
30
|
+
Request: _streamFormDataPatch(Request),
|
|
31
|
+
Response: _streamFormDataPatch(Response),
|
|
32
|
+
Headers,
|
|
33
|
+
FormData: _FormData(FormData),
|
|
34
|
+
File,
|
|
35
|
+
Blob,
|
|
36
|
+
ReadableStream: Readable,
|
|
37
|
+
FormDataEncoder
|
|
38
|
+
});
|