Add blog post about refactoring, with refactoring image and render source
[ndcode_site.git] / blog / 20220128 / index.html.jst
1 return async env => {
2   let blog_post = await _require('/_lib/blog_post.jst')
3   let fa_cloud_upload_alt = await env.site.get_min_svg('/_svg/fa_cloud-upload-alt.svg')
4   let fa_server = await env.site.get_min_svg('/blog/20220128/fa_server.svg')
5   let icon_cross = await env.site.get_min_svg('/_svg/icon_cross.svg')
6   let icon_tick = await env.site.get_min_svg('/_svg/icon_tick.svg')
7
8   await blog_post(
9     env,
10     // head
11     async _out => {},
12     // body
13     async _out => {
14       p {
15         'So after gaining considerable momentum to complete the refactoring project of the last 3–4 weeks, I am happy with things and can take a breather. Whilst the site still has the same basic functionality (read static content, sign up, verify email, password reset, sign in/out, change details and password), things are very different under the hood, and there are also visible UX improvements.'
16       }
17
18       p {
19         'The '
20         a (href="../20220114/index.html") {
21           tt {'LogJSON'}
22         }
23         ' database has been in a useable state for about 2 weeks and since then I have been changing all the flows to store the data (such as user-account data upon sign up) in the central '
24         tt {'LogJSON'}
25         ' database for the site, rather than individual JSON files. But since this required rewriting almost all code, I decided the rewrite would encompass the following points:'
26         ul {
27           li {
28             'Change data storage to use '
29             tt {'LogJSON'}
30             ' (the original goal of the rewrite).'
31           }
32           li {
33             'Partition the logic into API endpoints and pages.'
34           }
35           li {
36             'Rearrange the code into a sensible directory structure.'
37           }
38           li {
39             'UX improvements, such as custom forms and fewer pop-ups.'
40           }
41         }
42         'I will discuss each point in further detail next.'
43       }
44
45       h4 {
46         'Change data storage to use '
47         tt {'LogJSON'}
48       }
49
50       h5 {
51         'The experience with '
52         tt {'LogJSON'}
53         ' to date'
54       }
55
56       p {
57         'After some weeks of using '
58         tt {'LogJSON'}
59         ' for the backend storage, the experience has been very good. Compared to the traditional 2-dimensional structure of a relational database (SQL), the tree-like structure of '
60         tt {'LogJSON'}
61         ' proves to be quite convenient.'
62       }
63
64       p {
65         'The JSON structure of the database at the moment looks like '
66         pre {
67         `{
68   "globals": {...},
69   "sessions": {...},
70   "accounts": {...},
71   "navigation: {...}
72   "nodemailers: {...}
73 }
74 `
75         }
76         'where '
77         tt {'globals'}
78         ' stores miscellaneous information such as the site’s title and copyright message, '
79         tt {'sessions'}
80         ' stores information about each browser session connected to the site (including sign-in status), '
81         tt {'accounts'}
82         ' stores information about signed-up users, '
83         tt {'navigation'}
84         ' stores the navigation tree of the site (basically a mapping from directory names to page titles, in a hierarchical structure), and '
85         tt {'nodemailers'}
86         ' stores information about mail accounts and servers which we use for verifying users’ email addresses and similar purposes.'
87       }
88
89       p {
90         'The '
91         tt {'sessions'}
92         ' and '
93         tt {'accounts'}
94         ' JSON Objects are keyed by the user’s session or account identifier respectively, and lead to a JSON Object per session or account. These latter do not have a predefined structure, as any part of the business logic is able to add a new key to the session or account as required. This is useful because normally this kind of addition would require a SQL structure change or a new table, both of which are inconvenient and time-consuming to implement.'
95       }
96
97       p {
98         'Of course, it is also easy to add new top-level keys to the database for new functionality that doesn’t reside in the session or account. The general approach I have taken is that any code accessing the database and expecting a particular key should add that key if it does not exist. Ultimately we might want to have a schema for the database, and predefined objects with predefined field sets. This would also work well, but would require a bit more effort upfront.'
99       }
100
101       p {
102         'From a historical perspective, my understanding was that IBM was making object-oriented databases in the 70s and 80s before Oracle popularized the relational paradigm with SQL in the late 80s and 90s. Having taught the relational algebra at undergraduate level, I appreciate there are many good things about it, and I would not want to dismiss it out of hand. But I still think that the idea of object databases deserves reconsideration, hence my experiment here.'
103       }
104
105       h5 {
106         'Use of '
107         tt {'LogJSON'}
108         ' transactions'
109       }
110
111       p {
112         'In my initial attempt at the conversion to '
113         tt {'LogJSON'}
114         ', I was trying to keep the number of transactions to a minimum, particularly considering that all parent objects to a changed object must be rewritten to the log every transaction.'
115       }
116
117       p {
118         'So each API endpoint would, for example, invoke the code to generate a new session cookie and enter or locate a session object in the database, after having opened the transaction it would use to perform its own operation. This avoided having an extra transaction just to deal with the session cookie, but also made the logic for each API endpoint more complex. A similar issue arose when serving pages and dealing with analytics or sign in/out status for the navbar.'
119       }
120
121       p {
122         'A particularly annoying thing about this ‘optimization’ was that transactions which were inherently read-only were becoming read-write in general, since they might write a new session even if the rest of the transaction was only reading. This added considerable complexity to the read-only transactions such as ‘get’ API endpoints and the navbar template.'
123       }
124
125       p {
126         'Therefore, I eventually realized that this was not really an optimization, and so I changed it so that when you hit the server, it will usually run 3 or 4 smaller database transactions throughout the process, e.g. for the session cookie, the analytics, the navbar, and whatever operation is needed by the page itself. Some of these are read-only, which will improve concurrency. A cost is that the log grows slightly faster, as there are multiple write transactions too.'
127       }
128
129       h5 {
130         'Potential issues with '
131         tt {'LogJSON'}
132         ' and solutions'
133       }
134
135       p {
136         'It must be admitted that the '
137         tt {'accounts'}
138         ' object is not scalable as things stand, since for example when I create a management interface for users, I will want to display something like 20 users per page sorted by email, and at present you would have to extract the entire set of keys from the '
139         tt {'accounts'}
140         ' object simultaneously and sort them to make any sense of them.'
141       }
142
143       p {
144         'Therefore, I plan to create a B-Tree index of email addresses in sorted order, which will be maintained as accounts are added and removed. This B-Tree will be encoded into JSON and stored in the database as well. I will do this before attempting to create the management interface.'
145       }
146
147       p {
148         'Another scalability issue is that '
149         tt {'LogJSON'}
150         ' generally writes out an entire JSON Object (plus all its parent objects) when it changes. Although it does not write out the child objects that have not changed, an object with many keys will still place a strain on the system (since all keys and pointers to their corresponding child objects must be written out even if only one key or pointer has changed). I see two possible solutions to this issue:'
151       }
152
153       p {
154         b {
155           'Improve '
156           tt {'LogJSON'}
157           ' to only write out changed keys.'
158         }
159         ' Later on we will need to track the deltas (changed keys) of each transaction precisely in order to improve concurrency (needed in turn if the load on the webserver is high). If we write the transactions to the log in delta-format, then they will be quicker to write. But conversely, they will be slower to read.'
160       }
161
162       p {
163         b {
164           'Modify database clients to avoid writing large objects.'
165         }
166         ' We could use a hashing approach to implement this. Suppose the user’s account key was '
167         tt {'jane@doe.com'}
168         ', we could hash this to find a hex value of say '
169         tt {'0x569a'}
170         ', and then we break up the '
171         tt {'accounts'}
172         ' object by storing under the hash first, e.g.'
173         pre {
174           `{
175   ...
176   "accounts": {
177     ...
178     "56": {
179       ...
180       "9a": {
181         ...
182         "jane@doe.com": {...},
183         ...
184       },
185       ...
186     },
187     ...
188   },
189   ...
190 }
191 `
192         }
193         'and this way the top 2 levels of the '
194         tt {'accounts'}
195         ' table would have at most 256 entries each (easy to write out each time they change) and the next level would have about 1/65536 the number of entries as the original table. This would scale to at least hundreds of thousands of accounts. An adaptive scheme might also be considered.'
196       }
197
198       p {
199         'A cheap way to achieve the second goal might be simply to store the data in B-Trees (implemented via JSON Arrays of some fixed maximum size corresponding to the B-Tree block size) rather than JSON Objects directly. It would give the advantage of being able to extract data in order as well as by key. Even if we didn’t use this latter feature, the nature of the B-Tree itself provides an adaptive way of keeping the index in manageably sized chunks.'
200       }
201
202       h5 {
203         'Final comments on the '
204         tt {'LogJSON'}
205         ' experiment'
206       }
207
208       p {
209         'I also fixed several minor bugs in '
210         tt {'LogJSON'}
211         ' over the last few weeks and it appears to be stable at this point. I must admit I have not constructed a large-scale programmatic test (add and remove thousands of items at different hierarchical levels, perhaps concurrently, and test that everything is stored correctly throughout the process) but I will do so eventually. I’ll also make a similar test to simulate heavy load on the website and exercise the database in the process.'
212       }
213
214       p {
215         'Overall, I’m satisfied with the progress of the '
216         tt {'LogJSON'}
217         ' experiment, though I haven’t implemented the planned B-Tree layer yet. (I have been doing some research into B-Trees in preparation and have found some MIT-licensed sample code, although it’s not perfect for my requirement).'
218       }
219
220       h4 {
221         'Partition the logic into API endpoints and pages'
222       }
223
224       p {
225         'Under the new scheme, API endpoints use POST requests and return machine-readable responses, whereas pages use GET requests and return human-readable markup. This helps to separate business logic from presentation logic. Whilst I’m not generally a huge fan of model/view/controller type schemes, I must admit that my previous code was a bit messy when both logics were intermixed, and it was hard to see what was happening when you returned to the code later on.'
226       }
227
228       p {
229         'In the previous way, with the business logic encoded into the particular pages it was used on, the page would generally check for a POST request and do something different in such case, for instance it might save something in the database and return a thank-you page.'
230       }
231
232       p {
233         'I did have a few machine-readable API endpoints for special cases such as the sign-in, and some interesting API endpoints uch as the sign-out which would return status text to be displayed in the page. But as there was no clear separation between API endpoints and pages, these endpoints simply appeared as pages that the user would not normally read directly.'
234       }
235
236       p {
237         'The new API-based way is much better from the UX perspective too, as we do not have to return a new page to the user each time they interact with the site, and given that APIs return machine-readable responses it is easy to indicate the progress of an operation with ticks, crosses and the like. And we can make the API more granular and present a running status to the user during a sequence of several API calls, which is easier to implement and better for the UX.'
238       }
239
240       p {
241         'Interestingly, I can still use some text-based status messages (where the API endpoint returns text to be embedded in the page, like it used to) because I have standardized all API errors to return an '
242         tt {'application/problem+json'}
243         ' object mostly following IETF standards, and this contains a '
244         tt {'detail'}
245         ' field where I can describe an error message for the user. However, in the success case, any message for the user is generated by client-side code rather than being returned from the API as in the old way.'
246       }
247
248       p {
249         'The new API endpoints are very easy to use, since I have the utility class '
250         tt {'Problem'}
251         ' and utility function '
252         tt {'api_call()'}
253         ' which take care of marshalling the call across the client/server boundary. At the server side, each endpoint such as '
254         tt {'/api/account/sign_in.json'}
255         ' is held in a corresponding source file such as '
256         tt {'/api/account/sign_in.json.jst'}
257         ' and the marshalling is done by a subroutine '
258         tt {'post_request()'}
259         ' in '
260         tt {'/_lib/post_request.jst'}
261         ', which also avoids a lot of duplication compared to before. To create a new endpoint, you simply copy a similar endpoint source file to the new endpoint name.'
262       }
263
264       h4 {
265         'Rearrange the code into a sensible directory structure'
266       }
267
268       p {
269         'Basically under the new system, things are placed in different directories from the root according to their function, similar to how in a '
270         tt {'unix'}
271         ' system you have '
272         tt {'/bin'}
273         ', '
274         tt {'/lib'}
275         ', '
276         tt {'/usr'}
277         ' and so on, and a package will divide up its files between these directories. Whilst there is some argument about this (would it not be better to have each package keep all its files in the same place?), there are also advantages to separating things out by function.'
278       }
279
280       p {
281         'So under the new scheme, API endpoints are under '
282         tt {'/api'}
283         ', utility code and templates are under '
284         tt {'/_lib'}
285         ', and pages are under the original directory structure reflecting the navigation tree of the site.'
286       }
287
288       p {
289         'In the previous way, it mainly followed the navigation tree of the site, which was not very flexible, since the navigation tree does not necessarily reflect the structure we need for the logic, and also was quite messy as API, utility and template code occurred at various levels of the hierarchy among the code that generated the pages.'
290       }
291
292       p {
293         'The previous theory was that if for example I wanted to add a blog functionality to some other website, I’d just copy the '
294         tt {'/blog'}
295         ' directory and all business logic and templates would automatically be installed as well. But I have decided that mixing everything together was too impractical, so I might have to write an installation procedure at some point if I want a component system for sites. For the time being I’ll just install things in the needed places manually, which is not really much of a hassle.'
296       }
297
298       h4 {
299         'UX improvements, such as custom forms and fewer pop-ups'
300       }
301
302       h5 {
303         'Old style form filling'
304       }
305
306       p {
307         'In the old way we were simply using plain old HTML forms with a ‘Submit’ button, and the browser would encode them into something like JSON (I am not entirely clear how this encoding works) and submit them with a POST request.'
308       }
309
310       p {
311         'I created a little in-browser simulation to demonstrate how this the old style of form worked. Please have a play with it, and then we will demonstrate some UX improvements. Note that for example purposes, you can trigger a server-side error by entering the name ‘Jane Doe’.'
312       }
313
314       div.row.mb-3 {
315         div.col-md-8 {
316           div.card {
317             div.card-body(style="height: 400px;") {
318               p {
319                 a#old-refresh(href='#') {'Refresh'}
320               }
321
322               div#old-main {
323                 p {
324                   'Please enter your details to receive a greeting message!'
325                 }
326
327                 form#old-form {
328                   div.row {
329                     div.col-md-6 {
330                       div.form-group {
331                         label.form-label(for="old-given-names") {'Given names *'}
332                         input.form-control#old-given-names(type="text" placeholder="Your given names" required maxlength=256) {}
333                       }
334                     }
335                     div.col-md-6 {
336                       div.form-group {
337                         label.form-label(for="old-family-name") {'Family name *'}
338                         input.form-control#old-family-name(type="text" placeholder="Your family name" required maxlength=256) {}
339                       }
340                     }
341                   }
342                 }
343                 button.btn.btn-success#old-submit(type="button") {'Submit'}
344                 p.text-muted.'mt-3'.mb-0 {'* This field is required.'}
345               }
346
347               p.mb-0#old-message(hidden) {}
348             }
349           }
350         }
351         div.'col-md-4'.align-self-center {
352           div.icon {
353             svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512") {
354               circle#old-pip0(cx="64" cy="256" r="16") {}
355               circle#old-pip1(cx="128" cy="256" r="16") {}
356               circle#old-pip2(cx="192" cy="256" r="16") {}
357               circle#old-pip3(cx="256" cy="256" r="16") {}
358               circle#old-pip4(cx="320" cy="256" r="16") {}
359               circle#old-pip5(cx="384" cy="256" r="16") {}
360               circle#old-pip6(cx="448" cy="256" r="16") {}
361             }
362           }
363           div.icon.'ml-3'#old-server {
364             _out.push(fa_server)
365           }
366           pre.wrap#old-details(style="height: 250px;") {
367             `details = undefined
368 `
369           }
370         }
371       }
372
373       p {
374         'In the above demonstration form, we had followed one good UX practice:'
375       }
376       div.alert.alert-success {
377         div.icon24-outer.mr-2 {
378           div.icon24-inner {_out.push(icon_tick)}
379         }
380         'Tell the user why we need this information and what we will do with it.'
381       }
382
383       p {
384         'However, we had also broken another recommended UX practice:'
385       }
386       div.alert.alert-danger {
387         div.icon24-outer.mr-2 {
388           div.icon24-inner {_out.push(icon_cross)}
389         }
390         'Don’t repeat the field name in the placeholder text.'
391       }
392       div.alert.alert-success {
393         div.icon24-outer.mr-2 {
394           div.icon24-inner {_out.push(icon_tick)}
395         }
396         'Use a real person’s name or details for example purposes, as this will speed data entry.'
397       }
398
399       p {
400         'We are using the browser’s built-in validation style above, which isn’t consistent across browsers. On Chromium, if you do not fill in a required field, a tooltip appears saying ‘Please fill in this field’. I don’t think this text can be changed, and more annoyingly, only the first error is indicted. Whilst this doesn’t specifically violate any UX guidelines, we can do better:'
401       }
402       div.alert.alert-success {
403         div.icon24-outer.mr-2 {
404           div.icon24-inner {_out.push(icon_tick)}
405         }
406         'Use a custom validation style to highlight errors with colour and provide useful hints for the expected input.'
407       }
408
409       p {
410         'Another UX issue that is demonstrated in the above example is:'
411       }
412       div.alert.alert-danger {
413         div.icon24-outer.mr-2 {
414           div.icon24-inner {_out.push(icon_cross)}
415         }
416         'Don’t throw away the user’s partial input if they have to refresh the page!'
417       }
418       div.alert.alert-success {
419         div.icon24-outer.mr-2 {
420           div.icon24-inner {_out.push(icon_tick)}
421         }
422         'Save a draft to the server during data entry, and pre-populate the form on refresh.'
423       }
424
425       p {
426         'Form filling was one of the killer applications for the early web, and the old way of doing things worked well for its time. Whilst the traditional way is often like it is for a reason, and I do not agree with change for the sake of change, there are definitely improvements to be made in this case. We will look at the improved form UX next.'
427       }
428
429       h5 {
430         'New style form filling'
431       }
432
433       p {
434         'In the new way we use an API endpoint, or several, to receive the user’s input. This allows us to stay on the same page and provide interactive feedback to the user, e.g. if something goes wrong. We can also implement advanced features, such as the saving of drafts, etc.'
435       }
436
437       p {
438         'Here is an in-browser simulation to demonstrate the improved form-filling UX. Please have a play with it, and recall that for example purposes, you can trigger a server-side error by entering the name ‘Jane Doe’.'
439       }
440
441       div.row.mb-3 {
442         div.col-md-8 {
443           div.card {
444             div.card-body(style="height: 450px;") {
445               p {
446                 a#new-refresh(href='#') {'Refresh'}
447               }
448
449               div#new-main {
450                 p {
451                   'Please enter your details to receive a greeting message!'
452                 }
453
454                 form#new-form {
455                   div.row {
456                     div.col-md-6 {
457                       div.form-group {
458                         label.form-label(for="new-given-names") {'Given names *'}
459                         input.form-control#new-given-names(type="text" placeholder="Jane" required maxlength=256) {}
460                         div.invalid-feedback {'Please enter a name we can address you by.'}
461                       }
462                     }
463                     div.col-md-6 {
464                       div.form-group {
465                         label.form-label(for="new-family-name") {'Family name *'}
466                         input.form-control#new-family-name(type="text" placeholder="Doe" required maxlength=256) {}
467                         div.invalid-feedback {'Please enter something. You can enter ‘X’ if you do not have a family name.'}
468                       }
469                     }
470                   }
471                 }
472                 button.btn.btn-success#new-submit(type="button" disabled) {
473                   div.icon24-outer.mr-2#new-icon {
474                     div.icon24-inner {_out.push(fa_cloud_upload_alt)}
475                   }
476                   div.icon24-outer.mr-2#new-tick(hidden) {
477                     div.icon24-inner {_out.push(icon_tick)}
478                   }
479                   div.icon24-outer.mr-2#new-cross(hidden) {
480                     div.icon24-inner {_out.push(icon_cross)}
481                   }
482                   div.icon24-outer.mr-2#new-spinner(hidden) {
483                     div.icon24-inner {
484                       div.spinner-border.spinner-border-sm(role="status") {}
485                     }
486                   }
487                   'Submit'
488                 }
489                 p.text-muted.'mt-3'.mb-0 {'* This field is required.'}
490               }
491
492               div.alert.mb-0#new-message(hidden) {}
493             }
494           }
495         }
496         div.'col-md-4'.align-self-center {
497           div.icon {
498             svg(xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512") {
499               circle#new-pip0(cx="64" cy="256" r="16") {}
500               circle#new-pip1(cx="128" cy="256" r="16") {}
501               circle#new-pip2(cx="192" cy="256" r="16") {}
502               circle#new-pip3(cx="256" cy="256" r="16") {}
503               circle#new-pip4(cx="320" cy="256" r="16") {}
504               circle#new-pip5(cx="384" cy="256" r="16") {}
505               circle#new-pip6(cx="448" cy="256" r="16") {}
506             }
507           }
508           div.icon.'ml-3'#new-server {
509             _out.push(fa_server)
510           }
511           pre.wrap#new-details(style="height: 250px;") {
512             `draft_details = undefined
513 details = undefined
514 `
515           }
516         }
517       }
518
519       p {
520         'In this simulation we are able to see all communication with the server, including the saving of the draft which is normally silent from the user’s viewpoint. The draft is saved every 3 seconds (plus the round-trip time) whilst editing is taking place. The simulation correctly shows also that you can trick the system by refreshing the page very quickly after an edit.'
521       }
522
523       p {
524         'Note that the draft can be saved '
525         i {'after'}
526         ' the form is submitted, which is probably redundant but harmless. It might be good to clear the form once submitted, and the corresponding draft, but I have not formed a clear policy on this. For the time being, I’m relying on stale drafts expiring at the server after a timeout of 1 day.'
527       }
528
529       p {
530         'Some other good UX practices that we are adopting in the improved form are:'
531       }
532       div.alert.alert-success {
533         div.icon24-outer.mr-2 {
534           div.icon24-inner {_out.push(icon_tick)}
535         }
536         'Provide an icon in every button, this speeds entry and also assists non-English speaking users.'
537       }
538       div.alert.alert-success {
539         div.icon24-outer.mr-2 {
540           div.icon24-inner {_out.push(icon_tick)}
541         }
542         'Provide strong visual indication of completion, success or failure by means of icons and colours.'
543       }
544       div.alert.alert-success {
545         div.icon24-outer.mr-2 {
546           div.icon24-inner {_out.push(icon_tick)}
547         }
548         'Show a pacifier when undertaking an operation that takes time, this confirms that the last click was received and also makes the operation seem quicker.'
549       }
550       div.alert.alert-success {
551         div.icon24-outer.mr-2 {
552           div.icon24-inner {_out.push(icon_tick)}
553         }
554         'Disable UI elements such as buttons when it doesn’t make sense to interact with them yet.'
555       }
556
557       h5 {
558         'Final comments on form filling'
559       }
560
561       p {
562       }
563
564       p {
565         'Whilst the old way was sufficient, it was only that, sufficient. The new way provides a more vibrant experience for the user, while capturing the data in a more efficient way by guiding the user through the process. We are also striving to reduce user frustration that could result in them leaving the form (and lost sales/conversions in a commercial context).'
566       }
567
568       p {
569         'Sadly I have not had time to make the form accessible yet, and that will probably be the subject of a future article. Apart from that, the experiment with providing a modern form-filling UX has been very successful. Although it has taken a week or so of experimentation, I consider this time well spent, as I learned something about modern UX design.'
570       }
571
572       h4 {
573         'Conclusions'
574       }
575
576       p {
577         'The new internals of the site are much more pleasant to work on, as things are much more organized, and the '
578         tt {'LogJSON'}
579         ' database allows plenty of flexibility. Thus we can quickly implement UX features such as the saving of drafts. Moreover, the new UX (implemented during the database upgrade) is much more visually pleasing and easier to use.'
580       }
581     },
582     // scripts
583     async _out => {
584       //script(src="/js/utils.js") {}
585
586       script {
587         document.addEventListener(
588           'DOMContentLoaded',
589           () => {
590             let id_old_details = document.getElementById('old-details')
591             let id_old_family_name = document.getElementById('old-family-name')
592             let id_old_form = document.getElementById('old-form')
593             let id_old_given_names = document.getElementById('old-given-names')
594             let id_old_main = document.getElementById('old-main')
595             let id_old_message = document.getElementById('old-message')
596             let id_old_pips = [
597               document.getElementById('old-pip0'),
598               document.getElementById('old-pip1'),
599               document.getElementById('old-pip2'),
600               document.getElementById('old-pip3'),
601               document.getElementById('old-pip4'),
602               document.getElementById('old-pip5'),
603               document.getElementById('old-pip6')
604             ]
605             let id_old_refresh = document.getElementById('old-refresh')
606             let id_old_server = document.getElementById('old-server')
607             let id_old_submit = document.getElementById('old-submit')
608
609             let id_new_cross = document.getElementById('new-cross')
610             let id_new_details = document.getElementById('new-details')
611             let id_new_family_name = document.getElementById('new-family-name')
612             let id_new_form = document.getElementById('new-form')
613             let id_new_given_names = document.getElementById('new-given-names')
614             let id_new_icon = document.getElementById('new-icon')
615             let id_new_main = document.getElementById('new-main')
616             let id_new_message = document.getElementById('new-message')
617             let id_new_pips = [
618               document.getElementById('new-pip0'),
619               document.getElementById('new-pip1'),
620               document.getElementById('new-pip2'),
621               document.getElementById('new-pip3'),
622               document.getElementById('new-pip4'),
623               document.getElementById('new-pip5'),
624               document.getElementById('new-pip6')
625             ]
626             let id_new_refresh = document.getElementById('new-refresh')
627             let id_new_server = document.getElementById('new-server')
628             let id_new_spinner = document.getElementById('new-spinner')
629             let id_new_submit = document.getElementById('new-submit')
630             let id_new_tick = document.getElementById('new-tick')
631
632             // old simulation
633             let old_pips_to_server = async () => {
634               for (let i = 0; i < 7; ++i) {
635                 id_old_pips[i].setAttribute('r', 32)
636                 await new Promise(resolve => setTimeout(resolve, 100))
637                 id_old_pips[i].setAttribute('r', 16)
638               }
639             }
640             let old_pips_at_server = async success => {
641               id_old_server.classList.add(
642                 success ? 'text-success' : 'text-danger'
643               )
644               await new Promise(resolve => setTimeout(resolve, 100))
645               id_old_server.classList.remove(
646                 success ? 'text-success' : 'text-danger'
647               )
648             }
649             let old_pips_from_server = async () => {
650               for (let i = 6; i >= 0; --i) {
651                 id_old_pips[i].setAttribute('r', 32)
652                 await new Promise(resolve => setTimeout(resolve, 100))
653                 id_old_pips[i].setAttribute('r', 16)
654               }
655             }
656
657             id_old_refresh.addEventListener(
658               'click'
659               async event => {
660                 event.preventDefault()
661                 id_old_main.hidden = true
662                 id_old_message.hidden = true
663                 await old_pips_to_server()
664                 id_old_details.textContent =
665                   `details = undefined
666 `
667                 await old_pips_at_server(true)
668                 await old_pips_from_server()
669                 id_old_given_names.value = ''
670                 id_old_family_name.value = ''
671                 id_old_main.hidden = false
672               }
673             )
674             id_old_submit.addEventListener(
675               'click',
676               async () => {
677                 if (!id_old_form.reportValidity())
678                   return
679                 let details = {
680                   given_names: id_old_given_names.value.slice(0, 256),
681                   family_name: id_old_family_name.value.slice(0, 256)
682                 }
683                 await old_pips_to_server()
684                 id_old_details.textContent =
685                   `details = ${JSON.stringify(details, null, 2)}
686 `
687                 let success =
688                   details.given_names.toLowerCase() !== 'jane' ||
689                     details.family_name.toLowerCase() !== 'doe'
690                 await old_pips_at_server(success)
691                 await old_pips_from_server()
692                 if (success) {
693                   //id_old_message.classList.remove('text-danger')
694                   id_old_message.textContent = `Hello, ${details.given_names} ${details.family_name}!`
695                 }
696                 else {
697                   //id_old_message.classList.add('text-danger')
698                   id_old_message.textContent = 'Our server has determined that you are not a real person!'
699                 }
700                 id_old_main.hidden = true
701                 id_old_message.hidden = false
702               }
703             )
704
705             // new simulation
706             let new_pips_to_server = async () => {
707               for (let i = 0; i < 7; ++i) {
708                 id_new_pips[i].setAttribute('r', 32)
709                 await new Promise(resolve => setTimeout(resolve, 100))
710                 id_new_pips[i].setAttribute('r', 16)
711               }
712             }
713             let new_pips_at_server = async success => {
714               id_new_server.classList.add(
715                 success ? 'text-success' : 'text-danger'
716               )
717               await new Promise(resolve => setTimeout(resolve, 100))
718               id_new_server.classList.remove(
719                 success ? 'text-success' : 'text-danger'
720               )
721             }
722             let new_pips_from_server = async () => {
723               for (let i = 6; i >= 0; --i) {
724                 id_new_pips[i].setAttribute('r', 32)
725                 await new Promise(resolve => setTimeout(resolve, 100))
726                 id_new_pips[i].setAttribute('r', 16)
727               }
728             }
729
730             let new_details, new_draft_details
731             let new_refresh_details = () => {
732               id_new_details.textContent =
733                 `draft_details = ${JSON.stringify(new_draft_details, null, 2)}
734 details = ${JSON.stringify(new_details, null, 2)}
735 `
736             }
737
738             let new_input_semaphore = new BinarySemaphore(false)
739             ;(
740               async () => {
741                 while (true) {
742                   await new_input_semaphore.acquire()
743                   await new Promise(resolve => setTimeout(resolve, 3000))
744                   new_input_semaphore.try_acquire()
745                   draft_details =
746                     id_new_given_names.value.length === 0 &&
747                       id_new_family_name.value.length === 0 ?
748                       undefined :
749                       {
750                         given_names: id_new_given_names.value.slice(0, 256),
751                         family_name: id_new_family_name.value.slice(0, 256)
752                       }
753                   await new_pips_to_server()
754                   new_draft_details = draft_details
755                   new_refresh_details()
756                   await new_pips_at_server(true)
757                   await new_pips_from_server()
758                 }
759               }
760             )() // ignore returned promise (start thread)
761
762             let new_edited = () => {
763               new_input_semaphore.release()
764
765               id_new_submit.disabled =
766                 id_new_given_names.value.length === 0 &&
767                   id_new_family_name.value.length === 0
768               id_new_icon.hidden = false
769               id_new_tick.hidden = true
770               id_new_cross.hidden = true
771               id_new_spinner.hidden = true
772               id_new_message.hidden = true
773             }
774
775             id_new_given_names.addEventListener('input', new_edited)
776             id_new_family_name.addEventListener('input', new_edited)
777
778             id_new_refresh.addEventListener(
779               'click'
780               async event => {
781                 event.preventDefault()
782                 id_new_main.hidden = true
783                 id_new_message.hidden = true
784                 await new_pips_to_server()
785                 await new_pips_at_server(true)
786                 let draft_details = new_draft_details
787                 await new_pips_from_server()
788                 id_new_given_names.value =
789                   draft_details ? draft_details.given_names : ''
790                 id_new_family_name.value =
791                   draft_details ? draft_details.family_name : ''
792                 id_new_submit.disabled =
793                   id_new_given_names.value.length === 0 &&
794                     id_new_family_name.value.length === 0
795                 id_new_main.hidden = false
796               }
797             )
798             id_new_submit.addEventListener(
799               'click',
800               async () => {
801                 id_new_icon.hidden = false
802                 id_new_tick.hidden = true
803                 id_new_cross.hidden = true
804                 id_new_spinner.hidden = true
805                 // the below causes an ugly flicker, so just keep the message
806                 //id_new_message.hidden = true
807
808                 if (!id_new_form.checkValidity()) {
809                   id_new_form.classList.add('was-validated');
810
811                   id_new_icon.hidden = true
812                   id_new_cross.hidden = false
813                   return
814                 }
815                 id_new_form.classList.remove('was-validated');
816
817                 let details = {
818                   given_names: id_new_given_names.value.slice(0, 256),
819                   family_name: id_new_family_name.value.slice(0, 256)
820                 }
821
822                 id_new_icon.hidden = true
823                 id_new_spinner.hidden = false
824                 await new_pips_to_server()
825                 new_details = details
826                 new_refresh_details()
827                 let success =
828                   details.given_names.toLowerCase() !== 'jane' ||
829                     details.family_name.toLowerCase() !== 'doe'
830                 await new_pips_at_server(success)
831                 await new_pips_from_server()
832
833                 id_new_spinner.hidden = true
834                 if (success) {
835                   id_new_tick.hidden = false
836
837                   id_new_message.classList.add('alert-success')
838                   id_new_message.classList.remove('alert-danger')
839                   id_new_message.textContent = `Hello, ${details.given_names} ${details.family_name}!`
840                 }
841                 else {
842                   id_new_cross.hidden = false
843
844                   id_new_message.classList.remove('alert-success')
845                   id_new_message.classList.add('alert-danger')
846                   id_new_message.textContent = 'Our server has determined that you are not a real person!'
847                 }
848                 id_new_message.hidden = false
849               }
850             )
851           }
852         )
853       }
854
855       style {
856         .wrap {
857           white-space: pre-wrap;
858           word-wrap: break-word;
859         }
860
861         .icon {
862           display: inline-block;
863           vertical-align: center;
864           width: 128px;
865           height: 128px;
866           fill: currentColor;
867           stroke: currentColor;
868         }
869       }
870     }
871   )
872 }