neo.mjs 3.2.5 → 3.2.6

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.
@@ -0,0 +1,36 @@
1
+ import Neo from '../src/Neo.mjs';
2
+ import * as core from '../src/core/_export.mjs';
3
+ import ServiceBase from '../src/worker/ServiceBase.mjs';
4
+
5
+ /**
6
+ * @class Neo.ServiceWorker
7
+ * @extends Neo.worker.ServiceBase
8
+ * @singleton
9
+ */
10
+ class ServiceWorker extends ServiceBase {
11
+ static getConfig() {return {
12
+ /**
13
+ * @member {String} className='Neo.ServiceWorker'
14
+ * @protected
15
+ */
16
+ className: 'Neo.ServiceWorker',
17
+ /**
18
+ * @member {Boolean} singleton=true
19
+ * @protected
20
+ */
21
+ singleton: true,
22
+ /**
23
+ * @member {String} workerId='service'
24
+ * @protected
25
+ */
26
+ workerId: 'service'
27
+ }}
28
+ }
29
+
30
+ Neo.applyClassConfig(ServiceWorker);
31
+
32
+ let instance = Neo.create(ServiceWorker);
33
+
34
+ Neo.applyToGlobalNs(instance);
35
+
36
+ export default instance;
@@ -61,7 +61,7 @@ if (programOpts.info) {
61
61
  type : 'list',
62
62
  name : 'threads',
63
63
  message: 'Please choose the threads to build:',
64
- choices: ['all', 'app', 'canvas', 'data', 'main', 'vdom'],
64
+ choices: ['all', 'app', 'canvas', 'data', 'main', 'service', 'vdom'],
65
65
  default: 'all'
66
66
  });
67
67
  }
@@ -88,11 +88,12 @@ if (programOpts.info) {
88
88
  }
89
89
 
90
90
  function parseThreads(tPath) {
91
- (threads === 'all' || threads === 'main') && spawnSync(webpack, ['--config', `${tPath}.main.mjs`], cpOpts);
92
- (threads === 'all' || threads === 'app') && spawnSync(webpack, ['--config', `${tPath}.appworker.mjs`, `--env insideNeo=${insideNeo}`], cpOpts);
93
- (threads === 'all' || threads === 'canvas') && spawnSync(webpack, ['--config', `${tPath}.worker.mjs`, `--env insideNeo=${insideNeo} worker=canvas`], cpOpts);
94
- (threads === 'all' || threads === 'data') && spawnSync(webpack, ['--config', `${tPath}.worker.mjs`, `--env insideNeo=${insideNeo} worker=data`], cpOpts);
95
- (threads === 'all' || threads === 'vdom') && spawnSync(webpack, ['--config', `${tPath}.worker.mjs`, `--env insideNeo=${insideNeo} worker=vdom`], cpOpts);
91
+ (threads === 'all' || threads === 'main') && spawnSync(webpack, ['--config', `${tPath}.main.mjs`], cpOpts);
92
+ (threads === 'all' || threads === 'app') && spawnSync(webpack, ['--config', `${tPath}.appworker.mjs`, `--env insideNeo=${insideNeo}`], cpOpts);
93
+ (threads === 'all' || threads === 'canvas') && spawnSync(webpack, ['--config', `${tPath}.worker.mjs`, `--env insideNeo=${insideNeo} worker=canvas`], cpOpts);
94
+ (threads === 'all' || threads === 'data') && spawnSync(webpack, ['--config', `${tPath}.worker.mjs`, `--env insideNeo=${insideNeo} worker=data`], cpOpts);
95
+ (threads === 'all' || threads === 'service') && spawnSync(webpack, ['--config', `${tPath}.worker.mjs`, `--env insideNeo=${insideNeo} worker=service`], cpOpts);
96
+ (threads === 'all' || threads === 'vdom') && spawnSync(webpack, ['--config', `${tPath}.worker.mjs`, `--env insideNeo=${insideNeo} worker=vdom`], cpOpts);
96
97
  }
97
98
 
98
99
  // dist/development
@@ -14,6 +14,10 @@
14
14
  "input": "./src/worker/Data.mjs",
15
15
  "output": "dataworker.js"
16
16
  },
17
+ "service": {
18
+ "input": "./apps/ServiceWorker.mjs",
19
+ "output": "serviceworker.js"
20
+ },
17
21
  "vdom": {
18
22
  "input": "./src/worker/VDom.mjs",
19
23
  "output": "vdomworker.js"
@@ -0,0 +1,36 @@
1
+ import Neo from '../src/Neo.mjs';
2
+ import * as core from '../src/core/_export.mjs';
3
+ import ServiceBase from '../src/worker/ServiceBase.mjs';
4
+
5
+ /**
6
+ * @class Neo.ServiceWorker
7
+ * @extends Neo.worker.ServiceBase
8
+ * @singleton
9
+ */
10
+ class ServiceWorker extends ServiceBase {
11
+ static getConfig() {return {
12
+ /**
13
+ * @member {String} className='Neo.ServiceWorker'
14
+ * @protected
15
+ */
16
+ className: 'Neo.ServiceWorker',
17
+ /**
18
+ * @member {Boolean} singleton=true
19
+ * @protected
20
+ */
21
+ singleton: true,
22
+ /**
23
+ * @member {String} workerId='service'
24
+ * @protected
25
+ */
26
+ workerId: 'service'
27
+ }}
28
+ }
29
+
30
+ Neo.applyClassConfig(ServiceWorker);
31
+
32
+ let instance = Neo.create(ServiceWorker);
33
+
34
+ Neo.applyToGlobalNs(instance);
35
+
36
+ export default instance;
@@ -4,5 +4,6 @@
4
4
  "environment" : "development",
5
5
  "mainPath" : "./Main.mjs",
6
6
  "renderCountDeltas": true,
7
- "themes" : ["neo-theme-dark"]
7
+ "themes" : ["neo-theme-dark"],
8
+ "useServiceWorker" : true
8
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neo.mjs",
3
- "version": "3.2.5",
3
+ "version": "3.2.6",
4
4
  "description": "The webworkers driven UI framework",
5
5
  "type": "module",
6
6
  "repository": {
@@ -41,16 +41,16 @@
41
41
  "chalk": "^5.0.0",
42
42
  "clean-webpack-plugin": "^4.0.0",
43
43
  "commander": "^9.0.0",
44
- "cssnano": "^5.0.17",
44
+ "cssnano": "^5.1.0",
45
45
  "envinfo": "^7.8.1",
46
- "fs-extra": "^10.0.0",
46
+ "fs-extra": "^10.0.1",
47
47
  "highlightjs-line-numbers.js": "^2.8.0",
48
48
  "inquirer": "^8.1.5",
49
49
  "neo-jsdoc": "^1.0.1",
50
50
  "neo-jsdoc-x": "^1.0.4",
51
- "postcss": "^8.4.6",
52
- "sass": "^1.49.8",
53
- "webpack": "^5.69.1",
51
+ "postcss": "^8.4.7",
52
+ "sass": "^1.49.9",
53
+ "webpack": "^5.70.0",
54
54
  "webpack-cli": "^4.9.2",
55
55
  "webpack-dev-server": "4.7.4",
56
56
  "webpack-hook-plugin": "^1.0.7",
@@ -134,7 +134,7 @@ const DefaultConfig = {
134
134
  * Experimental flag if an offscreen canvas worker should get created.
135
135
  * @default false
136
136
  * @memberOf! module:Neo
137
- * @name config.useCssVars
137
+ * @name config.useCanvasWorker
138
138
  * @type Boolean
139
139
  */
140
140
  useCanvasWorker: false,
@@ -163,6 +163,15 @@ const DefaultConfig = {
163
163
  * @type Boolean
164
164
  */
165
165
  useGoogleAnalytics: false,
166
+ /**
167
+ * True will add the ServiceWorker main thread addon to support caching of assets (PWA)
168
+ * See: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
169
+ * @default false
170
+ * @memberOf! module:Neo
171
+ * @name config.useServiceWorker
172
+ * @type Boolean
173
+ */
174
+ useServiceWorker: false,
166
175
  /**
167
176
  * Creates App, Data & VDom as SharedWorkers.
168
177
  * Set this one to true in case you want to connect multiple main threads.
package/src/Main.mjs CHANGED
@@ -188,9 +188,11 @@ class Main extends core.Base {
188
188
  *
189
189
  */
190
190
  async onDomContentLoaded() {
191
- let me = this,
192
- config = Neo.config,
193
- imports = [];
191
+ let me = this,
192
+ config = Neo.config,
193
+ mainThreadAddons = config.mainThreadAddons,
194
+ imports = [],
195
+ modules;
194
196
 
195
197
  DomAccess.onDomContentLoaded();
196
198
 
@@ -203,18 +205,20 @@ class Main extends core.Base {
203
205
  __webpack_require__.p = config.basePath.substring(6);
204
206
  }
205
207
 
206
- config.mainThreadAddons.forEach(addon => {
207
- if (addon !== 'AnalyticsByGoogle') {
208
- imports.push(import(`./main/addon/${addon}.mjs`));
209
- }
210
- });
211
-
212
208
  // intended for the online examples where we need an easy way to add GA to every generated app
213
- if (config.useGoogleAnalytics || config.mainThreadAddons.includes('AnalyticsByGoogle')) {
214
- imports.push(import('./main/addon/AnalyticsByGoogle.mjs'));
209
+ if (config.useGoogleAnalytics && !mainThreadAddons.includes('AnalyticsByGoogle')) {
210
+ mainThreadAddons.push('AnalyticsByGoogle');
215
211
  }
216
212
 
217
- const modules = await Promise.all(imports);
213
+ if (config.useServiceWorker && !mainThreadAddons.includes('ServiceWorker')) {
214
+ mainThreadAddons.push('ServiceWorker');
215
+ }
216
+
217
+ mainThreadAddons.forEach(addon => {
218
+ imports.push(import(`./main/addon/${addon}.mjs`));
219
+ });
220
+
221
+ modules = await Promise.all(imports);
218
222
 
219
223
  me.addon = {};
220
224
 
@@ -0,0 +1,68 @@
1
+ import Base from '../../core/Base.mjs';
2
+ import RemoteMethodAccess from '../../worker/mixin/RemoteMethodAccess.mjs';
3
+ import WorkerManager from '../../worker/Manager.mjs';
4
+
5
+ /**
6
+ * Creates a ServiceWorker instance, in case Neo.config.useServiceWorker is set to true
7
+ * @class Neo.main.addon.ServiceWorker
8
+ * @extends Neo.core.Base
9
+ * @singleton
10
+ */
11
+ class ServiceWorker extends Base {
12
+ static getConfig() {return {
13
+ /**
14
+ * @member {String} className='Neo.main.addon.ServiceWorker'
15
+ * @protected
16
+ */
17
+ className: 'Neo.main.addon.ServiceWorker',
18
+ /**
19
+ * @member {String[]|Neo.core.Base[]|null} mixins=[RemoteMethodAccess]
20
+ */
21
+ mixins: [RemoteMethodAccess],
22
+ /**
23
+ * @member {ServiceWorkerRegistration|null} registration=null
24
+ * @protected
25
+ */
26
+ registration: null,
27
+ /**
28
+ * @member {Boolean} singleton=true
29
+ * @protected
30
+ */
31
+ singleton: true
32
+ }}
33
+
34
+ /**
35
+ * @param {Object} config
36
+ */
37
+ construct(config) {
38
+ if ('serviceWorker' in navigator) {
39
+ let me = this,
40
+ config = Neo.config,
41
+ devMode = config.environment === 'development',
42
+ fileName = devMode ? 'ServiceWorker.mjs' : 'serviceworker.js',
43
+ folder = window.location.pathname.includes('/examples/') ? 'examples/' : 'apps/',
44
+ opts = devMode ? {type: 'module'} : {};
45
+
46
+ navigator.serviceWorker.register(config.basePath + folder + fileName, opts)
47
+ .then(registration => {
48
+ me.registration = registration;
49
+
50
+ navigator.serviceWorker.onmessage = WorkerManager.onWorkerMessage.bind(WorkerManager);
51
+
52
+ WorkerManager.sendMessage('service', {
53
+ action: 'registerNeoConfig',
54
+ data : config,
55
+ port : registration.active
56
+ });
57
+ })
58
+ }
59
+ }
60
+ }
61
+
62
+ Neo.applyClassConfig(ServiceWorker);
63
+
64
+ let instance = Neo.create(ServiceWorker);
65
+
66
+ Neo.applyToGlobalNs(instance);
67
+
68
+ export default instance;
@@ -72,7 +72,7 @@ class App extends Base {
72
72
  }
73
73
 
74
74
  /**
75
- * @param {JSON} data
75
+ * @param {Object} data
76
76
  */
77
77
  createThemeMap(data) {
78
78
  Neo.ns('Neo.cssMap.fileInfo', true);
@@ -265,7 +265,7 @@ class App extends Base {
265
265
  this.onRegisterApp({ appName });
266
266
 
267
267
  this.sendMessage('main', {
268
- action:'registerAppName',
268
+ action: 'registerAppName',
269
269
  appName
270
270
  });
271
271
  }
@@ -218,6 +218,10 @@ class Manager extends Base {
218
218
  * @returns {Worker}
219
219
  */
220
220
  getWorker(name) {
221
+ if (name === 'service') {
222
+ return navigator.serviceWorker?.controller;
223
+ }
224
+
221
225
  return name instanceof Worker ? name : this.workers[name].worker;
222
226
  }
223
227
 
@@ -386,7 +390,12 @@ class Manager extends Base {
386
390
  message, worker;
387
391
 
388
392
  if (!me.stopCommunication) {
389
- worker = me.getWorker(dest);
393
+ if (opts.port) {
394
+ worker = opts.port;
395
+ delete opts.port;
396
+ } else {
397
+ worker = me.getWorker(dest);
398
+ }
390
399
 
391
400
  if (!worker) {
392
401
  throw new Error('Called sendMessage for a worker that does not exist: ' + dest);
@@ -0,0 +1,298 @@
1
+ import Base from '../core/Base.mjs';
2
+ import Message from './Message.mjs';
3
+ import RemoteMethodAccess from './mixin/RemoteMethodAccess.mjs';
4
+
5
+ /**
6
+ * @class Neo.worker.ServiceBase
7
+ * @extends Neo.core.Base
8
+ * @abstract
9
+ */
10
+ class ServiceBase extends Base {
11
+ /**
12
+ * @member {Object[]|null} channelPorts=null
13
+ * @protected
14
+ */
15
+ channelPorts = null
16
+ /**
17
+ * @member {Client|null} lastClient=null
18
+ * @protected
19
+ */
20
+ lastClient = null
21
+ /**
22
+ * @member {Object[]} promises=[]
23
+ * @protected
24
+ */
25
+ promises = []
26
+ /**
27
+ * @member {String[]} remotes=[]
28
+ * @protected
29
+ */
30
+ remotes = []
31
+
32
+ static getConfig() {return {
33
+ /**
34
+ * @member {String} className='Neo.worker.ServiceBase'
35
+ * @protected
36
+ */
37
+ className: 'Neo.worker.ServiceBase',
38
+ /**
39
+ * @member {String} cacheName='neo-runtime'
40
+ */
41
+ cacheName: 'neo-runtime',
42
+ /**
43
+ * @member {String[]|null} cachePaths
44
+ */
45
+ cachePaths: [
46
+ 'raw.githubusercontent.com/',
47
+ '/dist/production/',
48
+ '/fontawesome',
49
+ '/resources/'
50
+ ],
51
+ /**
52
+ * @member {String[]|Neo.core.Base[]|null} mixins=[RemoteMethodAccess]
53
+ */
54
+ mixins: [RemoteMethodAccess],
55
+ /**
56
+ * Remote method access for other workers
57
+ * @member {Object} remote={app: [//...]}
58
+ * @protected
59
+ */
60
+ remote: {
61
+ app: [
62
+ 'clearCache',
63
+ 'clearCaches'
64
+ ]
65
+ },
66
+ /**
67
+ * @member {String|null} workerId=null
68
+ * @protected
69
+ */
70
+ workerId: null
71
+ }}
72
+
73
+ /**
74
+ * @param {Object} config
75
+ */
76
+ construct(config) {
77
+ super.construct(config);
78
+
79
+ let me = this,
80
+ bind = name => me[name].bind(me);
81
+
82
+ me.channelPorts = [];
83
+
84
+ Object.assign(globalThis, {
85
+ onactivate: bind('onActivate'),
86
+ onfetch : bind('onFetch'),
87
+ oninstall : bind('onInstall'),
88
+ onmessage : bind('onMessage')
89
+ });
90
+
91
+ Neo.currentWorker = me;
92
+ Neo.workerId = me.workerId;
93
+ }
94
+
95
+ /**
96
+ * @param {String} name=this.cacheName
97
+ */
98
+ clearCache(name=this.cacheName) {
99
+ caches.keys()
100
+ .then(cacheNames => cacheNames.filter(cacheName => cacheName === name))
101
+ .then(cachesToDelete => Promise.all(cachesToDelete.map(cacheToDelete => caches.delete(cacheToDelete))))
102
+ }
103
+
104
+ /**
105
+ *
106
+ */
107
+ clearCaches() {
108
+ caches.keys()
109
+ .then(cachesToDelete => Promise.all(cachesToDelete.map(cacheToDelete => caches.delete(cacheToDelete))))
110
+ }
111
+
112
+ /**
113
+ * @param {Client} client
114
+ */
115
+ createMessageChannel(client) {
116
+ let me = this,
117
+ channel = new MessageChannel(),
118
+ port = channel.port2;
119
+
120
+ channel.port1.onmessage = me.onMessage.bind(me);
121
+
122
+ me.sendMessage('app', {action: 'registerPort', transfer: port}, [port]);
123
+
124
+ me.channelPorts.push({
125
+ clientId : client.id,
126
+ destination: 'app',
127
+ port : channel.port1
128
+ });
129
+ }
130
+
131
+ /**
132
+ *
133
+ * @param {String} destination
134
+ * @param {String} clientId=this.lastClient.id
135
+ * @returns {MessagePort|null}
136
+ */
137
+ getPort(destination, clientId=this.lastClient?.id) {
138
+ for (let port of this.channelPorts) {
139
+ if (clientId === port.clientId && destination === port.destination) {
140
+ return port.port;
141
+ }
142
+ }
143
+
144
+ return null;
145
+ }
146
+
147
+ /**
148
+ * Ignore the call in case there is no connected client in place yet
149
+ */
150
+ initRemote() {
151
+ let me = this,
152
+ lastClientId = me.lastClient?.id;
153
+
154
+ if (lastClientId && !me.remotes.includes(lastClientId)) {
155
+ me.remotes.push(lastClientId);
156
+ super.initRemote();
157
+ }
158
+ }
159
+
160
+ /**
161
+ * @param {ExtendableMessageEvent} event
162
+ */
163
+ onActivate(event) {
164
+ console.log('onActivate', event);
165
+ }
166
+
167
+ /**
168
+ * @param {Client} source
169
+ */
170
+ onConnect(source) {
171
+ console.log('onConnect', source);
172
+
173
+ this.createMessageChannel(source);
174
+ this.initRemote();
175
+ }
176
+
177
+ /**
178
+ * @param {ExtendableMessageEvent} event
179
+ */
180
+ onFetch(event) {
181
+ let hasMatch = false,
182
+ request = event.request,
183
+ key;
184
+
185
+ for (key of this.cachePaths) {
186
+ if (request.url.includes(key)) {
187
+ hasMatch = true;
188
+ break;
189
+ }
190
+ }
191
+
192
+ hasMatch && event.respondWith(
193
+ caches.match(request)
194
+ .then(cachedResponse => cachedResponse || caches.open(this.cacheName)
195
+ .then(cache => fetch(request)
196
+ .then(response => cache.put(request, response.clone())
197
+ .then(() => response)
198
+ )))
199
+ );
200
+ }
201
+
202
+ /**
203
+ * @param {ExtendableMessageEvent} event
204
+ */
205
+ onInstall(event) {
206
+ console.log('onInstall', event);
207
+ globalThis.skipWaiting();
208
+ }
209
+
210
+ /**
211
+ * For a client based message we receive an ExtendableMessageEvent,
212
+ * for a MessageChannel based message a MessageEvent
213
+ * @param {ExtendableMessageEvent|MessageEvent} event
214
+ */
215
+ onMessage(event) {
216
+ let me = this,
217
+ data = event.data,
218
+ action = data.action,
219
+ replyId = data.replyId,
220
+ promise;
221
+
222
+ if (event.source) { // ExtendableMessageEvent
223
+ me.lastClient = event.source;
224
+ }
225
+
226
+ if (!action) {
227
+ throw new Error('Message action is missing: ' + data.id);
228
+ }
229
+
230
+ if (action !== 'reply') {
231
+ me['on' + Neo.capitalize(action)](data, event);
232
+ } else if (promise = action === 'reply' && me.promises[replyId]) {
233
+ promise[data.reject ? 'reject' : 'resolve'](data.data);
234
+ delete me.promises[replyId];
235
+ }
236
+ }
237
+
238
+ /**
239
+ * @param {Object} msg
240
+ * @param {ExtendableMessageEvent} event
241
+ */
242
+ onPing(msg, event) {
243
+ this.resolve(msg, {originMsg: msg});
244
+ }
245
+
246
+ /**
247
+ * @param {Object} msg
248
+ * @param {ExtendableMessageEvent} event
249
+ */
250
+ onRegisterNeoConfig(msg, event) {
251
+ Neo.config = Neo.config || {};
252
+ Object.assign(Neo.config, msg.data);
253
+
254
+ this.onConnect(event.source);
255
+ }
256
+
257
+ /**
258
+ * @param {String} dest app, data, main or vdom (excluding the current worker)
259
+ * @param {Object} opts configs for Neo.worker.Message
260
+ * @param {Array} [transfer] An optional array of Transferable objects to transfer ownership of.
261
+ * If the ownership of an object is transferred, it becomes unusable (neutered) in the context it was sent from
262
+ * and becomes available only to the worker it was sent to.
263
+ * @returns {Promise<any>}
264
+ */
265
+ promiseMessage(dest, opts, transfer) {
266
+ let me = this;
267
+
268
+ return new Promise(function(resolve, reject) {
269
+ let message = me.sendMessage(dest, opts, transfer),
270
+ msgId = message.id;
271
+
272
+ me.promises[msgId] = {reject, resolve};
273
+ });
274
+ }
275
+
276
+ /**
277
+ * @param {String} dest app, data, main or vdom (excluding the current worker)
278
+ * @param {Object} opts configs for Neo.worker.Message
279
+ * @param {Array} [transfer] An optional array of Transferable objects to transfer ownership of.
280
+ * If the ownership of an object is transferred, it becomes unusable (neutered) in the context it was sent from
281
+ * and becomes available only to the worker it was sent to.
282
+ * @returns {Neo.worker.Message}
283
+ * @protected
284
+ */
285
+ sendMessage(dest, opts, transfer) {
286
+ opts.destination = dest;
287
+
288
+ let message = new Message(opts),
289
+ port = this.getPort(dest) || this.lastClient;
290
+
291
+ port.postMessage(message, transfer);
292
+ return message;
293
+ }
294
+ }
295
+
296
+ Neo.applyClassConfig(ServiceBase);
297
+
298
+ export default ServiceBase;
@@ -56,7 +56,7 @@ class RemoteMethodAccess extends Base {
56
56
  methods = remote.methods,
57
57
  pkg = Neo.ns(className, true);
58
58
 
59
- methods.forEach(function(method) {
59
+ methods.forEach(method => {
60
60
  if (remote.origin !== 'main' && pkg[method]) {
61
61
  throw new Error('Duplicate remote method definition ' + className + '.' + method);
62
62
  }