├── .editorconfig
├── .eslintignore
├── .eslintrc.json
├── .github
└── FUNDING.yml
├── .gitignore
├── JavaScript
├── api
│ ├── about.js
│ ├── contacts.js
│ ├── fields.js
│ ├── links.js
│ ├── stack.js
│ └── team.js
├── server.js
└── static
│ ├── 404.html
│ ├── console.css
│ ├── console.js
│ ├── favicon.ico
│ ├── favicon.png
│ ├── index.html
│ ├── manifest.json
│ ├── metarhia.png
│ ├── metarhia.svg
│ └── worker.js
├── LICENSE
└── README.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | end_of_line = lf
6 | charset = utf-8
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [{*.js,*.mjs,*.ts,*.json,*.yml}]
11 | indent_size = 2
12 | indent_style = space
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": "eslint:recommended",
8 | "parserOptions": {
9 | "ecmaVersion": "latest"
10 | },
11 | "globals": {
12 | "BigInt": true
13 | },
14 | "rules": {
15 | "indent": [
16 | "error",
17 | 2
18 | ],
19 | "linebreak-style": [
20 | "error",
21 | "unix"
22 | ],
23 | "quotes": [
24 | "error",
25 | "single"
26 | ],
27 | "semi": [
28 | "error",
29 | "always"
30 | ],
31 | "no-loop-func": [
32 | "error"
33 | ],
34 | "block-spacing": [
35 | "error",
36 | "always"
37 | ],
38 | "camelcase": [
39 | "error"
40 | ],
41 | "eqeqeq": [
42 | "error",
43 | "always"
44 | ],
45 | "strict": [
46 | "error",
47 | "global"
48 | ],
49 | "brace-style": [
50 | "error",
51 | "1tbs",
52 | {
53 | "allowSingleLine": true
54 | }
55 | ],
56 | "comma-style": [
57 | "error",
58 | "last"
59 | ],
60 | "comma-spacing": [
61 | "error",
62 | {
63 | "before": false,
64 | "after": true
65 | }
66 | ],
67 | "eol-last": [
68 | "error"
69 | ],
70 | "func-call-spacing": [
71 | "error",
72 | "never"
73 | ],
74 | "key-spacing": [
75 | "error",
76 | {
77 | "beforeColon": false,
78 | "afterColon": true,
79 | "mode": "minimum"
80 | }
81 | ],
82 | "keyword-spacing": [
83 | "error",
84 | {
85 | "before": true,
86 | "after": true,
87 | "overrides": {
88 | "function": {
89 | "after": false
90 | }
91 | }
92 | }
93 | ],
94 | "max-len": [
95 | "error",
96 | {
97 | "code": 80,
98 | "ignoreUrls": true
99 | }
100 | ],
101 | "max-nested-callbacks": [
102 | "error",
103 | {
104 | "max": 7
105 | }
106 | ],
107 | "new-cap": [
108 | "error",
109 | {
110 | "newIsCap": true,
111 | "capIsNew": false,
112 | "properties": true
113 | }
114 | ],
115 | "new-parens": [
116 | "error"
117 | ],
118 | "no-lonely-if": [
119 | "error"
120 | ],
121 | "no-trailing-spaces": [
122 | "error"
123 | ],
124 | "no-unneeded-ternary": [
125 | "error"
126 | ],
127 | "no-whitespace-before-property": [
128 | "error"
129 | ],
130 | "object-curly-spacing": [
131 | "error",
132 | "always"
133 | ],
134 | "operator-assignment": [
135 | "error",
136 | "always"
137 | ],
138 | "operator-linebreak": [
139 | "error",
140 | "after"
141 | ],
142 | "semi-spacing": [
143 | "error",
144 | {
145 | "before": false,
146 | "after": true
147 | }
148 | ],
149 | "space-before-blocks": [
150 | "error",
151 | "always"
152 | ],
153 | "space-before-function-paren": [
154 | "error",
155 | {
156 | "anonymous": "never",
157 | "named": "never",
158 | "asyncArrow": "always"
159 | }
160 | ],
161 | "space-in-parens": [
162 | "error",
163 | "never"
164 | ],
165 | "space-infix-ops": [
166 | "error"
167 | ],
168 | "space-unary-ops": [
169 | "error",
170 | {
171 | "words": true,
172 | "nonwords": false,
173 | "overrides": {
174 | "typeof": false
175 | }
176 | }
177 | ],
178 | "no-unreachable": [
179 | "error"
180 | ],
181 | "no-global-assign": [
182 | "error"
183 | ],
184 | "no-self-compare": [
185 | "error"
186 | ],
187 | "no-unmodified-loop-condition": [
188 | "error"
189 | ],
190 | "no-constant-condition": [
191 | "error",
192 | {
193 | "checkLoops": false
194 | }
195 | ],
196 | "no-console": [
197 | "off"
198 | ],
199 | "no-useless-concat": [
200 | "error"
201 | ],
202 | "no-useless-escape": [
203 | "error"
204 | ],
205 | "no-shadow-restricted-names": [
206 | "error"
207 | ],
208 | "no-use-before-define": [
209 | "error",
210 | {
211 | "functions": false
212 | }
213 | ],
214 | "arrow-parens": [
215 | "error",
216 | "always"
217 | ],
218 | "arrow-body-style": [
219 | "error",
220 | "as-needed"
221 | ],
222 | "arrow-spacing": [
223 | "error"
224 | ],
225 | "no-confusing-arrow": [
226 | "error",
227 | {
228 | "allowParens": true
229 | }
230 | ],
231 | "no-useless-computed-key": [
232 | "error"
233 | ],
234 | "no-useless-rename": [
235 | "error"
236 | ],
237 | "no-var": [
238 | "error"
239 | ],
240 | "object-shorthand": [
241 | "error",
242 | "always"
243 | ],
244 | "prefer-arrow-callback": [
245 | "error"
246 | ],
247 | "prefer-const": [
248 | "error"
249 | ],
250 | "prefer-numeric-literals": [
251 | "error"
252 | ],
253 | "prefer-rest-params": [
254 | "error"
255 | ],
256 | "prefer-spread": [
257 | "error"
258 | ],
259 | "rest-spread-spacing": [
260 | "error",
261 | "never"
262 | ],
263 | "template-curly-spacing": [
264 | "error",
265 | "never"
266 | ],
267 | "consistent-return": [
268 | "error",
269 | { "treatUndefinedAsUnspecified": true }
270 | ]
271 | }
272 | }
273 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: tshemsedinov
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .DS_Store
4 |
--------------------------------------------------------------------------------
/JavaScript/api/about.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = async () => [
4 | 'Metarhia is a Community and Technology Stack',
5 | 'for Distributed Highload Applications and Data Storage',
6 | '',
7 | 'Activities:',
8 | '• Academic fields: Research, Education and Open Lectures',
9 | '• Open Source Contribution e.g. Node.js, Impress, Metasync, etc.',
10 | '• Services and Products',
11 | '',
12 | 'Metarhia provides following services:',
13 | '• Software Development',
14 | '• Software Audit, Quality Control and Code Review',
15 | '• Business Processes Analysis',
16 | '• Architecture Solutions and Consulting',
17 | '• Database structure and technical specification',
18 | '• Project planning: time and cost estimation',
19 | '• Education, Training, Team building and Recruiting',
20 | '',
21 | 'Metarhia is a group of IT professionals, located in Kiev (Ukraine)',
22 | 'and working together in software development, internet solutions',
23 | 'and production automation. We are experienced in development and',
24 | 'system integration, ours architects are over 20 years in information',
25 | 'technologies. Ours software developers have practical knowledge in',
26 | 'programming including C, C++, JavaScript, Rust, Go, Swift, Java,',
27 | 'Objective-C, Kotlin, C#, Delphi, Assembler, Python, Haskell, etc.',
28 | 'We provide solutions for Unix/Linux, Windows, OSX, Android, Internet',
29 | 'solutions and Embedded systems.'
30 | ];
31 |
--------------------------------------------------------------------------------
/JavaScript/api/contacts.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = async () => [
4 | 'Timur Shemsedinov',
5 | 'tshemsedinov@github',
6 | 'timur.shemsedinov@gmail.com',
7 | 'tshemsedinov@facebook',
8 | 'marcusaurelius@habrahabr',
9 | ];
10 |
--------------------------------------------------------------------------------
/JavaScript/api/fields.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = async () => [
4 | 'Software fields:',
5 | '• Highload applications, Scaling and Performance optimization',
6 | '• Corporate applications, Databases and Information Systems',
7 | '• Systems Architecture and System Integration',
8 | '• Interactive applications, games and second screen TV solutions',
9 | '• Clusterware and massively parallel distributed cluster/cloud',
10 | '• Big-data and big-memory solutions',
11 | '• Deep learning, Neural networks, Data analysis',
12 | '• Production automation, cybernetics, telemetry',
13 | '• Network applications and protocols',
14 | '• Mobile, Desktop and Web GUI Applications',
15 | '• Embedded systems, Hardware and System Programming',
16 | '',
17 | 'Applied fields:',
18 | '• eGovernance Information Systems',
19 | '• Health Information Systems',
20 | '• Financial analytics and Trading Systems',
21 | '• Asset tracking solutions, RFID & GPS navigation',
22 | '• RFID (Radio Frequency IDentification)',
23 | '• Oil and gas transporting automation and telemetry',
24 | '• Social networking and Messaging solutions',
25 | '• Document flow automation, Timing and Planning solutions',
26 | '• Expert systems and CAD/CAM software',
27 | ];
28 |
--------------------------------------------------------------------------------
/JavaScript/api/links.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = async () => [
4 | 'Links:',
5 | '• Open Source code https://github.com/metarhia',
6 | '• Educational code Examples https://github.com/HowProgrammingWorks',
7 | '',
8 | 'Meetup Groups:',
9 | '• https://www.meetup.com/HowProgrammingWorks',
10 | '• https://www.meetup.com/NodeUA',
11 | '• https://www.meetup.com/KievNodeJS',
12 | '',
13 | 'Telegram Channels:',
14 | '• https://t.me/HowProgrammingWorks',
15 | '• https://t.me/metarhia',
16 | '',
17 | 'Telegram Groups:',
18 | '• https://t.me/MetarhiaHPW',
19 | '• https://t.me/nodeua',
20 | ];
21 |
--------------------------------------------------------------------------------
/JavaScript/api/stack.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = async () => [
4 | 'Metarhia Technology Stack Key Ideas:',
5 | '• Unification: API, data, contracts',
6 | '• Homogeneity of server infrastructure',
7 | '• No back compatibility (no legacy support)',
8 | '• Open Source & no vendor lock',
9 | '• Architectural decisions (single plan)',
10 | '• Community and trainings',
11 | '',
12 | 'Impress Application Server',
13 | 'for highload clusters and private clouds with Node.js',
14 | 'https://github.com/metarhia/impress',
15 | '',
16 | 'JSTP protocol',
17 | 'RPC, event bus, db sync on TCP, TLS, WebSocket and JSON5;',
18 | 'with SDK for mobile and desktop: JavaScript, Java, Swift,',
19 | 'C++, Haskell, Python, Objective-C, PHP, Golang, C#',
20 | 'https://github.com/metarhia/jstp',
21 | '',
22 | 'GlobalStorage',
23 | 'Distributed reactive in-memory DBMS, work in progress',
24 | 'https://github.com/metarhia/globalstorage',
25 | '',
26 | 'Metasync: asynchronous programming abstractions',
27 | 'https://github.com/metarhia/metasync',
28 | '',
29 | 'Maojian: testing framework',
30 | 'https://github.com/metarhia/maojian',
31 | ];
32 |
--------------------------------------------------------------------------------
/JavaScript/api/team.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = async () => [
4 | 'Metarhia core team:',
5 | '@tshemsedinov @aqrln @belochub @nechaido @GYFK @lidaamber',
6 | '@lundibundi @GreatAndPowerfulKing @grimelion @DzyubSpirit',
7 | '@RayGron @alinkedd @j-martyn @johnbizokk @bugagashenkj',
8 | '@Kowalski0805 @dimanadko @kuvichkamaksim @mille-nium',
9 | '@o-rumiantsev @Tariod',
10 | ];
11 |
--------------------------------------------------------------------------------
/JavaScript/server.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('node:fs');
4 | const http = require('node:http');
5 | const path = require('node:path');
6 |
7 | const PORT = 8000;
8 |
9 | const STATIC_PATH = path.join(process.cwd(), './static');
10 | const API_PATH = './api/';
11 |
12 | const MIME_TYPES = {
13 | default: 'application/octet-stream',
14 | html: 'text/html; charset=UTF-8',
15 | js: 'application/javascript; charset=UTF-8',
16 | json: 'application/json',
17 | css: 'text/css',
18 | png: 'image/png',
19 | jpg: 'image/jpg',
20 | gif: 'image/gif',
21 | ico: 'image/x-icon',
22 | svg: 'image/svg+xml',
23 | };
24 |
25 | const toBool = [() => true, () => false];
26 |
27 | const prepareFile = async (url) => {
28 | const paths = [STATIC_PATH, url];
29 | if (url.endsWith('/')) paths.push('index.html');
30 | const filePath = path.join(...paths);
31 | const pathTraversal = !filePath.startsWith(STATIC_PATH);
32 | const exists = await fs.promises.access(filePath).then(...toBool);
33 | const found = !pathTraversal && exists;
34 | const streamPath = found ? filePath : STATIC_PATH + '/404.html';
35 | const ext = path.extname(streamPath).substring(1).toLowerCase();
36 | const stream = fs.createReadStream(streamPath);
37 | return { found, ext, stream };
38 | };
39 |
40 | const api = new Map();
41 |
42 | const receiveArgs = async (req) => {
43 | const buffers = [];
44 | for await (const chunk of req) buffers.push(chunk);
45 | const data = Buffer.concat(buffers).toString();
46 | return JSON.parse(data);
47 | };
48 |
49 | const cacheFile = (name) => {
50 | const filePath = API_PATH + name;
51 | const key = path.basename(filePath, '.js');
52 | try {
53 | const libPath = require.resolve(filePath);
54 | delete require.cache[libPath];
55 | } catch {
56 | return;
57 | }
58 | try {
59 | const method = require(filePath);
60 | api.set(key, method);
61 | } catch {
62 | api.delete(name);
63 | }
64 | };
65 |
66 | const cacheFolder = (path) => {
67 | fs.readdir(path, (err, files) => {
68 | if (!err) files.forEach(cacheFile);
69 | });
70 | };
71 |
72 | const watch = (path) => {
73 | fs.watch(path, (event, file) => {
74 | cacheFile(file);
75 | });
76 | };
77 |
78 | cacheFolder(API_PATH);
79 | watch(API_PATH);
80 |
81 | const httpError = (res, status, message) => {
82 | res.statusCode = status;
83 | res.end(`"${message}"`);
84 | };
85 |
86 | http.createServer(async (req, res) => {
87 | const [first, second] = req.url.substring(1).split('/');
88 | if (first === 'api') {
89 | const method = api.get(second);
90 | const args = await receiveArgs(req);
91 | try {
92 | const result = await method(...args);
93 | if (!result) {
94 | httpError(res, 500, 'Server error');
95 | return;
96 | }
97 | res.end(JSON.stringify(result));
98 | } catch (err) {
99 | console.dir({ err });
100 | httpError(res, 500, 'Server error');
101 | }
102 | } else {
103 | const file = await prepareFile(req.url);
104 | const statusCode = file.found ? 200 : 404;
105 | const mimeType = MIME_TYPES[file.ext] || MIME_TYPES.default;
106 | res.writeHead(statusCode, { 'Content-Type': mimeType });
107 | file.stream.pipe(res);
108 | console.log(`${req.method} ${req.url} ${statusCode}`);
109 | }
110 | }).listen(PORT);
111 |
112 | console.log(`Server running at http://127.0.0.1:${PORT}/`);
113 |
--------------------------------------------------------------------------------
/JavaScript/static/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Error 404: File not found
6 |
7 |
8 | Error 404: File not found
9 |
10 |
11 |
--------------------------------------------------------------------------------
/JavaScript/static/console.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | outline: none;
5 | cursor: default;
6 | -moz-user-select: inherit;
7 | }
8 | a, a * {
9 | cursor: pointer;
10 | }
11 | html, body {
12 | padding: 0;
13 | margin: 0;
14 | font-family: 'Share Tech Mono', monospace;
15 | font-size: 11pt;
16 | overflow: hidden;
17 | height: 100%;
18 | background: #000;
19 | color: #FFF;
20 | }
21 | body {
22 | height: 100%;
23 | }
24 | input, textarea {
25 | -webkit-border-radius: 0;
26 | -webkit-touch-callout: text;
27 | -webkit-user-select: text;
28 | -khtml-user-select: text;
29 | -moz-user-select: text;
30 | -ms-user-select: text;
31 | user-select: text;
32 | }
33 | ul,ol {
34 | padding: 0 0 0 30px;
35 | }
36 | #screenConsole {
37 | height: 100%;
38 | }
39 | #screenConsole > div {
40 | overflow: hidden;
41 | }
42 | #panelColors {
43 | height: 100%;
44 | width: 8px;
45 | float: left;
46 | background: #FF0000;
47 | }
48 | .colorA { background: #AA0077; height: 40px; }
49 | .colorB { background: #552277; }
50 | .colorC { background: #2222BB; }
51 | .colorD { background: #0033DD; }
52 | .colorE { background: #006699; }
53 | .colorF { background: #009933; }
54 | .colorG { background: #7FD900; }
55 | .colorH { background: #F2F200; }
56 | .colorI { background: #FFBF00; }
57 | .colorJ { background: #FF8C00; }
58 | .colorK { background: #FF3300; }
59 | .colorL { background: #FF0000; height: 40px; }
60 | #panelLogo {
61 | margin: 4px;
62 | position: absolute;
63 | top: 0;
64 | left: 8px;
65 | right: 8px;
66 | background: #000;
67 | }
68 | #panelConsole {
69 | position: absolute;
70 | left: 8px;
71 | right: 8px;
72 | top: 8px;
73 | bottom: 0;
74 | }
75 | #panelScroll {
76 | height: 100%;
77 | width: 8px;
78 | float: right;
79 | background: #262626;
80 | }
81 | #controlScroll {
82 | background: #009933;
83 | position: absolute;
84 | width: 8px;
85 | height: 25px;
86 | bottom: 0px;
87 | }
88 | #panelColors > div {
89 | height: 25pt;
90 | }
91 | #controlShadow {
92 | position: absolute;
93 | top: -10px;
94 | left: 0;
95 | right: 0;
96 | height: 10px;
97 | box-shadow: 0px 0px 20px 6px rgba(0,0,0,1);
98 | z-index: 1;
99 | }
100 | #controlBrowse {
101 | padding: 4px;
102 | color: #009933;
103 | overflow-x: hidden;
104 | overflow-y: scroll;
105 | position: absolute;
106 | left: 0;
107 | right: 0;
108 | top: 0;
109 | bottom: 0;
110 | }
111 | #controlBrowse td {
112 | padding: 2px 4px;
113 | }
114 | #controlBrowse th {
115 | padding: 2px 4px;
116 | font-weight: bold;
117 | color: #FFBF00;
118 | }
119 | #controlBrowse tr:nth-child(even) {
120 | background: #101010;
121 | }
122 | #controlBrowse tr:nth-child(odd) {
123 | background: #262626;
124 | }
125 | #controlInput {
126 | color: #009933;
127 | }
128 | #controlInput span {
129 | animation: blinker 1s ease-out infinite;
130 | margin-left: 2px;
131 | }
132 | @keyframes blinker {
133 | 50% { opacity: 0.0; }
134 | }
135 | #controlKeyboard {
136 | background: #000;
137 | position: absolute;
138 | bottom: 0;
139 | right: 0;
140 | left: 0;
141 | }
142 | #controlKeyboard .key {
143 | font-size: 1.4em;
144 | background: #262626;
145 | display: inline-block;
146 | text-align: center;
147 | width: 10%;
148 | height: 25px;
149 | }
150 | .caps {
151 | text-transform: uppercase;
152 | }
153 | ::-webkit-scrollbar {
154 | display: none;
155 | }
156 | #controlBrowseSpacer {
157 | height: 100%;
158 | }
159 |
--------------------------------------------------------------------------------
/JavaScript/static/console.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | console.log('JavaScript has loaded');
4 |
5 | const registerServiceWorker = () => {
6 | if (!Reflect.has(navigator, 'serviceWorker')) {
7 | console.log('Service workers are not supported');
8 | return;
9 | }
10 | const { serviceWorker } = navigator;
11 | serviceWorker.register('/worker.js').then((registration) => {
12 | if (registration.installing) {
13 | console.log('Service worker installing');
14 | console.log(registration.installing);
15 | return;
16 | }
17 | if (registration.waiting) {
18 | console.log('Service worker installed');
19 | console.log(registration.waiting);
20 | return;
21 | }
22 | if (registration.active) {
23 | console.log('Service worker active');
24 | console.log(registration.active);
25 | return;
26 | }
27 | }).catch((error) => {
28 | console.log('Registration failed');
29 | console.log(error);
30 | });
31 | };
32 |
33 | window.addEventListener('load', () => {
34 | console.log('The page has loaded');
35 | registerServiceWorker();
36 | });
37 |
38 | window.addEventListener('beforeinstallprompt', (event) => {
39 | console.log('Installing PWA');
40 | console.dir({ beforeinstallprompt: event });
41 | });
42 |
43 | window.addEventListener('appinstalled', (event) => {
44 | console.log('PWA installed');
45 | console.dir({ appinstalled: event });
46 | });
47 |
48 | // AJAX API Builder
49 |
50 | const buildAPI = (methods) => {
51 | const api = {};
52 | for (const method of methods) {
53 | api[method] = (...args) => new Promise((resolve, reject) => {
54 | const url = `/api/${method}`;
55 | console.log(url, args);
56 | fetch(url, {
57 | method: 'POST',
58 | headers: { 'Content-Type': 'application/json' },
59 | body: JSON.stringify(args),
60 | }).then((res) => {
61 | const { status } = res;
62 | if (status !== 200) {
63 | reject(new Error(`Status Code: ${status}`));
64 | return;
65 | }
66 | resolve(res.json());
67 | });
68 | });
69 | }
70 | return api;
71 | };
72 |
73 | // Console Emulation
74 |
75 | const ALPHA_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
76 | const ALPHA_LOWER = 'abcdefghijklmnopqrstuvwxyz';
77 | const ALPHA = ALPHA_UPPER + ALPHA_LOWER;
78 | const DIGIT = '0123456789';
79 | const CHARS = ALPHA + DIGIT;
80 | const TIME_LINE = 300;
81 | const TIME_CHAR = 20;
82 |
83 | const KEY_CODE = {
84 | BACKSPACE: 8, TAB: 9, ENTER: 13, PAUSE: 19, ESC: 27, SPACE: 32,
85 | PGUP: 33, PGDN: 34, END: 35, HOME: 36,
86 | LT: 37, UP: 38, RT: 39, DN: 40, INS: 45, DEL: 46,
87 | F1: 112, F2: 113, F3: 114, F4: 115, F5: 116, F6: 117,
88 | F7: 118, F8: 119, F9: 120, F10: 121, F11: 122, F12: 123,
89 | ACCENT: 192,
90 | };
91 |
92 | const KEY_NAME = {};
93 | for (const keyName in KEY_CODE) KEY_NAME[KEY_CODE[keyName]] = keyName;
94 |
95 | let controlKeyboard, panelScroll;
96 | let controlInput, controlBrowse, controlScroll;
97 |
98 | const api = buildAPI(['about']);
99 |
100 | const pad = (padChar, length) => new Array(length + 1).join(padChar);
101 |
102 | const isMobile = () => (
103 | navigator.userAgent.match(/Android/i) ||
104 | navigator.userAgent.match(/webOS/i) ||
105 | navigator.userAgent.match(/iPhone/i) ||
106 | navigator.userAgent.match(/iPad/i) ||
107 | navigator.userAgent.match(/iPod/i) ||
108 | navigator.userAgent.match(/BlackBerry/i) ||
109 | navigator.userAgent.match(/Windows Phone/i)
110 | );
111 |
112 | let viewportHeight, viewableRatio;
113 | let contentHeight, scrollHeight;
114 | let thumbHeight, thumbPosition;
115 |
116 | const refreshScroll = () => {
117 | viewportHeight = controlBrowse.offsetHeight;
118 | contentHeight = controlBrowse.scrollHeight;
119 | viewableRatio = viewportHeight / contentHeight;
120 | scrollHeight = panelScroll.offsetHeight;
121 | thumbHeight = scrollHeight * viewableRatio;
122 | thumbPosition = controlBrowse.scrollTop * thumbHeight / viewportHeight;
123 | controlScroll.style.top = thumbPosition + 'px';
124 | controlScroll.style.height = thumbHeight + 'px';
125 | };
126 |
127 | const scrollBottom = () => {
128 | refreshScroll();
129 | controlBrowse.scrollTop = controlBrowse.scrollHeight;
130 | };
131 |
132 | const initScroll = () => {
133 | controlBrowse.scrollTop = controlBrowse.scrollHeight;
134 | controlBrowse.addEventListener('scroll', refreshScroll);
135 | window.addEventListener('orientationchange', () => {
136 | setTimeout(scrollBottom, 0);
137 | });
138 | };
139 |
140 | const showKeyboard = () => {
141 | if (!isMobile()) return;
142 | controlKeyboard.style.display = 'block';
143 | controlBrowse.style.bottom = controlKeyboard.offsetHeight + 'px';
144 | };
145 |
146 | const inputSetValue = (value) => {
147 | controlInput.inputValue = value;
148 | if (controlInput.inputType === 'masked') {
149 | value = pad('*', value.length);
150 | }
151 | value = value.replace(/ /g, ' ');
152 | controlInput.innerHTML = (
153 | controlInput.inputPrompt + value + '█'
154 | );
155 | };
156 |
157 | const input = (type, prompt, callback) => {
158 | showKeyboard();
159 | controlInput.style.display = 'none';
160 | controlBrowse.removeChild(controlInput);
161 | controlInput.inputActive = true;
162 | controlInput.inputPrompt = prompt;
163 | inputSetValue('');
164 | controlInput.inputType = type;
165 | controlInput.inputCallback = callback;
166 | controlBrowse.appendChild(controlInput);
167 | controlInput.style.display = 'block';
168 | setTimeout(scrollBottom, 0);
169 | };
170 |
171 | const clear = () => {
172 | const elements = controlBrowse.children;
173 | let element;
174 | for (let i = elements.length - 2; i > 1; i--) {
175 | element = elements[i];
176 | controlBrowse.removeChild(element);
177 | }
178 | };
179 |
180 | const print = (s) => {
181 | const list = Array.isArray(s);
182 | let line = list ? s.shift() : s;
183 | if (!line) line = '';
184 | const element = document.createElement('div');
185 | if (!line) line = '\xa0';
186 | if (line.charAt(0) === '<') {
187 | element.innerHTML += line;
188 | } else {
189 | const timer = setInterval(() => {
190 | const char = line.charAt(0);
191 | element.innerHTML += char;
192 | line = line.substr(1);
193 | if (!line) clearInterval(timer);
194 | controlBrowse.scrollTop = controlBrowse.scrollHeight;
195 | scrollBottom();
196 | }, TIME_CHAR);
197 | }
198 | if (list && s.length) setTimeout(print, TIME_LINE, s);
199 | controlBrowse.insertBefore(element, controlInput);
200 | controlBrowse.scrollTop = controlBrowse.scrollHeight;
201 | scrollBottom();
202 | };
203 |
204 | const enterKey = () => {
205 | input('masked', 'Key: ', (err, key) => {
206 | api.signin({ key }, (err, data) => {
207 | if (data.result === 'ok') {
208 | print('You are logged in');
209 | } else {
210 | print('Incorect key');
211 | enterKey();
212 | }
213 | });
214 | });
215 | };
216 |
217 | const format = (obj) => {
218 | let res = 'Parameter | Value |
';
219 | let key, val;
220 | for (key in obj) {
221 | val = obj[key];
222 | res += `${key} | ${val} |
`;
223 | }
224 | return res + '
';
225 | };
226 |
227 | const inputKeyboardEvents = {
228 | ESC() {
229 | inputSetValue('');
230 | },
231 | BACKSPACE() {
232 | let value = controlInput.inputValue;
233 | value = value.slice(0, -1);
234 | inputSetValue(value);
235 | },
236 | ENTER() {
237 | const result = controlInput.inputValue;
238 | let value = result;
239 | if (controlInput.inputType === 'masked') {
240 | value = pad('*', value.length);
241 | }
242 | print(controlInput.inputPrompt + value);
243 | controlInput.style.display = 'none';
244 | controlInput.inputActive = false;
245 | controlInput.inputCallback(null, value);
246 | },
247 | CAPS() {
248 | if (controlKeyboard.className === 'caps') {
249 | controlKeyboard.className = '';
250 | } else {
251 | controlKeyboard.className = 'caps';
252 | }
253 | },
254 | KEY(char) { // Alpha or Digit
255 | if (controlKeyboard.className === 'caps') {
256 | char = char.toUpperCase();
257 | }
258 | let value = controlInput.inputValue;
259 | value += char;
260 | inputSetValue(value);
261 | }
262 | };
263 |
264 | const makeKeyboardClick = (char) => (e) => {
265 | char = e.target.inputChar;
266 | if (char === '_') char = ' ';
267 | let keyName = 'KEY';
268 | if (char === '<') keyName = 'BACKSPACE';
269 | if (char === '>') keyName = 'ENTER';
270 | if (char === '^') keyName = 'CAPS';
271 | const fn = inputKeyboardEvents[keyName];
272 | if (fn) fn(char);
273 | e.stopPropagation();
274 | return false;
275 | };
276 |
277 | const initKeyboard = () => {
278 | if (!isMobile()) return;
279 | controlKeyboard.style.display = 'block';
280 | const KEYBOARD_LAYOUT = [
281 | '1234567890',
282 | 'qwertyuiop',
283 | 'asdfghjkl<',
284 | '^zxcvbnm_>'
285 | ];
286 | let i, j, char, keyboardClick;
287 | let keyboardLine, elementKey, elementLine;
288 | for (i = 0; i < KEYBOARD_LAYOUT.length; i++) {
289 | keyboardLine = KEYBOARD_LAYOUT[i];
290 | elementLine = document.createElement('div');
291 | controlKeyboard.appendChild(elementLine);
292 | for (j = 0; j < keyboardLine.length; j++) {
293 | char = keyboardLine[j];
294 | if (char === ' ') char = ' ';
295 | elementKey = document.createElement('div');
296 | elementKey.innerHTML = char;
297 | elementKey.inputChar = char;
298 | elementKey.className = 'key';
299 | elementKey.style.opacity = ((i + j) % 2) ? 0.8 : 1;
300 | keyboardClick = makeKeyboardClick(char);
301 | elementKey.addEventListener('click', keyboardClick);
302 | elementLine.appendChild(elementKey);
303 | }
304 | }
305 | controlBrowse.style.bottom = controlKeyboard.offsetHeight + 'px';
306 | };
307 |
308 | document.onkeydown = (event) => {
309 | let keyName, fn;
310 | if (controlInput.inputActive) {
311 | keyName = KEY_NAME[event.keyCode];
312 | fn = inputKeyboardEvents[keyName];
313 | if (fn) {
314 | fn();
315 | return false;
316 | }
317 | }
318 | };
319 |
320 | document.onkeypress = (event) => {
321 | if (controlInput.inputActive) {
322 | const fn = inputKeyboardEvents['KEY'];
323 | const char = String.fromCharCode(event.keyCode);
324 | if (CHARS.includes(char) && fn) {
325 | fn(char);
326 | return false;
327 | }
328 | }
329 | };
330 |
331 | const commandLoop = () => {
332 | input('command', '.', (err, line) => {
333 | exec(line);
334 | commandLoop();
335 | });
336 | };
337 |
338 | const commands = {};
339 |
340 | const help = [
341 | '', 'Commands: about, fields, team, links, stack, contacts'
342 | ];
343 |
344 | const exec = async (line) => {
345 | const args = line.split(' ');
346 | const cmd = args.shift();
347 | const data = await api[cmd](args);
348 | print(data);
349 | commandLoop();
350 | };
351 |
352 | window.addEventListener('load', () => {
353 | panelScroll = document.getElementById('panelScroll');
354 | controlInput = document.getElementById('controlInput');
355 | controlKeyboard = document.getElementById('controlKeyboard');
356 | controlBrowse = document.getElementById('controlBrowse');
357 | controlScroll = document.getElementById('controlScroll');
358 | initKeyboard();
359 | initScroll();
360 | const path = window.location.pathname.substring(1);
361 | print([
362 | 'Metarhia/KPI is a Research & Development Center',
363 | 'in Kiev Polytechnic Institute (ICT faculty)',
364 | ].concat(help));
365 | if (path) {
366 | setTimeout(() => {
367 | exec('contacts ' + path);
368 | window.history.replaceState(null, '', '/');
369 | }, TIME_LINE * 3);
370 | }
371 | commandLoop();
372 | });
373 |
--------------------------------------------------------------------------------
/JavaScript/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HowProgrammingWorks/ServiceWorker/95c326cb2d28995ee98822484b4faee21288a0e4/JavaScript/static/favicon.ico
--------------------------------------------------------------------------------
/JavaScript/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HowProgrammingWorks/ServiceWorker/95c326cb2d28995ee98822484b4faee21288a0e4/JavaScript/static/favicon.png
--------------------------------------------------------------------------------
/JavaScript/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Metarhia Console
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/JavaScript/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Metarhia",
3 | "name": "Metarhia console",
4 | "icons": [
5 | {
6 | "src": "/favicon.png",
7 | "type": "image/png",
8 | "sizes": "128x128"
9 | },
10 | {
11 | "src": "/metarhia.png",
12 | "type": "image/png",
13 | "sizes": "256x256"
14 | }
15 | ],
16 | "start_url": "/",
17 | "background_color": "#552277",
18 | "display": "standalone",
19 | "scope": "/",
20 | "theme_color": "#009933"
21 | }
22 |
--------------------------------------------------------------------------------
/JavaScript/static/metarhia.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HowProgrammingWorks/ServiceWorker/95c326cb2d28995ee98822484b4faee21288a0e4/JavaScript/static/metarhia.png
--------------------------------------------------------------------------------
/JavaScript/static/metarhia.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/JavaScript/static/worker.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const files = [
4 | '/',
5 | '/console.css',
6 | '/console.js',
7 | '/favicon.ico',
8 | '/favicon.png',
9 | '/manifest.json',
10 | '/metarhia.png',
11 | '/metarhia.svg',
12 | ];
13 |
14 | self.addEventListener('install', (event) => event.waitUntil(
15 | caches.open('v1').then((cache) => cache.addAll(files))
16 | ));
17 |
18 | self.addEventListener('fetch', (event) => {
19 | event.respondWith(caches.match(event.request).then((response) => {
20 | if (response !== undefined) return response;
21 | return fetch(event.request).then((response) => {
22 | const responseClone = response.clone();
23 | caches.open('v1').then((cache) => {
24 | cache.put(event.request, responseClone);
25 | });
26 | return response;
27 | }).catch((error) => {
28 | throw error;
29 | });
30 | }));
31 | });
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-2022 How.Programming.Works contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Service Workers for PWA caching, proxy and offline
2 |
3 | [](https://youtu.be/s7AIwZMTVPs)
4 |
--------------------------------------------------------------------------------