@akc42/app-utils 3.1.5

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/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # app-utils
2
+ General utilities that I use for building SPAs
3
+
4
+ New from release 3.0
5
+
6
+ config-promise is a function which returns both a promise for the config AND adds the config items to sessionStorage.
7
+
8
+ debug has been replaced with a version that works differently and is incompatible with our previous usage (hence the bump to version 3)
9
+
10
+ # app-keys
11
+
12
+ `keys` is a module that provides keyboard support by normalising key
13
+ handling amongst browsers and providing a simple interface that can be used
14
+ by the application. Each keyboard usage request the user create a new
15
+ instance of the AppKeys class passing the key target in to the constructor.
16
+ This module will add and event handler for itself to that key target for the
17
+ keydown event. On that event it will parse the key combinations pressed and
18
+ fire a "key-pressed" event on the target.
19
+
20
+ The key grammer is of the form `[<modifier>+]<key>[:<event>]` and each of these is
21
+ space separated. optional `<modifiers>` (default none) are shift ctrl alt meta, and the optional
22
+ `<events>` are keydown and keyup (keydown is used as default)
23
+
24
+ To allow a using module the freedom to connect and disconnect to the dom, we
25
+ provide two methods to be called during the disconnectCallback and
26
+ connectCallback to disconnect and reconnect to this event
27
+
28
+ There are two models of using this, the first, shown below allows an element to add
29
+ the keys event to document body. This is good for pages that are swapped in and
30
+ out by a paging mechanism so that only one page is in the dom at a given time This is
31
+ good if only one handler is needed for the entire page.
32
+
33
+ NOTE: If you take this approach limit the keys to non function keys as the main app, with its main
34
+ menu may add a handler for the function keys and both menus, and any actions you choose would run at the same time.
35
+
36
+ ```
37
+ constructor() {
38
+ super();
39
+ ...
40
+ this._keyPressed = this._keyPressed.bind(this);
41
+ }
42
+ connectedCallback() {
43
+ super.connectedCallback();
44
+ document.body.addEventListener('key-pressed', this._keyPressed);
45
+ if (this.keys === undefined) {
46
+ this.keys = new AppKeys(document.body, 'Enter Esc'); //list the keys you want to identify (see comment above for grammer)
47
+ } else {
48
+ this.keys.connect();
49
+ }
50
+ }
51
+ disconnectedCallback() {
52
+ super.disconnectedCallback();
53
+ this.keys.disconnect();
54
+ document.body.removeEventListener('key-pressed', this._keyPressed);
55
+ }
56
+ _keyPressed(e) {
57
+ e.detail is an string containing the [<modifier>+]<key> combination from one you requested
58
+ }
59
+ ```
60
+
61
+ The second way of using this, better for when you don't want to react unless a particular area of the page has focus, or
62
+ if there are different areas of the page needing the respond depending which area
63
+ has focus. This is to create the AppKeys object in the firstUpdated function. Here is a possible example (just one area)
64
+
65
+ ```
66
+ connectedCallback() {
67
+ super.connectedCallback();
68
+ if (this.keys !== undefined) this.keys.connect();
69
+ }
70
+ disconnectedCallback() {
71
+ super.disconnectedCallback();
72
+ this.keys.disconnect();
73
+ }
74
+ firstUpdated() {
75
+ this.target = this.shadowRoot.querySelector('#keyreceive');
76
+ this.keys = new AppKeys(this.target, 'Enter'); //replace 'Enter' with space separated string of keys
77
+ }
78
+ ```
79
+ Then on the dom element in the Render function
80
+
81
+ `<div id="keyreceive" @keys-pressed=${this._keyPressed}>Contents</div>`
82
+
83
+ NOTE: There is no need to bind this._keyPressed to this (as shown in the first example). Lit manages it.
84
+
85
+ If for any reason you want more info about the keyPress, access
86
+ `this.keys.lastPressed`, and it will return the complete binding object
87
+
88
+
89
+ # config-promise
90
+
91
+ This module provides two functions:-
92
+
93
+ 1. This first is the default function which when called returns a promise.
94
+ 2. a `mockConfig` function which can be used by a test harness to provide the promise returned by the default function
95
+
96
+ If a request is made for the promise and it hasn't yet been created a new promise is made with a fetch get request to `/api/config`
97
+ to retrieve the configuration data. If the server is down, this request will continue attempting to get the value once a minute ad
98
+ infinitum.
99
+
100
+ The configuration data is returned to resolve the promise, but local session
101
+ storage also has one item added to it for each of the first level properties
102
+ of this data. If the property is a String, this is stored in the session
103
+ storage as is, otherwise the value is passed through `JSON.stringify` and that is stored.
104
+
105
+ # csv
106
+
107
+ the modules default export is a function which can be called with a name and optionally a parameters object. The name is added to `/api/csv/` to make a download request and if they exist the parameters object is turned into a query string. The response from that uri is downloaded (expected to be a csv file).
108
+
109
+ # debug
110
+
111
+ The purpose of this module is to provide a debugable capability which can be
112
+ dynamically switched on and off browser by setting a key in the config
113
+ returned by the server. It will post request to `/api/log` url with an
114
+ `application/json` body part containing message, topic and gap, where message is
115
+ the concatenation of the debug parameters separated by space, topic is the
116
+ topic of this debug message and gap is the number of milliseconds since the
117
+ last debug message with the same topic.
118
+
119
+ Topic data is held in a map, so this module can be used in multiple modules in
120
+ the client and if its the same topic then the gap will be since the last call
121
+ from **any** module.
122
+
123
+ To use the debug function in your code, import this module then set the topic
124
+ as shown.
125
+
126
+ import Debug from 'debug.js';
127
+
128
+ const debug = Debug('topic') topic should only contain the characters a-z or
129
+ A-Z as is converted to lowercase.
130
+
131
+ debug(..messages) //messages will be concatenated by space
132
+
133
+ the debug function will only log the message if config.debug (see
134
+ config-promise) is set to a string which is a comma separated list of topics
135
+ and that list has the topic for this debug call in it.
136
+
137
+ **Note**: the debug function checks with the server (via a debugconf api call)
138
+ to see if the topic is enabled. This is then cached for a minute, so any
139
+ calls around the same time will use the reply. This allows the server to
140
+ change what topics are available and for the client side to quickly find if it
141
+ should now start sending message
142
+
143
+ # dom-host
144
+
145
+ the default function of this module is called with a single parameter (normally `this` inside a custom element)
146
+ and it finds the parent. Its useful for custom elements to sit at the top level of the custom element above and listen for
147
+ events bubbling up from below. It uses this function to find the element to add an event listener to.
148
+
149
+ # master-tab-promise
150
+
151
+ the default function returns a promise, which when resolves will tell you if you are the first (and therefore master) tab in a particular application.
152
+
153
+ if the master tab closes an custom Event 'master-close' is dispatched on the window. You can use that to recheck the promise to see if you (or perhaps some other tab also still open is now master);
154
+
155
+
156
+ # pdf
157
+
158
+ the modules default export is a function which can be called with a name and optionally a parameters object. The name is added to `/api/pdf/` to post a message to the server, expecting it to stream a precreated, or created on the fly pdf document. This is opened in a new window.
159
+
160
+ # post-api
161
+
162
+ Provides a wrapper found the `fetch` api with error handling and built it post message support to a url prefixed with `/api/`.
163
+
164
+ # submit-function
165
+
166
+ Acts as the on-submit handler for a form. But instead of allowing the form to send itself it creates an ajax request with all the correct parameters.
167
+ In doing this it can traverse inside custom elements looking for input elements. It will also check for custom elements and if they have both a name and value property if can pretend to be in input. Also slots are also traversed for all assigned nodes.
168
+
169
+
170
+ # switch-path
171
+
172
+ Changed window location based on parameters provided
package/app-keys.js ADDED
@@ -0,0 +1,294 @@
1
+ /**
2
+ @licence
3
+ Copyright (c) 2020 Alan Chandler, all rights reserved
4
+
5
+ This file is part of @akc42/app-utils.
6
+
7
+ @akc42/app-utils is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ @akc42/app-utils is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ /*
22
+ `keys` is a module that provides keyboard support by normalising key
23
+ handling amongst browsers and providing a simple interface that can be used
24
+ by the application. Each keyboard usage request the user create a new
25
+ instance of the AppKeys class passing the key target in to the constructor.
26
+ This module will add and event handler for itself to that key target for the
27
+ keydown event. On that event it will parse the key combinations pressed and
28
+ fire a "key-pressed" event on the target.
29
+
30
+ The key grammer is of the form [<modifier>+]<key>[:<event>] and each of these is
31
+ space separated. optional <modifiers> (default none) are shift ctrl alt meta, and the optional
32
+ <events> are keydown and keyup (keydown is used as default)
33
+
34
+ To allow a using module the freedom to connect and disconnect to the dom, we
35
+ provide two methods to be called during the disconnectCallback and
36
+ connectCallback to disconnect and reconnect to this event
37
+
38
+ There are two models of using this, the first, shown below allows an element to add
39
+ the keys event to document body. This is good for pages that are swapped in and
40
+ out by a paging mechanism so that only one page is in the dom at a given time This is
41
+ good if only one handler is needed for the entire page.
42
+
43
+ NOTE: If you take this approach limit the keys to non function keys as the main app, with its main
44
+ menu may add a handler for the function keys and both menus, and any actions you choose would run at the same time.
45
+
46
+ constructor() {
47
+ super();
48
+ ...
49
+ this._keyPressed = this._keyPressed.bind(this);
50
+ }
51
+ connectedCallback() {
52
+ super.connectedCallback();
53
+ document.body.addEventListener('key-pressed', this._keyPressed);
54
+ if (this.keys === undefined) {
55
+ this.keys = new AppKeys(document.body, 'Enter Esc'); //list the keys you want to identify (see comment above for grammer)
56
+ } else {
57
+ this.keys.connect();
58
+ }
59
+ }
60
+ disconnectedCallback() {
61
+ super.disconnectedCallback();
62
+ this.keys.disconnect();
63
+ document.body.removeEventListener('key-pressed', this._keyPressed);
64
+ }
65
+ _keyPressed(e) {
66
+ e.detail is an string containing the [<modifier>+]<key> combination from one you requested
67
+ }
68
+
69
+ The second way of using this, better for when you don't want to react unless a particular area of the page has focus, or
70
+ if there are different areas of the page needing the respond depending which area
71
+ has focus. This is to create the AppKeys object in the firstUpdated function. Here is a possible example (just one area)
72
+
73
+
74
+ connectedCallback() {
75
+ super.connectedCallback();
76
+ if (this.keys !== undefined) this.keys.connect();
77
+ }
78
+ disconnectedCallback() {
79
+ super.disconnectedCallback();
80
+ this.keys.disconnect();
81
+ }
82
+ firstUpdated() {
83
+ this.target = this.shadowRoot.querySelector('#keyreceive');
84
+ this.keys = new AppKeys(this.target, 'Enter'); //replace 'Enter' with space separated string of keys
85
+ }
86
+ Then on the dom element in the Render function
87
+ <div id="keyreceive" @keys-pressed=${this._keyPressed}>Contents</div>
88
+
89
+ NOTE: There is no need to bind this._keyPressed to this (as in the first example). Lit manages it.
90
+
91
+ If for any reason you want more info about the keyPress, access
92
+ this.keys.lastPressed, and it will return the complete binding object
93
+
94
+ NOTE: I considered making this a mixin to avoid some of this verbosity, but decided in
95
+ the end the extra was worth it for the flexibility of the different use cases.
96
+ */
97
+ /*
98
+ Initial constants are taken from iron-a11y-keys behavior with the following licence. This same licences applies to those portions.
99
+
100
+ Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
101
+ This code may only be used under the BSD style license found at
102
+ http://polymer.github.io/LICENSE.txt The complete set of authors may be found at
103
+ http://polymer.github.io/AUTHORS.txt The complete set of contributors may be
104
+ found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as
105
+ part of the polymer project is also subject to an additional IP rights grant
106
+ found at http://polymer.github.io/PATENTS.txt
107
+ */
108
+
109
+ /**
110
+ * Special table for KeyboardEvent.keyCode. only used when KeyBoardEvent.key can't be
111
+ *
112
+ * Values from:
113
+ * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
114
+ */
115
+
116
+ const KEY_CODE = {
117
+ 8: 'backspace',
118
+ 9: 'tab',
119
+ 13: 'enter',
120
+ 27: 'esc',
121
+ 33: 'pageup',
122
+ 34: 'pagedown',
123
+ 35: 'end',
124
+ 36: 'home',
125
+ 32: 'space',
126
+ 37: 'left',
127
+ 38: 'up',
128
+ 39: 'right',
129
+ 40: 'down',
130
+ 46: 'del',
131
+ 106: '*'
132
+ };
133
+ /**
134
+ * MODIFIER_KEYS maps the short name for modifier keys used in a key
135
+ * combo string to the property name that references those same keys
136
+ * in a KeyboardEvent instance.
137
+ */
138
+
139
+ const MODIFIER_KEYS = {
140
+ 'shift': 'shift',
141
+ 'ctrl': 'control',
142
+ 'alt': 'alt',
143
+ 'meta': 'meta'
144
+ };
145
+ /**
146
+ * KeyboardEvent.key is mostly represented by printable character made by
147
+ * the keyboard, with unprintable keys labeled nicely.
148
+ *
149
+ * However, on OS X, Alt+char can make a Unicode character that follows an
150
+ * Apple-specific mapping. In this case, we fall back to .keyCode.
151
+ */
152
+
153
+ const KEY_CHAR = /[a-z0-9*]/;
154
+
155
+ /**
156
+ * Matches arrow keys in Gecko 27.0+
157
+ */
158
+
159
+ const ARROW_KEY = /^arrow/;
160
+ /**
161
+ * Matches space keys everywhere (notably including IE10's exceptional name
162
+ * `spacebar`).
163
+ */
164
+
165
+ const SPACE_KEY = /^space(bar)?/;
166
+ /**
167
+ * Matches ESC key.
168
+ *
169
+ * Value from: http://w3c.github.io/uievents-key/#key-Escape
170
+ */
171
+
172
+ const ESC_KEY = /^escape$/;
173
+
174
+ class AppKeys {
175
+ constructor(target, keys, stop) {
176
+ this.stop = stop; //marker to say stop propagation;
177
+ if (!(target instanceof HTMLElement)) throw new Error('AppKeys required an HTML Element as target');
178
+ this.target = target;
179
+ this.lastPressed = null;
180
+ const keyArray = keys.split(' ');
181
+ this.eventBindings = {};
182
+ for (let keyString of keyArray) {
183
+ const [keyCombo, event] = keyString.split(':');
184
+ const keyBinding = { combo: keyCombo, modifiers: [], hasModifiers: false };
185
+ const keyComboArray = keyCombo.split('+');
186
+ keyBinding.key = keyComboArray.pop();
187
+ if (keyBinding.key.toLowerCase() in MODIFIER_KEYS) {
188
+ //we asked for a modifier so push it back
189
+ keyComboArray.push(keyBinding.key);
190
+ }
191
+ if (keyComboArray.length === 0) {
192
+ //There were no modifiers, but what if we want a single upperCase char
193
+ if (keyBinding.key.length === 1 && keyBinding.key.toLowerCase() !== keyBinding.key) {
194
+ keyBinding.hasModifiers = true;
195
+ keyBinding.modifiers.push('shift');
196
+ }
197
+ } else {
198
+ for (let modifier of keyComboArray) {
199
+ modifier = modifier.toLowerCase();
200
+ if (modifier in MODIFIER_KEYS) {
201
+ keyBinding.hasModifiers = true;
202
+ keyBinding.modifiers.push(modifier);
203
+ }
204
+ }
205
+ }
206
+ keyBinding.key = keyBinding.key.toLowerCase();
207
+ if (this.eventBindings[event ? event : 'keydown'] === undefined) {
208
+ this.eventBindings[event ? event : 'keydown'] = { keyBindings: [] };
209
+ }
210
+ this.eventBindings[event ? event : 'keydown'].keyBindings.push(keyBinding);
211
+ }
212
+ this._bindHandlers();
213
+ }
214
+ connect() {
215
+ if (!this.handlersBound) this._bindHandlers();
216
+ }
217
+ disconnect() {
218
+ if (this.handlersBound) {
219
+ for (let event in this.eventBindings) {
220
+ this.target.removeEventListener(event, this.eventBindings[event].boundHandler);
221
+ }
222
+ this.handlersBound = false;
223
+
224
+ }
225
+ }
226
+ _bindHandlers() {
227
+ for (let event in this.eventBindings) {
228
+ this.eventBindings[event].boundHandler = this._keyHandler.bind(this, this.eventBindings[event].keyBindings);
229
+ this.target.addEventListener(event, this.eventBindings[event].boundHandler);
230
+ }
231
+ this.handlersBound = true;
232
+
233
+ }
234
+ _keyHandler(keyBindings, e) {
235
+ if (e.defaultPrevented) return;
236
+ const key = e.key.toLowerCase();
237
+ let validKey = '';
238
+
239
+ if (key === ' ' || SPACE_KEY.test(key)) {
240
+ validKey = 'space';
241
+ } else if (ESC_KEY.test(key)) {
242
+ validKey = 'esc';
243
+ } else if (key.length == 1) {
244
+ if (KEY_CHAR.test(key)) {
245
+ validKey = key;
246
+ } else {
247
+ if (e.keyCode) {
248
+ if (e.keyCode in KEY_CODE) {
249
+ validKey = KEY_CODE[e.keyCode];
250
+ }
251
+ }
252
+ }
253
+ } else if (ARROW_KEY.test(key)) {
254
+ validKey = key.replace('arrow', '');
255
+ } else if (key == 'multiply') {
256
+ // numpad '*' can map to Multiply on IE/Windows
257
+ validKey = '*';
258
+ } else {
259
+ validKey = key;
260
+ }
261
+ for (let binding of keyBindings) {
262
+ if (binding.hasModifiers) {
263
+ let modifierNotFound = false;
264
+ for (let modifier of binding.modifiers) {
265
+ if (!e[modifier + 'Key']) {
266
+ modifierNotFound = true;
267
+ break;
268
+ }
269
+ }
270
+ if (modifierNotFound) return; //didn't have the right combination of modifiers
271
+ if (binding.key in MODIFIER_KEYS) {
272
+ //we only wanted the modifiers so if our key IS the modifier we wanted
273
+ if (MODIFIER_KEYS[binding.key] === validKey) {
274
+ this.lastPressed = binding;
275
+ if (this.stop) e.stopPropagation();
276
+ if (!this.target.dispatchEvent(new CustomEvent('key-pressed', {bubbles: true, composed: true, cancelable: true, detail: binding.combo}))) {
277
+ e.preventDefault();
278
+ }
279
+ return;
280
+ }
281
+ }
282
+ }
283
+ if (binding.key === validKey) {
284
+ this.lastPressed = binding
285
+ if (this.stop) e.stopPropagation();
286
+ if (!this.target.dispatchEvent(new CustomEvent('key-pressed', { bubbles: true, composed: true, cancelable: true, detail: binding.combo }))) {
287
+ e.preventDefault();
288
+ }
289
+ return;
290
+ }
291
+ }
292
+ }
293
+ }
294
+ export default AppKeys;
@@ -0,0 +1,72 @@
1
+ /**
2
+ @licence
3
+ Copyright (c) 2020 Alan Chandler, all rights reserved
4
+
5
+ This file is part of @akc42/app-utils.
6
+
7
+ @akc42/app-utils is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ @akc42/app-utils is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ let configPromise;
22
+
23
+ export function mockConfig(promise) {
24
+ configPromise = promise;
25
+ }
26
+
27
+ async function config() {
28
+ if (configPromise === undefined) {
29
+ let resolved = false;
30
+ let resolver;
31
+ configPromise = new Promise(accept => resolver = accept);
32
+ let text;
33
+ let config;
34
+ while (!resolved) {
35
+ try {
36
+ const response = await window.fetch('/api/config', { method: 'get' })
37
+ if (!response.ok) throw new CustomEvent('api-error', { composed: true, bubbles: true, detail: response.status });
38
+ text = await response.text();
39
+ config = JSON.parse(text);
40
+ /*
41
+ some uses of this put the config in sessionStorage, but that only works if every config value is a string
42
+ so we check if they are a string, otherwise we stringify them
43
+ */
44
+ for (const p in config) {
45
+ const v = config[p];
46
+ if (typeof v === 'string') {
47
+ sessionStorage.setItem(p, v);
48
+ } else {
49
+ sessionStorage.setItem(p,JSON.stringify(v));
50
+ }
51
+ }
52
+ resolver(config);
53
+ resolved = true;
54
+ } catch (err) {
55
+ if (err.detail === 502) {
56
+ //server down so wait a minute and try again;
57
+ await new Promise(accept => {
58
+ setTimeout(accept, 60000);
59
+ });
60
+ } else {
61
+ if (err.type === 'api-error') throw err; //just throw whatever error we had
62
+ //we failed to parse the json - the actual code should be in the text near the end;
63
+ throw new CustomEvent('api-error', { composed: true, bubbles: true, detail: parseInt(text.substr(-6, 3), 10) });
64
+ }
65
+ }
66
+ }
67
+ }
68
+ return configPromise;
69
+ }
70
+
71
+
72
+ export default config;
package/csv.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ @licence
3
+ Copyright (c) 2021 Alan Chandler, all rights reserved
4
+
5
+ This file is part of @akc42/app-utils.
6
+
7
+ @akc42/app-utils is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ @akc42/app-utils is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ import {generateUri} from './switch-path.js';
22
+ const link = document.createElement('a');
23
+ link.setAttribute('download', null);
24
+
25
+ const csv = (url, params) => {
26
+ let href;
27
+ if (typeof url == 'string') {
28
+ href = generateUri(`/api/csv/${url}`, params ?? {});
29
+ } else {
30
+ //we assume it is a URL object
31
+ href = url.href;
32
+ }
33
+ link.setAttribute('href', href);
34
+ link.click();
35
+ };
36
+
37
+ export default csv;
package/debug.js ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ @licence
3
+ Copyright (c) 2021 Alan Chandler, all rights reserved
4
+
5
+ This file is part of @akc42/app-utils.
6
+
7
+ @akc42/app-utils is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
11
+
12
+ @akc42/app-utils is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with @akc42/app-utils. If not, see <http://www.gnu.org/licenses/>.
19
+ */
20
+
21
+ /*
22
+ The purpose of this module is to provide a debugable capability which can be
23
+ dynamically switched on and off browser by setting a key in the config
24
+ returned by the server. It will post request to '/api/log' url with
25
+ application/json body part containing message, topic and gap, where message is
26
+ the concatenation of the debug parameters separated by space, topic is the
27
+ topic of this debug message and gap is the number of milliseconds since the
28
+ last debug message with the same topic.
29
+
30
+ Topic data is held in a map, so this module can be used in multiple modules in
31
+ the client and if its the same topic then the gap will be since the last call
32
+ from ANY module.
33
+
34
+ To use the debug function in your code, import this module then set the topic
35
+ as shown.
36
+
37
+ import Debug from 'debug.js';
38
+
39
+ const debug = Debug('topic') topic should only contain the characters a-z or
40
+ A-Z as is converted to lowercase.
41
+
42
+ debug(..messages) //messages will be concatenated by space
43
+
44
+ the debug function will only log the message if config.debug (see
45
+ config-promise) is set to a string which is a comma separated list of topics
46
+ and that list has the topic for this debug call in it.
47
+
48
+ NOTE: It is normally expected for the server to provide a mechanism to update
49
+ the config before it is returned and for the client to be restarted to enable
50
+ the appropriate debug topics, an alternative could be for client side function
51
+ to use the `mockConfig` call to replace the promise with one which contained a
52
+ different list of topics. debug checks the list of topics on every call so
53
+ would dynamically pick up the changes
54
+
55
+ */
56
+
57
+ const topicMap = new Map();
58
+
59
+ import api from './post-api.js';
60
+
61
+ function Debug (t) {
62
+ if (typeof t !== 'string' || t.length === 0 || !/^[a-zA-Z]+$/.test(t)) {
63
+ console.error('Debug requires topic which is a non zero length string of only letters', t, 'Received');
64
+ throw new Error('Invalid Debug Topic');
65
+ }
66
+ const tl = t.toLowerCase();
67
+
68
+ if (topicMap.has(tl) ) {
69
+ const topic = topicMap.get(tl);
70
+ return topic.debug;
71
+ }
72
+
73
+ const topicHandler = {
74
+ topic: tl,
75
+ timestamp: new Date().getTime(),
76
+ expires: 0, //when our knowledge of enabled expires
77
+ enabled: false, //is this topic enabled
78
+ debug: async function (...args) {
79
+ //do time calc before potential delay to see if we are enabled
80
+ const now = new Date().getTime();
81
+ const gap = now - this.timestamp;
82
+ this.timestamp = now;
83
+ if (now > this.expires) {
84
+ //do this first so following requests don't try to find out again whilst I am checking, wasting calls
85
+ this.expires = now + 60000;
86
+ //expired so find out if topic is enabled
87
+ this.enabled = await api(`debugconf/${this.topic}`);
88
+
89
+ }
90
+ if (this.enabled) {
91
+ const message = args.reduce((cum, arg) => {
92
+ return `${cum} ${arg}`.trim();
93
+ }, '');
94
+ console.log(`+${gap}ms`, this.topic, message);
95
+ const blob = new Blob([JSON.stringify({
96
+ message: message,
97
+ gap: gap
98
+ })], { type: 'application/json' })
99
+
100
+ navigator.sendBeacon(`/api/debuglog/${this.topic}`, blob);
101
+ }
102
+ }
103
+ }
104
+ topicHandler.debug = topicHandler.debug.bind(topicHandler);
105
+ topicMap.set(topicHandler.topic, topicHandler);
106
+ return topicHandler.debug
107
+ }
108
+
109
+ export default Debug;