Compare commits

..

262 Commits

Author SHA1 Message Date
Brian Picciano
60404da798 Update contacts to include signal username 2024-03-24 15:12:52 +01:00
Brian Picciano
1757c74cda Revert "Basic architecture diagram for a potential deadlink checker"
This reverts commit 8ca4861a23.
2023-12-30 17:27:38 +01:00
Brian Picciano
8ca4861a23 Basic architecture diagram for a potential deadlink checker 2023-12-24 15:27:23 +01:00
Brian Picciano
169cefa46c Fix dev gemini public URL 2023-11-28 21:37:18 +01:00
Brian Picciano
c8c7b31d1f Add new GPG key 2023-11-28 21:36:07 +01:00
Brian Picciano
b02115e390 Fix go module path 2023-10-28 10:03:51 +02:00
Brian Picciano
ba2d581975 Fix gemini gateway proxying to work with gateways like tildeverse's 2023-09-21 10:51:30 +02:00
Brian Picciano
78bbfa42fa Remove mailinglist and proof-of-work functionality 2023-08-25 21:12:57 +02:00
Brian Picciano
c4ec906406 Remove leftover static assets specific to individual blog posts 2023-08-13 22:06:32 +02:00
Brian Picciano
0bc9cd83b4 Confirm before deleting a post 2023-08-13 21:42:27 +02:00
Brian Picciano
e8a1f51fb6 Fix vendor sha which somehow became wrong 2023-08-10 17:00:14 +02:00
Brian Picciano
d7507d2def Clean up homepage, add ghost to gmi, make separator smaller 2023-08-10 16:56:51 +02:00
Brian Picciano
5559e01343 Implement asset.Loader
This moved a bunch of logic out of http and into the asset package,
making it available for gmit too.
2023-04-17 04:17:45 +02:00
Brian Picciano
7872296b83 Move asset store into its own package 2023-04-15 21:07:16 +02:00
Brian Picciano
68f3215df6 Add support for http loading files from an asset archive 2023-04-15 20:57:39 +02:00
Brian Picciano
b5e1103caf Update go to 1.19 (by updating nixpkgs) 2023-04-15 20:19:08 +02:00
Brian Picciano
861070f74d Prefix html title tag for posts with their title 2023-04-14 01:15:25 +02:00
Brian Picciano
7d48d2abba Fix mediaType for feed.xml over gemini 2023-02-13 17:52:17 +01:00
Brian Picciano
8108d55c29 Fix some line wraps in gemini copy 2023-02-13 17:24:09 +01:00
Brian Picciano
300a78ef48 Include link to source 2023-02-13 17:15:48 +01:00
Brian Picciano
80c1631bdd Touch up RSS feed links and post page 2023-01-24 13:35:26 +01:00
Brian Picciano
3f97311514 Add notice atop md->gmi translated pages 2023-01-24 13:19:23 +01:00
Brian Picciano
b766eefe7c Add gemini CTA to HTTP pages 2023-01-24 13:13:07 +01:00
Brian Picciano
6b3933282e Refactor URL construction a bit
BlogHTTPURL and BlogGeminiURL are now used specifically to construct
absolute URLs to their respective endpoints.
2023-01-24 12:45:10 +01:00
Brian Picciano
63cc6cb217 Fix squished manage post edit button 2023-01-24 12:19:21 +01:00
Brian Picciano
c1c1bb2c4c Implement cache and logger middlewares for gemini 2023-01-23 22:31:19 +01:00
Brian Picciano
024f514886 Add BlogHTTPURL preprocess function 2023-01-23 21:44:10 +01:00
Brian Picciano
26dbc6691d Fix page navigation in posts/drafts manage pages 2023-01-23 21:33:45 +01:00
Brian Picciano
57719f29b9 Small fixes to index pages 2023-01-23 18:55:10 +01:00
Brian Picciano
c4520f2c84 Automatically bridge gemini links to a gateway on http site 2023-01-23 16:02:35 +01:00
Brian Picciano
aba69d4329 Small formatting fixes on gmi site 2023-01-23 15:41:41 +01:00
Brian Picciano
acec797048 Final fleshing out of gemini content 2023-01-22 14:12:54 +01:00
Brian Picciano
5b5a043868 Implement gemini atom feed 2023-01-22 12:45:03 +01:00
Brian Picciano
1cfdac5e8c Allow url construction to work if blog is under a sub-path 2023-01-22 12:09:50 +01:00
Brian Picciano
f536b16e17 Add wtfpl.txt to gemini 2023-01-21 19:41:40 +01:00
Brian Picciano
0d420f70d8 Fix StaticURL in gemini 2023-01-21 19:38:59 +01:00
Brian Picciano
2ca44b60d4 Add assets handler to gemini 2023-01-21 19:11:41 +01:00
Brian Picciano
bde3751fab Convert markdow to gemtext in gmi server 2023-01-21 18:36:28 +01:00
Brian Picciano
ffdd9520b9 Implement preprocessing of post bodies for gemini 2023-01-21 17:37:22 +01:00
Brian Picciano
293655452c Continue to polish up posts pages 2023-01-21 16:46:11 +01:00
Brian Picciano
7878db5c95 Initial implementation of post rendering over gmi 2023-01-21 16:01:52 +01:00
Brian Picciano
84c1322c44 Got a basic gemini server running 2023-01-20 14:50:36 +01:00
Brian Picciano
0bd8bd6f23 Slight cleanup in http package 2023-01-19 18:03:20 +01:00
Brian Picciano
c23030733f Add support for gemtext posts 2023-01-19 16:02:27 +01:00
Brian Picciano
e7b5b55f67 Add format column to post tables 2023-01-18 20:15:12 +01:00
Brian Picciano
4878495914 Don't check CSRF for manage and edit methods 2022-11-29 22:20:34 +01:00
Brian Picciano
16579fdf7f Fix dumb wording on homepage 2022-11-29 21:56:21 +01:00
Brian Picciano
bf5d57ef52 Remove mention of the word 'blog' 2022-11-29 21:48:07 +01:00
Brian Picciano
f6ab6db9c2 Update copy on follow page to be less wordy 2022-11-29 21:45:48 +01:00
Brian Picciano
ceab45fefa Increase posts per page 2022-11-29 21:39:18 +01:00
Brian Picciano
b4d6133626 Small changes to header 2022-11-29 21:39:01 +01:00
Brian Picciano
7943865cc6 Move mediocregopher.com content to index of this site 2022-11-29 21:34:55 +01:00
Brian Picciano
1f3ae665ed Introduce EDIT and MANAGE methods
All admin "index" pages are moved under MANAGE, so that we can have (for
example) and normal "GET /posts" page later which would replace the
current index page, and potentially corresponding pages for the other
categories.

The EDIT method replaces the old `?edit` pattern, which normalizes how
we differentiate page functionality generally.
2022-11-29 20:59:31 +01:00
Brian Picciano
31f8f37c5a Modify base header some more 2022-11-29 12:45:30 +01:00
Brian Picciano
f8cc9f6aa7 Update header, remove 'Updated at' times on posts 2022-11-27 23:03:11 +01:00
Brian Picciano
7f79413102 Add the ghost to every page 2022-11-27 22:46:07 +01:00
Brian Picciano
b3d3fa7c62 Bold post titles in index 2022-11-27 22:04:31 +01:00
Brian Picciano
52d4fdac55 Remove all 'old nix' stuff, plus configs and unused dev environment 2022-11-27 22:02:20 +01:00
Brian Picciano
175ddfdbe9 Got it working with nix flake 2022-11-27 21:25:25 +01:00
Brian Picciano
03493f9d61 Fix font sizes for small devices 2022-11-23 19:21:25 +01:00
Brian Picciano
58086a3fda Accidentally removed 'Next Page' link 2022-11-23 19:03:01 +01:00
Brian Picciano
9a14e93524 Inrease font size for small devices 2022-11-23 18:57:43 +01:00
Brian Picciano
cbaaad38c9 Use unordered list instead of table on post index 2022-11-23 18:53:40 +01:00
Brian Picciano
14bf8033b2 Add favicon 2022-11-22 22:49:55 +01:00
Brian Picciano
954e76fe1f Reduce font size generally 2022-11-22 22:36:15 +01:00
Brian Picciano
59c8c30fb5 Introduce dark mode, clean up colors generally 2022-11-22 22:28:28 +01:00
Brian Picciano
0ad27f430d Fix width of index table 2022-11-07 22:34:42 +01:00
Brian Picciano
d957041113 Make description an optional field 2022-10-12 23:43:31 +02:00
Brian Picciano
d959985ed0 Remove --no-out-link from install-systemd 2022-10-11 18:17:21 +02:00
Brian Picciano
cef74151d2 fix bug in asset upload id checker 2022-09-13 13:25:04 +02:00
Brian Picciano
4f01edb923 move src out of srv, clean up default.nix and Makefile 2022-09-13 12:56:08 +02:00
Brian Picciano
5485984e05 remove redis and circus 2022-09-13 12:25:45 +02:00
Brian Picciano
b1641d1af9 remove chat functionality completely 2022-09-13 12:12:28 +02:00
Brian Picciano
1b3ca1af4b small change on admin 2022-08-19 21:58:18 -06:00
Brian Picciano
61d4e959b5 improve navigation around admin pages 2022-08-19 21:57:05 -06:00
Brian Picciano
f365b09757 implemented draft publishing and removed New Posts link/capability 2022-08-19 21:47:38 -06:00
Brian Picciano
33a81f73e1 load published posts test data in reverse order 2022-08-19 21:15:39 -06:00
Brian Picciano
c3135306b3 drafts functionality added, needs a publish button still 2022-08-18 23:07:09 -06:00
Brian Picciano
dfa9bcb9e2 WIP 2022-08-18 22:34:38 -06:00
Brian Picciano
7ac2f5ebb3 implement DraftStore 2022-08-18 22:25:17 -06:00
Brian Picciano
76ff79f470 add admin page, and spruce up posts and assets 2022-08-18 21:11:42 -06:00
Brian Picciano
98e0c3e89c fix CSRF checking so that localhost always works 2022-08-18 20:59:53 -06:00
Brian Picciano
f7b4fd644b improve how index table looks 2022-08-18 20:52:09 -06:00
Brian Picciano
062e5bbd10 delete some old script that aren't needed anymore 2022-08-18 20:28:04 -06:00
Brian Picciano
60018dd9f5 load some test data when starting the dev shell 2022-08-18 20:27:40 -06:00
Brian Picciano
d0a604b0ba update welcome message a bit 2022-08-16 22:37:11 -06:00
Brian Picciano
a125226825 move published date to bottom of post 2022-08-16 22:29:04 -06:00
Brian Picciano
bb6c715a0e more polish, use table on index page 2022-08-16 22:26:31 -06:00
Brian Picciano
c06dd5c587 make button vs link usage consistent 2022-08-16 22:04:57 -06:00
Brian Picciano
7bd7af44a9 remove target:_blanks 2022-08-16 22:01:25 -06:00
Brian Picciano
2144e9f2f1 make license link less obnoxious 2022-08-16 21:58:48 -06:00
Brian Picciano
cbc5816586 fix color of warning on follow page 2022-08-16 21:57:45 -06:00
Brian Picciano
36d79896e7 add a welcome paragraph 2022-08-16 21:55:32 -06:00
Brian Picciano
960e33d649 inline css into pages 2022-08-16 21:46:02 -06:00
Brian Picciano
bf8412969a finish switching to classless, got a color scheme worked out too 2022-08-16 21:32:50 -06:00
Brian Picciano
5bc4c2fe4e began using new.css, got rid of the old css almost entirely 2022-08-15 15:26:03 -06:00
Brian Picciano
cd1d97bebf don't allow gmail addresses to subscribe 2022-08-09 20:56:34 -06:00
Brian Picciano
7f6d7366e3 improve logging of ips, to account for xff and x-real-ip 2022-08-09 15:23:03 -06:00
Brian Picciano
fa47baffb6 Get rid of CSRF code in api.js 2022-06-03 13:42:59 -06:00
Brian Picciano
08811a6da7 Replace CSRF token checking with Referer checking 2022-05-24 17:42:00 -06:00
Brian Picciano
159638084e Fix CSRF loading on static GET pages 2022-05-24 17:27:03 -06:00
Brian Picciano
88ebaeda8f Don't set updated at field on feed, it's wonky 2022-05-22 09:33:24 -06:00
Brian Picciano
f3340ae5f4 Remove old code related to static, it's not needed anymore 2022-05-21 14:07:14 -06:00
Brian Picciano
55eb40d4bb Add chat page, though it's not used yet 2022-05-21 14:06:07 -06:00
Brian Picciano
4dc1683d3e Serve mailing list finalize and unsubscribe endpoints 2022-05-21 14:03:38 -06:00
Brian Picciano
7335295dc0 Fix rendering errors near script tags 2022-05-21 13:54:14 -06:00
Brian Picciano
b136b42a44 Give images a bottom margin 2022-05-21 13:00:04 -06:00
Brian Picciano
04b95b0e22 Cleanup after importing everything into prod 2022-05-21 12:57:08 -06:00
Brian Picciano
180575fe4a Fix post body templates not being parsed correctly 2022-05-21 12:10:38 -06:00
Brian Picciano
bdd1f01605 Fix srv binaries 2022-05-21 11:40:13 -06:00
Brian Picciano
01424c7dab Standardize URL generation across the blog 2022-05-21 11:34:03 -06:00
Brian Picciano
e269b111a1 List existing tags on edit post page 2022-05-21 11:20:45 -06:00
Brian Picciano
3059909deb Publish new posts to mailing list 2022-05-21 11:15:37 -06:00
Brian Picciano
3e67823205 Hide tag picker for now 2022-05-21 10:56:50 -06:00
Brian Picciano
e92f59fc7f Redirect legacy URL path 2022-05-21 10:50:42 -06:00
Brian Picciano
0361b1d4bf Fix annoying underline next to follow button 2022-05-21 10:37:02 -06:00
Brian Picciano
5a58890228 Fix code block formatting and clean up extraneous files 2022-05-21 10:36:50 -06:00
Brian Picciano
79452e7472 Re-arrange routes so that cache only applies to blog routes 2022-05-21 10:18:00 -06:00
Brian Picciano
fb905ad53c Add in-memory cache to GET requests, purges on successful POSTs 2022-05-21 10:04:50 -06:00
Brian Picciano
1de0ab3b72 Define an actual middleware type, use that to set up API routes 2022-05-21 09:17:43 -06:00
Brian Picciano
034342421b Simplify routes by moving formMiddleware to the global level 2022-05-20 19:29:01 -06:00
Brian Picciano
1181af0318 Don't use EDIT method, only POSTs should use alt methods 2022-05-20 19:19:32 -06:00
Brian Picciano
c4692f72e3 Add tag parameter to feed endpoint 2022-05-20 17:51:41 -06:00
Brian Picciano
1f42c5e000 Add tag selector to index 2022-05-20 17:43:47 -06:00
Brian Picciano
47d4787907 Always return results in time desc order from PostStore 2022-05-20 17:31:44 -06:00
Brian Picciano
99f8c1188c Add RSS feed generator 2022-05-20 17:24:52 -06:00
Brian Picciano
b4ca8853a9 Update http config names 2022-05-20 17:04:20 -06:00
Brian Picciano
1ffda21ae3 Implement ratelimit on authentications 2022-05-20 14:54:26 -06:00
Brian Picciano
ae1fa76efc Rename follow.html to follow 2022-05-20 14:32:31 -06:00
Brian Picciano
af434077ef Implement image macro for rendering images 2022-05-20 14:30:26 -06:00
Brian Picciano
16cfbd1915 Move static assets to within srv 2022-05-20 13:37:43 -06:00
Brian Picciano
3cdee89c96 Put post preview behind auth 2022-05-20 11:20:02 -06:00
Brian Picciano
09acb111a2 Rename api package to http 2022-05-20 11:17:31 -06:00
Brian Picciano
f69ed83de7 Add preview button to edit post page 2022-05-20 11:06:21 -06:00
Brian Picciano
2c4b617dde Implement saving of new and edited posts 2022-05-20 10:47:22 -06:00
Brian Picciano
75044eef03 Implement edit post page 2022-05-20 10:13:46 -06:00
Brian Picciano
0bc0204f0b Implement post deleting 2022-05-20 09:33:03 -06:00
Brian Picciano
1242be7cfe Implement posts index page 2022-05-20 08:42:54 -06:00
Brian Picciano
5a99778187 Scatter render.go contents everywhere 2022-05-20 07:59:36 -06:00
Brian Picciano
16f9da05e5 Fix spacing in assets table delete button 2022-05-19 22:47:57 -06:00
Brian Picciano
3664286506 Actually use the auth middleware for assets routes 2022-05-19 22:44:33 -06:00
Brian Picciano
8da42184eb Implement basic auth middleware 2022-05-19 21:35:45 -06:00
Brian Picciano
56530a8a66 Implement asset deletion and fix redirect logic 2022-05-18 10:59:07 -06:00
Brian Picciano
69de76cb32 Add asset file upload form, plus related necessary refactors 2022-05-17 15:54:20 -06:00
Brian Picciano
e406ad6e7c Re-arrange how api endpoints are defined 2022-05-17 15:25:13 -06:00
Brian Picciano
7b7bdcf57a Initial implementation of admin assets page 2022-05-17 14:39:42 -06:00
Brian Picciano
9a67ef9211 Add follow.html to v2 2022-05-17 14:15:38 -06:00
Brian Picciano
e742a2d6d5 Add BlogURL template function 2022-05-17 13:52:18 -06:00
Brian Picciano
0fdece68c0 Add /v2/assets/ handler, with resizing 2022-05-17 13:29:12 -06:00
Brian Picciano
f9d1f664f0 Implement import-asset script 2022-05-17 13:28:56 -06:00
Brian Picciano
4d23325823 Implement cfg.BoolVar method 2022-05-17 11:56:17 -06:00
Brian Picciano
788aba3d0d Fix new index page 2022-05-14 21:35:53 -06:00
Brian Picciano
af08122a25 Add static serve back to dev-shell 2022-05-14 21:28:33 -06:00
Brian Picciano
e24dd6d630 Import posts in dev-shell target 2022-05-14 20:42:43 -06:00
Brian Picciano
e41ff2b897 Implement index handler
This involved re-arranging how templates are being parsed, slightly.
2022-05-14 17:02:30 -06:00
Brian Picciano
4c04177c05 Move template rendering logic into api package 2022-05-14 17:02:16 -06:00
Brian Picciano
dd354bc323 Create srv.dev-shell target in Makefile 2022-05-14 15:22:32 -06:00
Brian Picciano
7e87c09c50 Add /posts handler to api 2022-05-14 15:22:16 -06:00
Brian Picciano
2929b4279c Implement rendering Posts to html 2022-05-14 15:22:10 -06:00
Brian Picciano
d284fe2d25 Fix integration tests 2022-05-08 16:57:04 -06:00
Brian Picciano
a6269d1fe0 Cleanup and fix dev entrypoint target 2022-05-08 16:49:20 -06:00
Brian Picciano
a759fc28f7 Add test target to Makefile to run full integration tests 2022-05-08 16:36:28 -06:00
Brian Picciano
ddb126db17 Move radix config into cfg, use that for integration tests 2022-05-08 16:36:08 -06:00
Brian Picciano
450136a0c0 Implement cachedAssetStore 2022-05-07 18:22:57 -06:00
Brian Picciano
3088c9d76c Implement AssetStore interface 2022-05-07 18:07:27 -06:00
Brian Picciano
cfb633b3b5 Cleanup various small issues with post package 2022-05-07 18:07:13 -06:00
Brian Picciano
07806c6942 Move sql db out of post.NewStore, so it can be shared 2022-05-07 14:56:34 -06:00
Brian Picciano
1495c78656 Move integration tests under a build tag 2022-05-07 14:55:19 -06:00
Brian Picciano
0d016129a4 Implement import-posts script 2022-05-07 14:33:57 -06:00
Brian Picciano
a10a604018 Refactor how data dir is initialized 2022-05-07 13:18:44 -06:00
Brian Picciano
c99b37c5b3 Use unix timestamps for post.Store 2022-05-06 21:49:00 -06:00
Brian Picciano
f7d72adfb5 Implement post.Store 2022-05-06 17:22:21 -06:00
Brian Picciano
d8b12cf17a Begin work on post package 2022-05-05 21:42:46 -06:00
Brian Picciano
eed10ce514 Fix various problems with the srv build 2022-05-05 21:20:22 -06:00
Brian Picciano
cc8b6289ac Improve fatal message for mailinglist.Store creation 2022-05-05 21:05:23 -06:00
Brian Picciano
39ad0e614f Use envvars to configure srv 2022-05-05 20:57:17 -06:00
Brian Picciano
5c247b6f29 Refactor cfg to use envvars as well 2022-05-05 20:30:36 -06:00
Brian Picciano
0277c02767 Get rid of loggerFatalError 2022-05-05 19:33:40 -06:00
Brian Picciano
24e9541e02 wifi passwords 2022-05-03 22:31:04 -06:00
Brian Picciano
13737cf85d ginger vm update 2022-04-03 15:49:24 -06:00
Brian Picciano
ae2f854bca redate open infra, make footnotes smaller 2022-03-13 09:41:17 -06:00
Brian Picciano
8f5577d665 open infra 2022-03-12 22:21:51 -07:00
Brian Picciano
6b56556dba cryptic-fs 2022-01-25 21:38:49 -07:00
Brian Picciano
ae3cc05ce7 New fonts, remove external dependencies, clean out unused code, remove twitter stuff 2022-01-21 22:28:48 -07:00
Brian Picciano
bfcdeb6233 ginger names 2022-01-21 22:28:42 -07:00
Brian Picciano
fdd02aff26 take tech tag off of dog money, and center the image 2022-01-19 18:58:51 -07:00
Brian Picciano
68caa928a8 dav 2022-01-02 19:45:45 -07:00
Brian Picciano
19f1efd748 ginger: it's alive 2021-12-31 13:37:32 -07:00
Brian Picciano
ed4d179680 minting a single nft 2021-12-02 07:54:55 -07:00
Brian Picciano
10cb34d0d0 add notes to follow page 2021-11-09 22:02:08 -07:00
Brian Picciano
0055a0f4fc managing home server with nix 2021-11-08 17:00:39 -07:00
Brian Picciano
f8f9fc8b1e fix tags 2021-10-31 18:02:43 -06:00
Brian Picciano
ac7215e523 dog money! 2021-10-31 17:37:32 -06:00
Brian Picciano
9a9b94e677 fix runDir startup 2021-10-10 16:32:05 -06:00
Brian Picciano
624a38af2b update appimages with nix wording 2021-09-22 22:04:16 -06:00
Brian Picciano
9655dc6677 appimages with nix 2021-09-22 21:55:01 -06:00
Brian Picciano
ee66563717 chat page kinda sorta works, needs lots of polish 2021-09-04 17:52:17 -06:00
Brian Picciano
34f44cb5d5 implementation of basic chat page which can show history and not much else 2021-09-02 17:02:20 -06:00
Brian Picciano
6bebc3fae7 have circus run static serve, and optionally able to skip services via Makefile 2021-08-30 21:38:09 -06:00
Brian Picciano
9343d2ea69 add chat handlers and only allow POST methods 2021-08-30 20:44:45 -06:00
Brian Picciano
3e9a17abb9 fix finalize and unsubscribe pages by making them use new api module 2021-08-30 10:58:12 -06:00
Brian Picciano
15ae483fad add CSRF checking 2021-08-29 22:15:58 -06:00
Brian Picciano
5746a510fc generalize api call logic and move it to a module 2021-08-29 21:34:24 -06:00
Brian Picciano
1608ca7425 better logging in srv when fake Mailer is used 2021-08-29 21:34:02 -06:00
Brian Picciano
bf40fa3868 small fixes to srv 2021-08-27 18:03:51 -06:00
Brian Picciano
a1f1044b48 ginger syntax 2021-08-27 17:51:55 -06:00
Brian Picciano
38fdd7725d premake the dataDir for srv 2021-08-26 21:28:27 -06:00
Brian Picciano
a9d8aa2591 implemented basic userID generation 2021-08-18 18:13:44 -06:00
Brian Picciano
bec64827d1 implement basic chat history endpoint 2021-08-18 17:13:25 -06:00
Brian Picciano
eaccf41563 MVP of chat package 2021-08-17 14:35:07 -06:00
Brian Picciano
ac5275353c implement publish sub-cmd of mailinglist-cli 2021-08-10 13:07:14 -06:00
Brian Picciano
ca27cd6c67 get a basic mailinglist-cli working 2021-08-08 09:07:51 -06:00
Brian Picciano
6feffc568a refactor how nix derivations are organized and built 2021-08-08 08:43:17 -06:00
Brian Picciano
0197d9cd49 split configuration parsing out into separate packages, split api out as well 2021-08-07 20:38:37 -06:00
Brian Picciano
dce39b836a add redis process, put circus in charge of process management 2021-08-06 20:34:18 -06:00
Brian Picciano
e6d607a248 self hosting blog mailing list 2021-08-04 18:31:10 -06:00
Brian Picciano
47f32e1468 add response headers to prevent caching 2021-08-04 17:13:02 -06:00
Brian Picciano
5ca7dadd02 full deployment via nix 2021-08-03 17:47:01 -06:00
Brian Picciano
8ebf4a26f4 gracefully shutdown on sigint/sigterm 2021-08-03 16:40:33 -06:00
Brian Picciano
9c3ea8dd80 implement finalize and unsubscribe endpoints 2021-08-03 16:28:22 -06:00
Brian Picciano
ec4aac24ab got pow working on the subscribe endpoint 2021-08-03 15:20:32 -06:00
Brian Picciano
20b9213010 add api endpoints for pow and mailinglist, plus some framework improvements 2021-08-02 15:21:12 -06:00
Brian Picciano
069ee93de1 implemented PoW backend 2021-08-01 17:54:53 -06:00
Brian Picciano
6bec0e3964 begin work on mailing list server, got the fundamental components done 2021-07-31 16:52:16 -06:00
Brian Picciano
0655b19283 update ruby deps 2021-07-31 12:45:23 -06:00
Brian Picciano
f1998c321a move static files into static sub-dir, refactor nix a bit 2021-07-31 11:35:39 -06:00
Brian Picciano
03a35dcc38 radix v4 2021-07-18 17:22:15 -06:00
Brian Picciano
bad1025bfa secure webapp 2021-07-14 16:27:02 -06:00
Brian Picciano
710efb7fae maddy vps 2021-07-06 10:25:13 -06:00
Brian Picciano
5b0cc1f13c viz7 2021-07-02 09:31:44 -06:00
Brian Picciano
14dc57c110 maddy 2021-06-29 09:17:24 -06:00
Brian Picciano
9be52bdaa4 viz6 2021-06-23 18:18:31 -06:00
Brian Picciano
21fe465deb defi 2021-06-08 08:56:47 -06:00
Brian Picciano
cb0595a9ce fix viz5 description 2021-05-28 22:35:17 -06:00
Brian Picciano
93fbf516aa viz5 2021-05-28 22:31:14 -06:00
Brian Picciano
1b1bc6ffd6 both new posts from vacation 2021-05-27 15:20:39 -06:00
Brian Picciano
681c5d8123 ripple v3 2021-05-11 18:31:32 -06:00
Brian Picciano
42c4692abc nfts 2021-05-04 08:37:34 -06:00
Brian Picciano
b95218e9d4 ginger loops 2021-04-27 08:45:28 -06:00
Brian Picciano
9ef363410f nix process composition 2021-04-22 11:21:13 -06:00
Brian Picciano
6ecd78dc62 fix reset button 2021-04-12 13:41:57 -06:00
Brian Picciano
9556472df5 ripple v2 2021-04-12 13:31:45 -06:00
Brian Picciano
72378552c3 distributed network file systems 2021-04-06 17:01:24 -06:00
Brian Picciano
f43f742fe0 fmail 2021-04-01 18:46:15 -06:00
Brian Picciano
7ab3b7ef36 better errors 2021-03-20 09:54:22 -06:00
Brian Picciano
78a5df1684 draw player last so ripples are always on bottom 2021-03-12 18:08:30 -07:00
Brian Picciano
481d14784d forgot the tag 2021-03-12 18:06:41 -07:00
Brian Picciano
9472718f52 ripple 2021-03-12 18:00:41 -07:00
Brian Picciano
3f01bac76d ginger conditionals errata 2021-03-04 17:21:00 -07:00
Brian Picciano
2387d70266 conditionals in ginger 2021-03-01 19:09:18 -07:00
Brian Picciano
8248024277 wedding 2021-02-26 13:56:52 -07:00
Brian Picciano
345faf3e89 building gomobile using nix post 2021-02-13 14:00:40 -07:00
Brian Picciano
5078982aa4 forgot to add a tag for last post 2021-02-08 11:04:18 -07:00
Brian Picciano
e6501d254f old code new ideas 2021-02-07 16:22:35 -07:00
Brian Picciano
267e0f21d8 add tags to all posts, and generate per-tag feeds 2021-02-01 21:49:41 -07:00
Brian Picciano
90b897dbbc dramatically simplify project dependencies by specifying specific jekyll and plugin versions 2021-02-01 21:34:27 -07:00
Brian Picciano
6286b3d8ec make 'make clean' be more resilient 2021-01-30 16:01:53 -07:00
Brian Picciano
a083661e00 building mobile nebula 2021-01-30 16:00:33 -07:00
Brian Picciano
f5af5eaba7 updates to the goodbye github post 2021-01-23 18:07:03 -07:00
Brian Picciano
b2a271128c goodbye github pages, also make header look nicer on mobile 2021-01-23 17:55:02 -07:00
Brian Picciano
3a2309c264 fix up makefile, include install target 2021-01-22 22:09:35 -07:00
Brian Picciano
bcf9b230be build the blog with nix 2021-01-21 17:22:53 -07:00
510 changed files with 7447 additions and 237614 deletions

7
.gitignore vendored
View File

@ -1,6 +1 @@
.bundle result
.sass-cache
Gemfile.lock
_site
*.gem
.jekyll-metadata

View File

@ -1,8 +0,0 @@
---
layout: default
---
<div style="text-align: center; margin-top:5rem;">
<h1>404</h1>
<p><strong>Page not found :(</strong></p>
</div>

1
CNAME
View File

@ -1 +0,0 @@
blog.mediocregopher.com

View File

@ -1,5 +0,0 @@
# frozen_string_literal: true
source "https://rubygems.org"
gem "github-pages", group: :jekyll_plugins

View File

@ -1,261 +0,0 @@
GEM
remote: https://rubygems.org/
specs:
activesupport (6.0.3.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.11.1)
colorator (1.1.0)
commonmarker (0.17.13)
ruby-enum (~> 0.5)
concurrent-ruby (1.1.7)
dnsruby (1.61.5)
simpleidn (~> 0.1)
em-websocket (0.5.2)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
ethon (0.12.0)
ffi (>= 1.3.0)
eventmachine (1.2.7)
execjs (2.7.0)
faraday (1.1.0)
multipart-post (>= 1.2, < 3)
ruby2_keywords
ffi (1.13.1)
forwardable-extended (2.6.0)
gemoji (3.0.1)
github-pages (209)
github-pages-health-check (= 1.16.1)
jekyll (= 3.9.0)
jekyll-avatar (= 0.7.0)
jekyll-coffeescript (= 1.1.1)
jekyll-commonmark-ghpages (= 0.1.6)
jekyll-default-layout (= 0.1.4)
jekyll-feed (= 0.15.1)
jekyll-gist (= 1.5.0)
jekyll-github-metadata (= 2.13.0)
jekyll-mentions (= 1.6.0)
jekyll-optional-front-matter (= 0.3.2)
jekyll-paginate (= 1.1.0)
jekyll-readme-index (= 0.3.0)
jekyll-redirect-from (= 0.16.0)
jekyll-relative-links (= 0.6.1)
jekyll-remote-theme (= 0.4.2)
jekyll-sass-converter (= 1.5.2)
jekyll-seo-tag (= 2.6.1)
jekyll-sitemap (= 1.4.0)
jekyll-swiss (= 1.0.0)
jekyll-theme-architect (= 0.1.1)
jekyll-theme-cayman (= 0.1.1)
jekyll-theme-dinky (= 0.1.1)
jekyll-theme-hacker (= 0.1.2)
jekyll-theme-leap-day (= 0.1.1)
jekyll-theme-merlot (= 0.1.1)
jekyll-theme-midnight (= 0.1.1)
jekyll-theme-minimal (= 0.1.1)
jekyll-theme-modernist (= 0.1.1)
jekyll-theme-primer (= 0.5.4)
jekyll-theme-slate (= 0.1.1)
jekyll-theme-tactile (= 0.1.1)
jekyll-theme-time-machine (= 0.1.1)
jekyll-titles-from-headings (= 0.5.3)
jemoji (= 0.12.0)
kramdown (= 2.3.0)
kramdown-parser-gfm (= 1.1.0)
liquid (= 4.0.3)
mercenary (~> 0.3)
minima (= 2.5.1)
nokogiri (>= 1.10.4, < 2.0)
rouge (= 3.23.0)
terminal-table (~> 1.4)
github-pages-health-check (1.16.1)
addressable (~> 2.3)
dnsruby (~> 1.60)
octokit (~> 4.0)
public_suffix (~> 3.0)
typhoeus (~> 1.3)
html-pipeline (2.14.0)
activesupport (>= 2)
nokogiri (>= 1.4)
http_parser.rb (0.6.0)
i18n (0.9.5)
concurrent-ruby (~> 1.0)
jekyll (3.9.0)
addressable (~> 2.4)
colorator (~> 1.0)
em-websocket (~> 0.5)
i18n (~> 0.7)
jekyll-sass-converter (~> 1.0)
jekyll-watch (~> 2.0)
kramdown (>= 1.17, < 3)
liquid (~> 4.0)
mercenary (~> 0.3.3)
pathutil (~> 0.9)
rouge (>= 1.7, < 4)
safe_yaml (~> 1.0)
jekyll-avatar (0.7.0)
jekyll (>= 3.0, < 5.0)
jekyll-coffeescript (1.1.1)
coffee-script (~> 2.2)
coffee-script-source (~> 1.11.1)
jekyll-commonmark (1.3.1)
commonmarker (~> 0.14)
jekyll (>= 3.7, < 5.0)
jekyll-commonmark-ghpages (0.1.6)
commonmarker (~> 0.17.6)
jekyll-commonmark (~> 1.2)
rouge (>= 2.0, < 4.0)
jekyll-default-layout (0.1.4)
jekyll (~> 3.0)
jekyll-feed (0.15.1)
jekyll (>= 3.7, < 5.0)
jekyll-gist (1.5.0)
octokit (~> 4.2)
jekyll-github-metadata (2.13.0)
jekyll (>= 3.4, < 5.0)
octokit (~> 4.0, != 4.4.0)
jekyll-mentions (1.6.0)
html-pipeline (~> 2.3)
jekyll (>= 3.7, < 5.0)
jekyll-optional-front-matter (0.3.2)
jekyll (>= 3.0, < 5.0)
jekyll-paginate (1.1.0)
jekyll-readme-index (0.3.0)
jekyll (>= 3.0, < 5.0)
jekyll-redirect-from (0.16.0)
jekyll (>= 3.3, < 5.0)
jekyll-relative-links (0.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-remote-theme (0.4.2)
addressable (~> 2.0)
jekyll (>= 3.5, < 5.0)
jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
rubyzip (>= 1.3.0, < 3.0)
jekyll-sass-converter (1.5.2)
sass (~> 3.4)
jekyll-seo-tag (2.6.1)
jekyll (>= 3.3, < 5.0)
jekyll-sitemap (1.4.0)
jekyll (>= 3.7, < 5.0)
jekyll-swiss (1.0.0)
jekyll-theme-architect (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-cayman (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-dinky (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-hacker (0.1.2)
jekyll (> 3.5, < 5.0)
jekyll-seo-tag (~> 2.0)
jekyll-theme-leap-day (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-merlot (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-midnight (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-minimal (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-modernist (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-primer (0.5.4)
jekyll (> 3.5, < 5.0)
jekyll-github-metadata (~> 2.9)
jekyll-seo-tag (~> 2.0)
jekyll-theme-slate (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-tactile (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-theme-time-machine (0.1.1)
jekyll (~> 3.5)
jekyll-seo-tag (~> 2.0)
jekyll-titles-from-headings (0.5.3)
jekyll (>= 3.3, < 5.0)
jekyll-watch (2.2.1)
listen (~> 3.0)
jemoji (0.12.0)
gemoji (~> 3.0)
html-pipeline (~> 2.2)
jekyll (>= 3.0, < 5.0)
kramdown (2.3.0)
rexml
kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0)
liquid (4.0.3)
listen (3.3.1)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
mercenary (0.3.6)
mini_portile2 (2.4.0)
minima (2.5.1)
jekyll (>= 3.5, < 5.0)
jekyll-feed (~> 0.9)
jekyll-seo-tag (~> 2.1)
minitest (5.14.2)
multipart-post (2.1.1)
nokogiri (1.10.10)
mini_portile2 (~> 2.4.0)
octokit (4.19.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
pathutil (0.16.2)
forwardable-extended (~> 2.6)
public_suffix (3.1.1)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
rexml (3.2.4)
rouge (3.23.0)
ruby-enum (0.8.0)
i18n
ruby2_keywords (0.0.2)
rubyzip (2.3.0)
safe_yaml (1.0.5)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
simpleidn (0.1.1)
unf (~> 0.1.4)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thread_safe (0.3.6)
typhoeus (1.4.0)
ethon (>= 0.9.0)
tzinfo (1.2.8)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
zeitwerk (2.4.1)
PLATFORMS
ruby
DEPENDENCIES
github-pages
BUNDLED WITH
2.1.4

View File

@ -1,21 +1,14 @@
The MIT License (MIT) DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (c) 2016-present Parker Moore and the minima contributors Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Permission is hereby granted, free of charge, to any person obtaining a copy Everyone is permitted to copy and distribute verbatim or modified
of this software and associated documentation files (the "Software"), to deal copies of this license document, and changing it is allowed as long
in the Software without restriction, including without limitation the rights as the name is changed.
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
all copies or substantial portions of the Software. TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,12 +0,0 @@
serve:
docker run -it --rm \
-v $$(pwd):/srv/jekyll \
-p 4000:4000 \
jekyll/jekyll \
jekyll serve -w -I -D -H 0.0.0.0
update:
docker run -it --rm \
-v $$(pwd):/srv/jekyll \
jekyll/jekyll \
bundle update

View File

@ -1,29 +0,0 @@
title: Mediocre Blog
author: Brian Picciano
email: mediocregopher@gmail.com
description: >-
A mix of tech, art, travel, and who knows what else.
baseurl: "" # the subpath of your site, e.g. /blog
url: https://blog.mediocregopher.com # the base hostname & protocol for your site, e.g. http://example.com
repository: mediocregopher/blog.mediocregopher.com
twitter_username: mediocre_gopher
github_username: mediocregopher
rss: rss
highlighter: rouge
plugins:
- jekyll-feed
- jekyll-seo-tag
- jekyll-paginate
- jekyll-relative-links
date_format: "%b %-d, %Y"
img_widths:
- 500
- 1000
- 1500
- 2000
- 2500
- 3000

View File

@ -1,7 +0,0 @@
<footer>
<p class="license light">
Unless otherwised specified, all works are licensed under the
<a href="{{ '/assets/wtfpl.txt' | relative_url}}">WTFPL</a>.
</p>
</footer>

View File

@ -1,12 +0,0 @@
<script>
if(!(window.doNotTrack === "1" || navigator.doNotTrack === "1" || navigator.doNotTrack === "yes" || navigator.msDoNotTrack === "1")) {
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '{{ site.google_analytics }}', 'auto');
ga('send', 'pageview');
}
</script>

View File

@ -1,18 +0,0 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
{%- seo -%}
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="{{ "/assets/normalize.css" | relative_url }}">
<link rel="stylesheet" href="{{ "/assets/skeleton.css" | relative_url }}">
<link rel="stylesheet" href="{{ "/assets/friendly.css" | relative_url }}">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
<link rel="stylesheet" href="{{ "/assets/main.css" | relative_url }}">
{%- feed_meta -%}
{%- if jekyll.environment == 'production' and site.google_analytics -%}
{%- include google-analytics.html -%}
{%- endif -%}
<script src="{{ "/assets/main.js" | relative_url }}"></script>
</head>

View File

@ -1,36 +0,0 @@
<header id="title-header" role="banner">
<div class="row">
<div class="seven columns">
<h1 class="title">
<a href="{{ "/" | relative_url }}">{{ site.title | escape }}</a>
</h1>
<div class="light social">
<span>By {{ site.author | escape }}</span>
<span class="author-icons">
<a href="mailto:mediocregopher@gmail.com">
<i class="fas fa-envelope"></i>
</a>
<a href="https://github.com/{{ site.github_username }}">
<i class="fab fa-github"></i>
</a>
<a href="https://twitter.com/{{ site.twitter_username }}">
<i class="fab fa-twitter"></i>
</a>
</span>
</div>
</div>
{%- if!page.nofollow != true %}
<div class="five columns light" style="text-align: right">
<span style="display:block; margin-bottom:0.5rem;">Get notified when new posts are published!</span>
<a href="{{ "/follow.html" | relative_url }}"><button class="button-primary">
<i class="fab fa-twitter"></i>
Follow
</button></a>
<a href="{{ "/feed.xml" | relative_url }}"><button class="button">
<i class="fas fa-rss"></i>
RSS
</button></a>
</div>
{% endif -%}
</div>
</header>

View File

@ -1,43 +0,0 @@
<div style="
box-sizing: border-box;
text-align: center;
padding-left: 2em;
padding-right: 2em;
margin-bottom: 1em;
{%- if include.float %}
float: {{ include.float }};
{% endif -%}
{%- if include.float or include.inline %}
max-width: 49%;
{% endif -%}
{%- if include.inline %}
display: inline-block;
{% endif -%}
">
<a href="/img/{{ include.dir }}/{{ include.file }}" target="_blank">
<picture>
{%- if include.width %}
{%- for targetWidth in site.img_widths reversed -%}
{% if include.width <= targetWidth %}{% continue %}{% endif %}
{%- if targetWidth > 1000 %}
<source media="(min-width: 1000px) and (min-resolution: {{ targetWidth | divided_by: 1000.0 }}dppx)"
{%- elsif targetWidth > 500 %}
<source media="(min-width: 500px), (min-resolution: 1.1dppx)"
{%- else %}
<source
{% endif %}
srcset="/img/{{ include.dir }}/{{ targetWidth }}px/{{ include.file }}"
>
{%- endfor %}
{%- endif %}
<img style="max-height: 60vh;"
{% if include.width < 1000 %}
src="/img/{{ include.dir }}/{{ include.file }}"
{% else %}
src="/img/{{ include.dir }}/1000px/{{ include.file }}"
{% endif %}
alt="{{ include.descr }}" />
</picture>
</a>
{%- if include.descr %}<br/><em>{{ include.descr }}</em>{%- endif %}
</div>

View File

@ -1,10 +0,0 @@
---
layout: default
---
{% capture body %}```{{ page.lang | default: "go" }}
{% include_relative {{ page.include }} %}```{% endcapture %}
<br/><a href="{{ page.include }}">Raw source file</a>
{{ body | markdownify }}

View File

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="{{ page.lang | default: site.lang | default: "en" }}">
{%- include head.html -%}
<body>
<div class="container">
{%- include header.html -%}
<main aria-label="Content">
{{ content }}
</main>
{%- include footer.html -%}
</div>
</body>
</html>

View File

@ -1,13 +0,0 @@
---
layout: default
---
<header id="post-header">
<h1 id="post-headline" itemprop="name headline">
{{ page.title | escape }}
</h1>
</header>
<div id="post-content">
{{ content }}
</div>

View File

@ -1,80 +0,0 @@
---
layout: default
---
<article itemscope itemtype="http://schema.org/BlogPosting">
<header id="post-header">
<h1 id="post-headline" itemprop="name headline">
{{ page.title | escape }}
</h1>
<div class="light">
<span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
<span itemprop="name">{{ site.author }}</span>
</span>
<!---->
<time datetime="{{ page.date | date_to_xmlschema }}" itemprop="datePublished">
{{ page.date | date: site.date_format }}
</time>
{%- if page.updated %}
<time datetime="{{ page.updated | date_to_xmlschema }}" itemprop="dateModified">
(Updated {{ page.updated | date: site.date_format }})
</time>
{% endif -%}
<description itemprop="about"><em>{{ page.description }}</em></description>
</div>
</header>
{% if page.series %}
{% assign foundThis = false %}
{% for post in site.posts reversed %}
{% if post.series == page.series %}
{% if post.url == page.url %}
{% assign foundThis = true %}
{% elsif foundThis %}
{% assign next = post %}
{% break %}
{% else %}
{% assign prev = post %}
{% endif %}
{% endif %}
{% endfor %}
{% if prev or next %}
<p class="light"><em>
This post is part of a series:<br/>
{% if prev %}
Previously: <a href="{{ prev.url | relative_url }}">{{ prev.title }}</a></br>
{% endif %}
{% if next %}
Next: <a href="{{ next.url | relative_url }}">{{ next.title }}</a></br>
{% endif %}
</em></p>
{% endif %}
{% endif %}
<div id="post-content" itemprop="articleBody">
{{ content }}
</div>
{% if page.git_repo %}
<p class="light">
<em>To check this project out locally:</em></br>
<pre><code>git clone {{ page.git_repo }}
{% if page.git_commit %}git checkout {{ page.git_commit }}{% endif %}</code></pre>
</p>
{% endif %}
{% if prev or next %}
<p class="light"><em>
If you liked this post, consider checking out other posts in the series:<br/>
{% if prev %}
Previously: <a href="{{ prev.url | relative_url }}">{{ prev.title }}</a></br>
{% endif %}
{% if next %}
Next: <a href="{{ next.url | relative_url }}">{{ next.title }}</a></br>
{% endif %}
</em></p>
{% endif %}
</article>

View File

@ -1,256 +0,0 @@
---
title: "Erlang, tcp sockets, and active true"
description: >-
Using `{active:once}` isn't always the best way to handle connections.
---
If you don't know erlang then [you're missing out][0]. If you do know erlang,
you've probably at some point done something with tcp sockets. Erlang's highly
concurrent model of execution lends itself well to server programs where a high
number of active connections is desired. Each thread can autonomously handle its
single client, greatly simplifying the logic of the whole application while
still retaining [great performance characteristics][1].
## Background
For an erlang thread which owns a single socket there are three different ways
to receive data off of that socket. These all revolve around the `active`
[setopts][2] flag. A socket can be set to one of:
* `{active,false}` - All data must be obtained through [recv/2][3] calls. This
amounts to syncronous socket reading.
* `{active,true}` - All data on the socket gets sent to the controlling thread
as a normal erlang message. It is the thread's
responsibility to keep up with the buffered data in the
message queue. This amounts to asyncronous socket reading.
* `{active,once}` - When set the socket is placed in `{active,true}` for a
single packet. That is, once set the thread can expect a
single message to be sent to when data comes in. To receive
any more data off of the socket the socket must either be
read from using [recv/2][3] or be put in `{active,once}` or
`{active,true}`.
## Which to use?
Many (most?) tutorials advocate using `{active,once}` in your application
\[0]\[1]\[2]. This has to do with usability and security. When in `{active,true}`
it's possible for a client to flood the connection faster than the receiving
process will process those messages, potentially eating up a lot of memory in
the VM. However, if you want to be able to receive both tcp data messages as
well as other messages from other erlang processes at the same time you can't
use `{active,false}`. So `{active,once}` is generally preferred because it
deals with both of these problems quite well.
## Why not to use `{active,once}`
Here's what your classic `{active,once}` enabled tcp socket implementation will
probably look like:
```erlang
-module(tcp_test).
-compile(export_all).
-define(TCP_OPTS, [
binary,
{packet, raw},
{nodelay,true},
{active, false},
{reuseaddr, true},
{keepalive,true},
{backlog,500}
]).
%Start listening
listen(Port) ->
{ok, L} = gen_tcp:listen(Port, ?TCP_OPTS),
?MODULE:accept(L).
%Accept a connection
accept(L) ->
{ok, Socket} = gen_tcp:accept(L),
?MODULE:read_loop(Socket),
io:fwrite("Done reading, connection was closed\n"),
?MODULE:accept(L).
%Read everything it sends us
read_loop(Socket) ->
inet:setopts(Socket, [{active, once}]),
receive
{tcp, _, _} ->
do_stuff_here,
?MODULE:read_loop(Socket);
{tcp_closed, _}-> donezo;
{tcp_error, _, _} -> donezo
end.
```
This code isn't actually usable for a production system; it doesn't even spawn a
new process for the new socket. But that's not the point I'm making. If I run it
with `tcp_test:listen(8000)`, and in other window do:
```bash
while [ 1 ]; do echo "aloha"; done | nc localhost 8000
```
We'll be flooding the the server with data pretty well. Using [eprof][4] we can
get an idea of how our code performs, and where the hang-ups are:
```erlang
1> eprof:start().
{ok,<0.34.0>}
2> P = spawn(tcp_test,listen,[8000]).
<0.36.0>
3> eprof:start_profiling([P]).
profiling
4> running_the_while_loop.
running_the_while_loop
5> eprof:stop_profiling().
profiling_stopped
6> eprof:analyze(procs,[{sort,time}]).
****** Process <0.36.0> -- 100.00 % of profiled time ***
FUNCTION CALLS % TIME [uS / CALLS]
-------- ----- --- ---- [----------]
prim_inet:type_value_2/2 6 0.00 0 [ 0.00]
....snip....
prim_inet:enc_opts/2 6 0.00 8 [ 1.33]
prim_inet:setopts/2 12303599 1.85 1466319 [ 0.12]
tcp_test:read_loop/1 12303598 2.22 1761775 [ 0.14]
prim_inet:encode_opt_val/1 12303599 3.50 2769285 [ 0.23]
prim_inet:ctl_cmd/3 12303600 4.29 3399333 [ 0.28]
prim_inet:enc_opt_val/2 24607203 5.28 4184818 [ 0.17]
inet:setopts/2 12303598 5.72 4533863 [ 0.37]
erlang:port_control/3 12303600 77.13 61085040 [ 4.96]
```
eprof shows us where our process is spending the majority of its time. The `%`
column indicates percentage of time the process spent during profiling inside
any function. We can pretty clearly see that the vast majority of time was spent
inside `erlang:port_control/3`, the BIF that `inet:setopts/2` uses to switch the
socket to `{active,once}` mode. Amongst the calls which were called on every
loop, it takes up by far the most amount of time. In addition all of those other
calls are also related to `inet:setopts/2`.
I'm gonna rewrite our little listen server to use `{active,true}`, and we'll do
it all again:
```erlang
-module(tcp_test).
-compile(export_all).
-define(TCP_OPTS, [
binary,
{packet, raw},
{nodelay,true},
{active, false},
{reuseaddr, true},
{keepalive,true},
{backlog,500}
]).
%Start listening
listen(Port) ->
{ok, L} = gen_tcp:listen(Port, ?TCP_OPTS),
?MODULE:accept(L).
%Accept a connection
accept(L) ->
{ok, Socket} = gen_tcp:accept(L),
inet:setopts(Socket, [{active, true}]), %Well this is new
?MODULE:read_loop(Socket),
io:fwrite("Done reading, connection was closed\n"),
?MODULE:accept(L).
%Read everything it sends us
read_loop(Socket) ->
%inet:setopts(Socket, [{active, once}]),
receive
{tcp, _, _} ->
do_stuff_here,
?MODULE:read_loop(Socket);
{tcp_closed, _}-> donezo;
{tcp_error, _, _} -> donezo
end.
```
And the profiling results:
```erlang
1> eprof:start().
{ok,<0.34.0>}
2> P = spawn(tcp_test,listen,[8000]).
<0.36.0>
3> eprof:start_profiling([P]).
profiling
4> running_the_while_loop.
running_the_while_loop
5> eprof:stop_profiling().
profiling_stopped
6> eprof:analyze(procs,[{sort,time}]).
****** Process <0.36.0> -- 100.00 % of profiled time ***
FUNCTION CALLS % TIME [uS / CALLS]
-------- ----- --- ---- [----------]
prim_inet:enc_value_1/3 7 0.00 1 [ 0.14]
prim_inet:decode_opt_val/1 1 0.00 1 [ 1.00]
inet:setopts/2 1 0.00 2 [ 2.00]
prim_inet:setopts/2 2 0.00 2 [ 1.00]
prim_inet:enum_name/2 1 0.00 2 [ 2.00]
erlang:port_set_data/2 1 0.00 2 [ 2.00]
inet_db:register_socket/2 1 0.00 3 [ 3.00]
prim_inet:type_value_1/3 7 0.00 3 [ 0.43]
.... snip ....
prim_inet:type_opt_1/1 19 0.00 7 [ 0.37]
prim_inet:enc_value/3 7 0.00 7 [ 1.00]
prim_inet:enum_val/2 6 0.00 7 [ 1.17]
prim_inet:dec_opt_val/1 7 0.00 7 [ 1.00]
prim_inet:dec_value/2 6 0.00 10 [ 1.67]
prim_inet:enc_opt/1 13 0.00 12 [ 0.92]
prim_inet:type_opt/2 19 0.00 33 [ 1.74]
erlang:port_control/3 3 0.00 59 [ 19.67]
tcp_test:read_loop/1 20716370 100.00 12187488 [ 0.59]
```
This time our process spent almost no time at all (according to eprof, 0%)
fiddling with the socket opts. Instead it spent all of its time in the
read_loop doing the work we actually want to be doing.
## So what does this mean?
I'm by no means advocating never using `{active,once}`. The security concern is
still a completely valid concern and one that `{active,once}` mitigates quite
well. I'm simply pointing out that this mitigation has some fairly serious
performance implications which have the potential to bite you if you're not
careful, especially in cases where a socket is going to be receiving a large
amount of traffic.
## Meta
These tests were done using R15B03, but I've done similar ones in R14 and found
similar results. I have not tested R16.
* \[0] http://learnyousomeerlang.com/buckets-of-sockets
* \[1] http://www.erlang.org/doc/man/gen_tcp.html#examples
* \[2] http://erlycoder.com/25/erlang-tcp-server-tcp-client-sockets-with-gen_tcp
[0]: http://learnyousomeerlang.com/content
[1]: http://www.metabrew.com/article/a-million-user-comet-application-with-mochiweb-part-1
[2]: http://www.erlang.org/doc/man/inet.html#setopts-2
[3]: http://www.erlang.org/doc/man/gen_tcp.html#recv-2
[4]: http://www.erlang.org/doc/man/eprof.html

View File

@ -1,77 +0,0 @@
---
title: Go+
description: >-
A simple proof-of-concept script for doing go dependency management.
---
Compared to other languages go has some strange behavior regarding its project
root settings. If you import a library called `somelib`, go will look for a
`src/somelib` folder in all of the folders in the `$GOPATH` environment
variable. This works nicely for globally installed packages, but it makes
encapsulating a project with a specific version, or modified version, rather
tedious. Whenever you go to work on this project you'll have to add its path to
your `$GOPATH`, or add the path permanently, which could break other projects
which may use a different version of `somelib`.
My solution is in the form of a simple script I'm calling go+. go+ will search
in currrent directory and all of its parents for a file called `GOPROJROOT`. If
it finds that file in a directory, it prepends that directory's absolute path to
your `$GOPATH` and stops the search. Regardless of whether or not `GOPROJROOT`
was found go+ will passthrough all arguments to the actual go call. The
modification to `$GOPATH` will only last the duration of the call.
As an example, consider the following:
```
/tmp
/hello
GOPROJROOT
/src
/somelib/somelib.go
/hello.go
```
If `hello.go` depends on `somelib`, as long as you run go+ from `/tmp/hello` or
one of its children your project will still compile
Here is the source code for go+:
```bash
#!/bin/sh
SEARCHING_FOR=GOPROJROOT
ORIG_DIR=$(pwd)
STOPSEARCH=0
SEARCH_DIR=$ORIG_DIR
while [ $STOPSEARCH = 0 ]; do
RES=$( find $SEARCH_DIR -maxdepth 1 -type f -name $SEARCHING_FOR | \
grep -P "$SEARCHING_FOR$" | \
head -n1 )
if [ "$RES" = "" ]; then
if [ "$SEARCH_DIR" = "/" ]; then
STOPSEARCH=1
fi
cd ..
SEARCH_DIR=$(pwd)
else
export GOPATH=$SEARCH_DIR:$GOPATH
STOPSEARCH=1
fi
done
cd "$ORIG_DIR"
exec go $@
```
## UPDATE: Goat
I'm leaving this post for posterity, but go+ has some serious flaws in it. For
one, it doesn't allow for specifying the version of a dependency you want to
use. To this end, I wrote [goat][0] which does all the things go+ does, plus
real dependency management, PLUS it is built in a way that if you've been
following go's best-practices for code organization you shouldn't have to change
any of your existing code AT ALL. It's cool, check it out.
[0]: http://github.com/mediocregopher/goat

View File

@ -1,100 +0,0 @@
---
title: Generations
description: >-
A simple file distribution strategy for very large scale, high-availability
file-services.
---
## The problem
At [cryptic.io][cryptic] we plan on having millions of different
files, any of which could be arbitrarily chosen to be served any given time.
These files are uploaded by users at arbitrary times.
Scaling such a system is no easy task. The solution I've seen implemented in the
past involves shuffling files around on a nearly constant basis, making sure
that files which are more "popular" are on fast drives, while at the same time
making sure that no drives are at capicty and at the same time that all files,
even newly uploaded ones, are stored redundantly.
The problem with this solution is one of coordination. At any given moment the
app needs to be able to "find" a file so it can give the client a link to
download the file from one of the servers that it's on. Full-filling this simple
requirement means that all datastores/caches where information about where a
file lives need to be up-to-date at all times, and even then there are
race-conditions and network failures to contend with, while at all times the
requirements of the app evolve and change.
## A simpler solution
Let's say you want all files which get uploaded to be replicated in triplicate
in some capacity. You buy three identical hard-disks, and put each on a separate
server. As files get uploaded by clients, each file gets put on each drive
immediately. When the drives are filled (which should be at around the same
time), you stop uploading to them.
That was generation 0.
You buy three more drives, and start putting all files on them instead. This is
going to be generation 1. Repeat until you run out of money.
That's it.
### That's it?
It seems simple and obvious, and maybe it's the standard thing which is done,
but as far as I can tell no-one has written about it (though I'm probably not
searching for the right thing, let me know if this is the case!).
### Advantages
* It's so simple to implement, you could probably do it in a day if you're
starting a project from scratch
* By definition of the scheme all files are replicated in multiple places.
* Minimal information about where a file "is" needs to be stored. When a file is
uploaded all that's needed is to know what generation it is in, and then what
nodes/drives are in that generation. If the file's name is generated
server-side, then the file's generation could be *part* of its name, making
lookup even faster.
* Drives don't need to "know" about each other. What I mean by this is that
whatever is running as the receive point for file-uploads on each drive doesn't
have to coordinate with its siblings running on the other drives in the
generation. In fact it doesn't need to coordinate with anyone. You could
literally rsync files onto your drives if you wanted to. I would recommend using
[marlin][0] though :)
* Scaling is easy. When you run out of space you can simply start a new
generation. If you don't like playing that close to the chest there's nothing to
say you can't have two generations active at the same time.
* Upgrading is easy. As long as a generation is not marked-for-upload, you can
easily copy all files in the generation into a new set of bigger, badder drives,
add those drives into the generation in your code, remove the old ones, then
mark the generation as uploadable again.
* Distribution is easy. You just copy a generation's files onto a new drive in
Europe or wherever you're getting an uptick in traffic from and you're good to
go.
* Management is easy. It's trivial to find out how many times a file has been
replicated, or how many countries it's in, or what hardware it's being served
from (given you have easy access to information about specific drives).
### Caveats
The big caveat here is that this is just an idea. It has NOT been tested in
production. But we have enough faith in it that we're going to give it a shot at
[cryptic.io][cryptic]. I'll keep this page updated.
The second caveat is that this scheme does not inherently support caching. If a
file suddenly becomes super popular the world over your hard-disks might not be
able to keep up, and it's probably not feasible to have an FIO drive in *every*
generation. I think that [groupcache][1] may be the answer to this problem,
assuming your files are reasonably small, but again I haven't tested it yet.
[cryptic]: https://cryptic.io
[0]: https://github.com/cryptic-io/marlin
[1]: https://github.com/golang/groupcache

View File

@ -1,248 +0,0 @@
---
title: Namecoin, A Replacement For SSL
description: >-
If we use the namecoin chain as a DNS service we get security almost for
free, along with lots of other benefits.
---
At [cryptic.io][cryptic] we are creating a client-side, in-browser encryption
system where a user can upload their already encrypted content to our storage
system and be 100% confident that their data can never be decrypted by anyone
but them.
One of the main problems with this approach is that the client has to be sure
that the code that's being run in their browser is the correct code; that is,
that they aren't the subject of a man-in-the-middle attack where an attacker is
turning our strong encryption into weak encryption that they could later break.
A component of our current solution is to deliver the site's javascript (and all
other assets, for that matter) using SSL encryption. This protects the files
from tampering in-between leaving our servers and being received by the client.
Unfortunately, SSL isn't 100% foolproof. This post aims to show why SSL is
faulty, and propose a solution.
## SSL
SSL is the mechanism by which web-browsers establish an encrypted connection to
web-servers. The goal of this connection is that only the destination
web-browser and the server know what data is passing between them. Anyone spying
on the connection would only see gibberish. To do this a secret key is first
established between the client and the server, and used to encrypt/decrypt all
data. As long as no-one but those parties knows that key, that data will never
be decrypted by anyone else.
SSL is what's used to establish that secret key on a per-session basis, so that
a key isn't ever re-used and so only the client and the server know it.
### Public-Private Key Cryptography
SSL is based around public-private key cryptography. In a public-private key
system, you have both a public key which is generated from a private key. The
public key can be given to anyone, but the private key must remain hidden. There
are two main uses for these two keys:
* Someone can encrypt a message with your public key, and only you (with the
private key) can decrypt it.
* You can sign a message with your private key, and anyone with your public key
can verify that it was you and not someone else who signed it.
These are both extremely useful functions, not just for internet traffic but for
any kind of communication form. Unfortunately, there remains a fundamental flaw.
At some point you must give your public key to the other person in an insecure
way. If an attacker was to intercept your message containing your public key and
swap it for their own, then all future communications could be compromised. That
attacker could create messages the other person would think are from you, and
the other person would encrypt messages meant for you but which would be
decrypt-able by the attacker.
### How does SSL work?
SSL is at its heart a public-private key system, but its aim is to be more
secure against the attack described above.
SSL uses a trust-chain to verify that a public key is the intended one. Your web
browser has a built-in set of public keys, called the root certificates, that it
implicitly trusts. These root certificates are managed by a small number of
companies designated by some agency who decides on these things.
When you receive a server's SSL certificate (its public key) that certificate
will be signed by a root certificate. You can verify that signature since you
have the root certificate's public key built into your browser. If the signature
checks out then you know a certificate authority trusts the public key the site
gave you, which means you can trust it too.
There's a bit (a lot!) more to SSL than this, but this is enough to understand
the fundamental problems with it.
### How SSL doesn't work
SSL has a few glaring problems. One, it implies we trust the companies holding
the root certificates to not be compromised. If some malicious agency was to get
ahold of a root certificate they could listen in on any connection on the
internet by swapping a site's real certificate with one they generate on the
fly. They could trivially steal any data we send on the internet.
The second problem is that it's expensive. Really expensive. If you're running a
business you'll have to shell out about $200 a year to keep your SSL certificate
signed (those signatures have an expiration date attached). Since there's very
few root authorities there's an effective monopoly on signatures, and there's
nothing we can do about it. For 200 bucks I know most people simply say "no
thanks" and go unencrypted. The solution is creating a bigger problem.
## Bitcoins
Time to switch gears, and propose a solution to the above issues: namecoins. I'm
going to first talk about what namecoins are, how they work, and why we need
them. To start with, namecoins are based on bitcoins.
If you haven't yet checked out bitcoins, [I highly encourage you to do
so][bitcoins]. They're awesome, and I think they have a chance of really
changing the way we think of and use money in the future. At the moment they're
still a bit of a novelty in the tech realm, but they're growing in popularity.
The rest of this post assumes you know more or less what bitcoins are, and how
they work.
## Namecoins
Few people actually know about bitcoins. Even fewer know that there's other
crypto-currencies besides bitcoins. Basically, developers of these alternative
currencies (altcoins, in the parlance of our times) took the original bitcoin
source code and modified it to produce a new, separate blockchain from the
original bitcoin one. The altcoins are based on the same idea as bitcoins
(namely, a chain of blocks representing all the transactions ever made), but
have slightly different characterstics.
One of these altcoins is called namecoin. Where other altcoins aim to be digital
currencies, and used as such (like bitcoins), namecoin has a different goal. The
point of namecoin is to create a global, distributed, secure key-value store.
You spend namecoins to claim arbitrary keys (once you've claimed it, you own it
for a set period of time) and to give those keys arbitrary values. Anyone else
with namecoind running can see these values.
### Why use it?
A blockchain based on a digital currency seems like a weird idea at first. I
know when I first read about it I was less than thrilled. How is this better
than a DHT? It's a key-value store, why is there a currency involved?
#### DHT
DHT stands for Distributed Hash-Table. I'm not going to go too into how they
work, but suffice it to say that they are essentially a distributed key-value
store. Like namecoin. The difference is in the operation. DHTs operate by
spreading and replicating keys and their values across nodes in a P2P mesh. They
have [lots of issues][dht] as far as security goes, the main one being that it's
fairly easy for an attacker to forge the value for a given key, and very
difficult to stop them from doing so or even to detect that it's happened.
Namecoins don't have this problem. To forge a particular key an attacker would
essentially have to create a new blockchain from a certain point in the existing
chain, and then replicate all the work put into the existing chain into that new
compromised one so that the new one is longer and other clients in the network
will except it. This is extremely non-trivial.
#### Why a currency?
To answer why a currency needs to be involved, we need to first look at how
bitcoin/namecoin work. When you take an action (send someone money, set a value
to a key) that action gets broadcast to the network. Nodes on the network
collect these actions into a block, which is just a collection of multiple
actions. Their goal is to find a hash of this new block, combined with some data
from the top-most block in the existing chain, combined with some arbitrary
data, such that the first n characters in the resulting hash are zeros (with n
constantly increasing). When they find one they broadcast it out on the network.
Assuming the block is legitimate they receive some number of coins as
compensation.
That compensation is what keeps a blockchain based currency going. If there
were no compensation there would be no reason to mine except out of goodwill, so
far fewer people would do it. Since the chain can be compromised if a malicious
group has more computing power than all legitimate miners combined, having few
legitimate miners is a serious problem.
In the case of namecoins, there's even more reason to involve a currency. Since
you have to spend money to make changes to the chain there's a disincentive for
attackers (read: idiots) to spam the chain with frivolous changes to keys.
#### Why a *new* currency?
I'll admit, it's a bit annoying to see all these altcoins popping up. I'm sure
many of them have some solid ideas backing them, but it also makes things
confusing for newcomers and dilutes the "market" of cryptocoin users; the more
users a particular chain has, the stronger it is. If we have many chains, all we
have are a bunch of weak chains.
The exception to this gripe, for me, is namecoin. When I was first thinking
about this problem my instinct was to just use the existing bitcoin blockchain
as a key-value storage. However, the maintainers of the bitcoin clients
(who are, in effect, the maintainers of the chain) don't want the bitcoin
blockchain polluted with non-commerce related data. At first I disagreed; it's a
P2P network, no-one gets to say what I can or can't use the chain for! And
that's true. But things work out better for everyone involved if there's two
chains.
Bitcoin is a currency. Namecoin is a key-value store (with a currency as its
driving force). Those are two completely different use-cases, with two
completely difference usage characteristics. And we don't know yet what those
characteristics are, or if they'll change. If the chain-maintainers have to deal
with a mingled chain we could very well be tying their hands with regards to
what they can or can't change with regards to the behavior of the chain, since
improving performance for one use-case may hurt the performance of the other.
With two separate chains the maintainers of each are free to do what they see
fit to keep their respective chains operating as smoothly as possible.
Additionally, if for some reason bitcoins fall by the wayside, namecoin will
still have a shot at continuing operation since it isn't tied to the former.
Tldr: separation of concerns.
## Namecoin as an alternative to SSL
And now to tie it all together.
There are already a number of proposed formats for standardizing how we store
data on the namecoin chain so that we can start building tools around it. I'm
not hugely concerned with the particulars of those standards, only that we can,
in some way, standardize on attaching a public key (or a fingerprint of one) to
some key on the namecoin blockchain. When you visit a website, the server
would then send both its public key and the namecoin chain key to be checked
against to the browser, and the browser would validate that the public key it
received is the same as the one on the namecoin chain.
The main issue with this is that it requires another round-trip when visiting a
website: One for DNS, and one to check the namecoin chain. And where would this
chain even be hosted?
My proposition is there would exist a number of publicly available servers
hosting a namecoind process that anyone in the world could send requests for
values on the chain. Browsers could then be made with a couple of these
hardwired in. ISPs could also run their own copies at various points in their
network to improve response-rates and decrease load on the globally public
servers. Furthermore, the paranoid could host their own and be absolutely sure
that the data they're receiving is valid.
If the above scheme sounds a lot like what we currently use for DNS, that's
because it is. In fact, one of namecoin's major goals is that it be used as a
replacement for DNS, and most of the talk around it is focused on this subject.
DNS has many of the same problems as SSL, namely single-point-of-failure and
that it's run by a centralized agency that we have to pay arbitrarily high fees
to. By switching our DNS and SSL infrastructure to use namecoin we could kill
two horribly annoying, monopolized, expensive birds with a single stone.
That's it. If we use the namecoin chain as a DNS service we get security almost
for free, along with lots of other benefits. To make this happen we need
cooperation from browser makers, and to standardize on a simple way of
retrieving DNS information from the chain that the browsers can use. The
protocol doesn't need to be very complex, I think HTTP/REST should suffice,
since the meat of the data will be embedded in the JSON value on the namecoin
chain.
If you want to contribute or learn more please check out [namecoin][nmc] and
specifically the [d namespace proposal][dns] for it.
[cryptic]: http://cryptic.io
[bitcoins]: http://vimeo.com/63502573
[dht]: http://www.globule.org/publi/SDST_acmcs2009.pdf
[nsa]: https://www.schneier.com/blog/archives/2013/09/new_nsa_leak_sh.html
[nmc]: http://dot-bit.org/Main_Page
[dns]: http://dot-bit.org/Namespace:Domain_names_v2.0

View File

@ -1,494 +0,0 @@
---
title: Diamond Square
description: >-
Tackling the problem of semi-realistic looking terrain generation in
clojure.
updated: 2018-09-06
---
![terrain][terrain]
I recently started looking into the diamond-square algorithm (you can find a
great article on it [here][diamondsquare]). The following is a short-ish
walkthrough of how I tackled the problem in clojure and the results. You can
find the [leiningen][lein] repo [here][repo] and follow along within that, or
simply read the code below to get an idea.
Also, Marco ported my code into clojurescript, so you can get random terrain
in your browser. [Check it out!][marco]
```clojure
(ns diamond-square.core)
; == The Goal ==
; Create a fractal terrain generator using clojure
; == The Algorithm ==
; Diamond-Square. We start with a grid of points, each with a height of 0.
;
; 1. Take each corner point of the square, average the heights, and assign that
; to be the height of the midpoint of the square. Apply some random error to
; the midpoint.
;
; 2. Creating a line from the midpoint to each corner we get four half-diamonds.
; Average the heights of the points (with some random error) and assign the
; heights to the midpoints of the diamonds.
;
; 3. We now have four square sections, start at 1 for each of them (with
; decreasing amount of error for each iteration).
;
; This picture explains it better than I can:
; https://blog.mediocregopher.com/img/diamond-square/dsalg.png
; (http://nbickford.wordpress.com/2012/12/21/creating-fake-landscapes/dsalg/)
;
; == The Strategy ==
; We begin with a vector of vectors of numbers, and iterate over it, filling in
; spots as they become available. Our grid will have the top-left being (0,0),
; y being pointing down and x going to the right. The outermost vector
; indicating row number (y) and the inner vectors indicate the column number (x)
;
; = Utility =
; First we create some utility functions for dealing with vectors of vectors.
(defn print-m
"Prints a grid in a nice way"
[m]
(doseq [n m]
(println n)))
(defn get-m
"Gets a value at the given x,y coordinate of the grid, with [0,0] being in the
top left"
[m x y]
((m y) x))
(defn set-m
"Sets a value at the given x,y coordinat of the grid, with [0,0] being in the
top left"
[m x y v]
(assoc m y
(assoc (m y) x v)))
(defn add-m
"Like set-m, but adds the given value to the current on instead of overwriting
it"
[m x y v]
(set-m m x y
(+ (get-m m x y) v)))
(defn avg
"Returns the truncated average of all the given arguments"
[& l]
(int (/ (reduce + l) (count l))))
; = Grid size =
; Since we're starting with a blank grid we need to find out what sizes the
; grids can be. For convenience the size (height and width) should be odd, so we
; easily get a midpoint. And on each iteration we'll be halfing the grid, so
; whenever we do that the two resultrant grids should be odd and halfable as
; well, and so on.
;
; The algorithm that fits this is size = 2^n + 1, where 1 <= n. For the rest of
; this guide I'll be referring to n as the "degree" of the grid.
(def exp2-pre-compute
(vec (map #(int (Math/pow 2 %)) (range 31))))
(defn exp2
"Returns 2^n as an integer. Uses pre-computed values since we end up doing
this so much"
[n]
(exp2-pre-compute n))
(def grid-sizes
(vec (map #(inc (exp2 %)) (range 1 31))))
(defn grid-size [degree]
(inc (exp2 degree)))
; Available grid heights/widths are as follows:
;[3 5 9 17 33 65 129 257 513 1025 2049 4097 8193 16385 32769 65537 131073
;262145 524289 1048577 2097153 4194305 8388609 16777217 33554433 67108865
;134217729 268435457 536870913 1073741825])
(defn blank-grid
"Generates a grid of the given degree, filled in with zeros"
[degree]
(let [gsize (grid-size degree)]
(vec (repeat gsize
(vec (repeat gsize 0))))))
(comment
(print-m (blank-grid 3))
)
; = Coordinate Pattern (The Tricky Part) =
; We now have to figure out which coordinates need to be filled in on each pass.
; A pass is defined as a square step followed by a diamond step. The next pass
; will be the square/dimaond steps on all the smaller squares generated in the
; pass. It works out that the number of passes required to fill in the grid is
; the same as the degree of the grid, where the first pass is 1.
;
; So we can easily find patterns in the coordinates for a given degree/pass,
; I've laid out below all the coordinates for each pass for a 3rd degree grid
; (which is 9x9).
; Degree 3 Pass 1 Square
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . . . 1 . . . .] (4,4)
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . . . . . . . .]
; Degree 3 Pass 1 Diamond
; [. . . . 2 . . . .] (4,0)
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . . . . . . . .]
; [2 . . . . . . . 2] (0,4) (8,4)
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . . . 2 . . . .] (4,8)
; Degree 3 Pass 2 Square
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . 3 . . . 3 . .] (2,2) (6,2)
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . . . . . . . .]
; [. . 3 . . . 3 . .] (2,6) (6,6)
; [. . . . . . . . .]
; [. . . . . . . . .]
; Degree 3 Pass 2 Diamond
; [. . 4 . . . 4 . .] (2,0) (6,0)
; [. . . . . . . . .]
; [4 . . . 4 . . . 4] (0,2) (4,2) (8,2)
; [. . . . . . . . .]
; [. . 4 . . . 4 . .] (2,4) (6,4)
; [. . . . . . . . .]
; [4 . . . 4 . . . 4] (0,6) (4,6) (8,6)
; [. . . . . . . . .]
; [. . 4 . . . 4 . .] (2,8) (6,8)
; Degree 3 Pass 3 Square
; [. . . . . . . . .]
; [. 5 . 5 . 5 . 5 .] (1,1) (3,1) (5,1) (7,1)
; [. . . . . . . . .]
; [. 5 . 5 . 5 . 5 .] (1,3) (3,3) (5,3) (7,3)
; [. . . . . . . . .]
; [. 5 . 5 . 5 . 5 .] (1,5) (3,5) (5,5) (7,5)
; [. . . . . . . . .]
; [. 5 . 5 . 5 . 5 .] (1,7) (3,7) (5,7) (7,7)
; [. . . . . . . . .]
; Degree 3 Pass 3 Square
; [. 6 . 6 . 6 . 6 .] (1,0) (3,0) (5,0) (7,0)
; [6 . 6 . 6 . 6 . 6] (0,1) (2,1) (4,1) (6,1) (8,1)
; [. 6 . 6 . 6 . 6 .] (1,2) (3,2) (5,2) (7,2)
; [6 . 6 . 6 . 6 . 6] (0,3) (2,3) (4,3) (6,3) (8,3)
; [. 6 . 6 . 6 . 6 .] (1,4) (3,4) (5,4) (7,4)
; [6 . 6 . 6 . 6 . 6] (0,5) (2,5) (4,5) (6,5) (8,5)
; [. 6 . 6 . 6 . 6 .] (1,6) (3,6) (5,6) (7,6)
; [6 . 6 . 6 . 6 . 6] (0,7) (2,7) (4,7) (6,7) (8,7)
; [. 6 . 6 . 6 . 6 .] (1,8) (3,8) (5,8) (7,8)
;
; I make two different functions, one to give the coordinates for the square
; portion of each pass and one for the diamond portion of each pass. To find the
; actual patterns it was useful to first look only at the pattern in the
; y-coordinates, and figure out how that translated into the pattern for the
; x-coordinates.
(defn grid-square-coords
"Given a grid degree and pass number, returns all the coordinates which need
to be computed for the square step of that pass"
[degree pass]
(let [gsize (grid-size degree)
start (exp2 (- degree pass))
interval (* 2 start)
coords (map #(+ start (* interval %))
(range (exp2 (dec pass))))]
(mapcat (fn [y]
(map #(vector % y) coords))
coords)))
;
; (grid-square-coords 3 2)
; => ([2 2] [6 2] [2 6] [6 6])
(defn grid-diamond-coords
"Given a grid degree and a pass number, returns all the coordinates which need
to be computed for the diamond step of that pass"
[degree pass]
(let [gsize (grid-size degree)
interval (exp2 (- degree pass))
num-coords (grid-size pass)
coords (map #(* interval %) (range 0 num-coords))]
(mapcat (fn [y]
(if (even? (/ y interval))
(map #(vector % y) (take-nth 2 (drop 1 coords)))
(map #(vector % y) (take-nth 2 coords))))
coords)))
; (grid-diamond-coords 3 2)
; => ([2 0] [6 0] [0 2] [4 2] [8 2] [2 4] [6 4] [0 6] [4 6] [8 6] [2 8] [6 8])
; = Height Generation =
; We now work on functions which, given a coordinate, will return what value
; coordinate will have.
(defn avg-points
"Given a grid and an arbitrary number of points (of the form [x y]) returns
the average of all the given points that are on the map. Any points which are
off the map are ignored"
[m & coords]
(let [grid-size (count m)]
(apply avg
(map #(apply get-m m %)
(filter
(fn [[x y]]
(and (< -1 x) (> grid-size x)
(< -1 y) (> grid-size y)))
coords)))))
(defn error
"Returns a number between -e and e, inclusive"
[e]
(- (rand-int (inc (* 2 e))) e))
; The next function is a little weird. It primarily takes in a point, then
; figures out the distance from that point to the points we'll take the average
; of. The locf (locator function) is used to return back the actual points to
; use. For the square portion it'll be the points diagonal from the given one,
; for the diamond portion it'll be the points to the top/bottom/left/right from
; the given one.
;
; Once it has those points, it finds the average and applies the error. The
; error function is nothing more than a number between -interval and +interval,
; where interval is the distance between the given point and one of the averaged
; points. It is important that the error decreases the more passes you do, which
; is why the interval is used.
;
; The error function is what should be messed with primarily if you want to
; change what kind of terrain you generate (a giant mountain instead of
; hills/valleys, for example). The one we use is uniform for all intervals, so
; it generates a uniform terrain.
(defn- grid-fill-point
[locf m degree pass x y]
(let [interval (exp2 (- degree pass))
leftx (- x interval)
rightx (+ x interval)
upy (- y interval)
downy (+ y interval)
v (apply avg-points m
(locf x y leftx rightx upy downy))]
(add-m m x y (+ v (error interval)))))
(def grid-fill-point-square
"Given a grid, the grid's degree, the current pass number, and a point on the
grid, fills in that point with the average (plus some error) of the
appropriate corner points, and returns the resultant grid"
(partial grid-fill-point
(fn [_ _ leftx rightx upy downy]
[[leftx upy]
[rightx upy]
[leftx downy]
[rightx downy]])))
(def grid-fill-point-diamond
"Given a grid, the grid's degree, the current pass number, and a point on the
grid, fills in that point with the average (plus some error) of the
appropriate edge points, and returns the resultant grid"
(partial grid-fill-point
(fn [x y leftx rightx upy downy]
[[leftx y]
[rightx y]
[x upy]
[x downy]])))
; = Filling in the Grid =
; We finally compose the functions we've been creating to fill in the entire
; grid
(defn- grid-fill-point-passes
"Given a grid, a function to fill in coordinates, and a function to generate
those coordinates, fills in all coordinates for a given pass, returning the
resultant grid"
[m fill-f coord-f degree pass]
(reduce
(fn [macc [x y]] (fill-f macc degree pass x y))
m
(coord-f degree pass)))
(defn grid-pass
"Given a grid and a pass number, does the square then the diamond portion of
the pass"
[m degree pass]
(-> m
(grid-fill-point-passes
grid-fill-point-square grid-square-coords degree pass)
(grid-fill-point-passes
grid-fill-point-diamond grid-diamond-coords degree pass)))
; The most important function in this guide, does all the work
(defn terrain
"Given a grid degree, generates a uniformly random terrain on a grid of that
degree"
([degree]
(terrain (blank-grid degree) degree))
([m degree]
(reduce
#(grid-pass %1 degree %2)
m
(range 1 (inc degree)))))
(comment
(print-m
(terrain 5))
)
; == The Results ==
; We now have a generated terrain, probably. We should check it. First we'll
; create an ASCII representation. But to do that we'll need some utility
; functions.
(defn max-terrain-height
"Returns the maximum height found in the given terrain grid"
[m]
(reduce max
(map #(reduce max %) m)))
(defn min-terrain-height
"Returns the minimum height found in the given terrain grid"
[m]
(reduce min
(map #(reduce min %) m)))
(defn norm
"Given x in the range (A,B), normalizes it into the range (0,new-height)"
[A B new-height x]
(int (/ (* (- x A) new-height) (- B A))))
(defn normalize-terrain
"Given a terrain map and a number of \"steps\", normalizes the terrain so all
heights in it are in the range (0,steps)"
[m steps]
(let [max-height (max-terrain-height m)
min-height (min-terrain-height m)
norm-f (partial norm min-height max-height steps)]
(vec (map #(vec (map norm-f %)) m))))
; We now define which ASCII characters we want to use for which heights. The
; vector starts with the character for the lowest height and ends with the
; character for the heighest height.
(def tiles
[\~ \~ \" \" \x \x \X \$ \% \# \@])
(defn tile-terrain
"Given a terrain map, converts it into an ASCII tile map"
[m]
(vec (map #(vec (map tiles %))
(normalize-terrain m (dec (count tiles))))))
(comment
(print-m
(tile-terrain
(terrain 5)))
; [~ ~ " " x x x X % $ $ $ X X X X X X $ x x x X X X x x x x " " " ~]
; [" ~ " " x x X X $ $ $ X X X X X X X X X X X X X X x x x x " " " "]
; [" " " x x x X X % $ % $ % $ $ X X X X $ $ $ X X X X x x x x " " "]
; [" " " x x X $ % % % % % $ % $ $ X X $ $ $ $ X X x x x x x x " " x]
; [" x x x x X $ $ # % % % % % % $ X $ X X % $ % X X x x x x x x x x]
; [x x x X $ $ $ % % % % % $ % $ $ $ % % $ $ $ $ X X x x x x x x x x]
; [X X X $ % $ % % # % % $ $ % % % % $ % $ $ X $ X $ X X x x x X x x]
; [$ $ X $ $ % $ % % % % $ $ $ % # % % % X X X $ $ $ X X X x x x x x]
; [% X X % % $ % % % $ % $ % % % # @ % $ $ X $ X X $ X x X X x x x x]
; [$ $ % % $ $ % % $ $ X $ $ % % % % $ $ X $ $ X X X X X X x x x x x]
; [% % % X $ $ % $ $ X X $ $ $ $ % % $ $ X X X $ X X X x x X x x X X]
; [$ $ $ X $ $ X $ X X X $ $ $ $ % $ $ $ $ $ X $ X x X X X X X x X X]
; [$ $ $ $ X X $ X X X X X $ % % % % % $ X $ $ $ X x X X X $ X X $ $]
; [X $ $ $ $ $ X X X X X X X % $ % $ $ $ X X X X X x x X X x X X $ $]
; [$ $ X X $ X X x X $ $ X X $ % X X X X X X X X X x X X x x X X X X]
; [$ $ X X X X X X X $ $ $ $ $ X $ X X X X X X X x x x x x x x X X X]
; [% % % $ $ X $ X % X X X % $ $ X X X X X X x x x x x x x x x X X $]
; [$ % % $ $ $ X X $ $ $ $ $ $ X X X X x X x x x x " x x x " x x x x]
; [$ X % $ $ $ $ $ X X X X X $ $ X X X X X X x x " " " " " " " " x x]
; [$ X $ $ % % $ X X X $ X X X x x X X x x x x x " " " " " ~ " " " "]
; [$ $ X X % $ % X X X X X X X X x x X X X x x x " " " " " " ~ " " "]
; [$ $ X $ % $ $ X X X X X X x x x x x x x x x " " " " " " " " " ~ ~]
; [$ $ $ $ $ X X $ X X X X X x x x x x x x x " " " " " " " ~ " " " ~]
; [$ % X X $ $ $ $ X X X X x x x x x x x x x x " " " " ~ " " ~ " " ~]
; [% $ $ X $ X $ X $ X $ X x x x x x x x x x x " " " " ~ ~ ~ " ~ " ~]
; [$ X X X X $ $ $ $ $ X x x x x x x x x x x " " " " ~ ~ ~ ~ ~ ~ ~ ~]
; [X x X X x X X X X X X X X x x x x x x x x x " " " ~ ~ " " ~ ~ ~ ~]
; [x x x x x x X x X X x X X X x x x x x x x " x " " " " " ~ ~ ~ ~ ~]
; [x x x x x x x x X X X X $ X X x X x x x x x x x x " ~ ~ ~ ~ ~ ~ ~]
; [" x x x x x X x X X X X X X X X X x x x x x x " " " " ~ ~ ~ ~ ~ ~]
; [" " " x x x X X X X $ $ $ X X X X X X x x x x x x x x " " ~ ~ ~ ~]
; [" " " " x x x X X X X X $ $ X X x X X x x x x x x x " " " " " ~ ~]
; [~ " " x x x x X $ X $ X $ $ X x X x x x x x x x x x x x x " " " ~]
)
; = Pictures! =
; ASCII is cool, but pictures are better. First we import some java libraries
; that we'll need, then define the colors for each level just like we did tiles
; for the ascii representation.
(import
'java.awt.image.BufferedImage
'javax.imageio.ImageIO
'java.io.File)
(def colors
[0x1437AD 0x04859D 0x007D1C 0x007D1C 0x24913C
0x00C12B 0x38E05D 0xA3A3A4 0x757575 0xFFFFFF])
; Finally we reduce over a BufferedImage instance to output every tile as a
; single pixel on it.
(defn img-terrain
"Given a terrain map and a file name, outputs a png representation of the
terrain map to that file"
[m file]
(let [img (BufferedImage. (count m) (count m) BufferedImage/TYPE_INT_RGB)]
(reduce
(fn [rown row]
(reduce
(fn [coln tile]
(.setRGB img coln rown (colors tile))
(inc coln))
0 row)
(inc rown))
0 (normalize-terrain m (dec (count colors))))
(ImageIO/write img "png" (File. file))))
(comment
(img-terrain
(terrain 10)
"resources/terrain.png")
; https://blog.mediocregopher.com/img/diamond-square/terrain.png
)
; == Conclusion ==
; There's still a lot of work to be done. The algorithm starts taking a
; non-trivial amount of time around the 10th degree, which is only a 1025x1025px
; image. I need to profile the code and find out where the bottlenecks are. It's
; possible re-organizing the code to use pmaps instead of reduces in some places
; could help.
```
[marco]: http://marcopolo.io/diamond-square/
[terrain]: /img/diamond-square/terrain.png
[diamondsquare]: http://www.gameprogrammer.com/fractal.html
[lein]: https://github.com/technomancy/leiningen
[repo]: https://github.com/mediocregopher/diamond-square

View File

@ -1,192 +0,0 @@
---
title: Erlang Pitfalls
description: >-
Common pitfalls that people may run into when designing and writing
large-scale erlang applications.
---
I've been involved with a large-ish scale erlang project at Grooveshark since
sometime around 2011. I started this project knowing absolutely nothing about
erlang, but now I feel I have accumulated enough knowlege over time that I could
conceivably give some back. Specifically, common pitfalls that people may run
into when designing and writing a large-scale erlang application. Some of these
may show up when searching for them, but some of them you may not even know you
need to search for.
## now() vs timestamp()
The cononical way of getting the current timestamp in erlang is to use
`erlang:now()`. This works great at small loads, but if you find your
application slowing down greatly at highly parallel loads and you're calling
`erlang:now()` a lot, it may be the culprit.
A property of this method you may not realize is that it is monotonically
increasing, meaning even if two processes call it at the *exact* same time they
will both receive different output. This is done through some locking on the
low-level, as well as a bit of math to balance out the time getting out of sync
in the scenario.
There are situations where fetching always unique timestamps is useful, such as
seeding RNGs and generating unique identifiers for things, but usually when
people fetch a timestamp they just want a timestamp. For these cases,
`os:timestamp()` can be used. It is not blocked by any locks, it simply returns
the time.
## The rpc module is slow
The built-in `rpc` module is slower than you'd think. This mostly stems from it
doing a lot of extra work for every `call` and `cast` that you do, ensuring that
certain conditions are accounted for. If, however, it's sufficient for the
calling side to know that a call timed-out on them and not worry about it any
further you may benefit from simply writing your own rpc module. Alternatively,
use [one which already exists](https://github.com/cloudant/rexi).
## Don't send anonymous functions between nodes
One of erlang's niceties is transparent message sending between two phsyical
erlang nodes. Once nodes are connected, a process on one can send any message to
a process on the other exactly as if they existed on the same node. This is fine
for many data-types, but for anonymous functions it should be avoided.
For example:
```erlang
RemotePid ! {fn, fun(I) -> I + 1 end}.
```
Would be better written as
```erlang
incr(I) ->
I + 1.
RemotePid ! {fn, ?MODULE, incr}.
```
and then using an `apply` on the RemotePid to actually execute the function.
This is because hot-swapping code messes with anonymous functions quite a bit.
Erlang isn't actually sending a function definition across the wire; it's simply
sending a reference to a function. If you've changed the code within the
anonymous function on a node, that reference changes. The sending node is
sending a reference to a function which may not exist anymore on the receiving
node, and you'll get a weird error which Google doesn't return many results for.
Alternatively, if you simply send atoms across the wire and use `apply` on the
other side, only atoms are sent and the two nodes involved can have totally
different ideas of what the function itself does without any problems.
## Hot-swapping code is a convenience, not a crutch
Hot swapping code is the bees-knees. It lets you not have to worry about
rolling-restarts for trivial code changes, and so adds stability to your
cluster. My warning is that you should not rely on it. If your cluster can't
survive a node being restarted for a code change, then it can't survive if that
node fails completely, or fails and comes back up. Design your system pretending
that hot-swapping does not exist, and only once you've done that allow yourself
to use it.
## GC sometimes needs a boost
Erlang garbage collection (GC) acts on a per-erlang-process basis, meaning that
each process decides on its own to garbage collect itself. This is nice because
it means stop-the-world isn't a problem, but it does have some interesting
effects.
We had a problem with our node memory graphs looking like an upwards facing
line, instead of a nice sinusoid relative to the number of connections during
the day. We couldn't find a memory leak *anywhere*, and so started profiling. We
found that the memory seemed to be comprised of mostly binary data in process
heaps. On a hunch my coworker Mike Cugini (who gets all the credit for this) ran
the following on a node:
```erlang
lists:foreach(erlang:garbage_collect/1, erlang:processes()).
```
and saw memory drop in a huge way. We made that code run every 10 minutes or so
and suddenly our memory problem went away.
The problem is that we had a lot of processes which individually didn't have
much heap data, but all-together were crushing the box. Each didn't think it had
enough to garbage collect very often, so memory just kept going up. Calling the
above forces all processes to garbage collect, and thus throw away all those
little binary bits they were hoarding.
## These aren't the solutions you are looking for
The `erl` process has tons of command-line options which allow you to tweak all
kinds of knobs. We've had tons of performance problems with our application, as
of yet not a single one has been solved with turning one of these knobs. They've
all been design issues or just run-of-the-mill bugs. I'm not saying the knobs
are *never* useful, but I haven't seen it yet.
## Erlang processes are great, except when they're not
The erlang model of allowing processes to manage global state works really well
in many cases. Possibly even most cases. There are, however, times when it
becomes a performance problem. This became apparent in the project I was working
on for Grooveshark, which was, at its heart, a pubsub server.
The architecture was very simple: each channel was managed by a process, client
connection processes subscribed to that channel and received publishes from it.
Easy right? The problem was that extremely high volume channels were simply not
able to keep up with the load. The channel process could do certain things very
fast, but there were some operations which simply took time and slowed
everything down. For example, channels could have arbitrary properties set on
them by their owners. Retrieving an arbitrary property from a channel was a
fairly fast operation: client `call`s the channel process, channel process
immediately responds with the property value. No blocking involved.
But as soon as there was any kind of call which required the channel process to
talk to yet *another* process (unfortunately necessary), things got hairy. On
high volume channels publishes/gets/set operations would get massively backed up
in the message queue while the process was blocked on another process. We tried
many things, but ultimately gave up on the process-per-channel approach.
We instead decided on keeping *all* channel state in a transactional database.
When client processes "called" operations on a channel, they really are just
acting on the database data inline, no message passing involved. This means that
read-only operations are super-fast because there is minimal blocking, and if
some random other process is being slow it only affects the one client making
the call which is causing it to be slow, and not holding up a whole host of
other clients.
## Mnesia might not be what you want
This one is probably a bit controversial, and definitely subject to use-cases.
Do your own testing and profiling, find out what's right for you.
Mnesia is erlang's solution for global state. It's an in-memory transactional
database which can scale to N nodes and persist to disk. It is hosted
directly in the erlang processes memory so you interact with it in erlang
directly in your code; no calling out to database drivers and such. Sounds great
right?
Unfortunately mnesia is not a very full-featured database. It is essentially a
key-value store which can hold arbitrary erlang data-types, albeit in a set
schema which you lay out for it during startup. This means that more complex
types like sorted sets and hash maps (although this was addressed with the
introduction of the map data-type in R17) are difficult to work with within
mnesia. Additionally, erlang's data model of immutability, while awesome
usually, can bite you here because it's difficult (impossible?) to pull out
chunks of data within a record without accessing the whole record.
For example, when retrieving the list of processes subscribed to a channel our
application doesn't simply pull the full list and iterate over it. This is too
slow, and in some cases the subscriber list was so large it wasn't actually
feasible. The channel process wasn't cleaning up its heap fast enough, so
multiple publishes would end up with multiple copies of the giant list in
memory. This became a problem. Instead we chain spawned processes, each of which
pull a set chunk of the subsciber list, and iterate over that. This is very
difficult to implement in mnesia without pulling the full subscriber list into
the process' memory at some point in the process.
It is, however, fairly trivial to implement in redis using sorted sets. For this
case, and many other cases after, the motto for performance improvements became
"stick it in redis". The application is at the point where *all* state which
isn't directly tied to a specific connection is kept in redis, encoded using
`term_to_binary`. The performance hit of going to an outside process for data
was actually much less than we'd originally thought, and ended up being a plus
since we had much more freedom to do interesting hacks to speedup up our
accesses.

View File

@ -1,165 +0,0 @@
---
title: Rabbit Hole
description: >-
Complex systems sometimes require complex debugging.
---
We've begun rolling out [SkyDNS][skydns] at my job, which has been pretty neat.
We're basing a couple future projects around being able to use it, and it's made
dynamic configuration and service discovery nice and easy.
This post chronicles catching a bug because of our switch to SkyDNS, and how we
discover its root cause. I like to call these kinds of bugs "rabbit holes"; they
look shallow at first, but anytime you make a little progress forward a little
more is always required, until you discover the ending somewhere totally
unrelated to the start.
## The Bug
We are seeing *tons* of these in the SkyDNS log:
```
[skydns] Feb 20 17:21:15.168 INFO | no nameservers defined or name too short, can not forward
```
I fire up tcpdump to see if I can see anything interesting, and sure enough run
across a bunch of these:
```
# tcpdump -vvv -s 0 -l -n port 53
tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
...
$fen_ip.50257 > $skydns_ip.domain: [udp sum ok] 16218+ A? unknown. (25)
$fen_ip.27372 > $skydns_ip.domain: [udp sum ok] 16218+ A? unknown. (25)
$fen_ip.35634 > $skydns_ip.domain: [udp sum ok] 59227+ A? unknown. (25)
$fen_ip.64363 > $skydns_ip.domain: [udp sum ok] 59227+ A? unknown. (25)
```
It appears that some of our front end nodes (FENs) are making tons of DNS
fequests trying to find the A record of `unknown`. Something on our FENs is
doing something insane and is breaking.
## The FENs
Hopping over to my favorite FEN we're able to see the packets in question
leaving on a tcpdump as well, but that's not helpful for finding the root cause.
We have lots of processes running on the FENs and any number of them could be
doing something crazy.
We fire up sysdig, which is similar to systemtap and strace in that it allows
you to hook into the kernel and view various kernel activites in real time, but
it's easier to use than both. The following command dumps all UDP packets being
sent and what process is sending them:
```
# sysdig fd.l4proto=udp
...
2528950 22:17:35.260606188 0 php-fpm (21477) < connect res=0 tuple=$fen_ip:61173->$skydns_ip:53
2528961 22:17:35.260611327 0 php-fpm (21477) > sendto fd=102(<4u>$fen_ip:61173->$skydns_ip:53) size=25 tuple=NULL
2528991 22:17:35.260631917 0 php-fpm (21477) < sendto res=25 data=.r...........unknown.....
2530470 22:17:35.261879032 0 php-fpm (21477) > ioctl fd=102(<4u>$fen_ip:61173->$skydns_ip:53) request=541B argument=7FFF82DC8728
2530472 22:17:35.261880574 0 php-fpm (21477) < ioctl res=0
2530474 22:17:35.261881226 0 php-fpm (21477) > recvfrom fd=102(<4u>$fen_ip:61173->$skydns_ip:53) size=1024
2530476 22:17:35.261883424 0 php-fpm (21477) < recvfrom res=25 data=.r...........unknown..... tuple=$skydns_ip:53->$fen_ip:61173
2530485 22:17:35.261888997 0 php-fpm (21477) > close fd=102(<4u>$fen_ip:61173->$skydns_ip:53)
2530488 22:17:35.261892626 0 php-fpm (21477) < close res=0
```
Aha! We can see php-fpm is requesting something over udp with the string
`unknown` in it. We've now narrowed down the guilty process, the rest should be
easy right?
## Which PHP?
Unfortunately we're a PHP shop; knowing that php-fpm is doing something on a FEN
narrows down the guilty codebase little. Taking the FEN out of our load-balancer
stops the requests for `unknown`, so we *can* say that it's some user-facing
code that is the culprit. Our setup on the FENs involves users hitting nginx
for static content and nginx proxying PHP requests back to php-fpm. Since all
our virtual domains are defined in nginx, we are able to do something horrible.
On the particular FEN we're on we make a guess about which virtual domain the
problem is likely coming from (our main app), and proxy all traffic from all
other domains to a different FEN. We still see requests for `unknown` leaving
the box, so we've narrowed the problem down a little more.
## The Despair
Nothing in our code is doing any direct DNS calls as far as we can find, and we
don't see any places PHP might be doing it for us. We have lots of PHP
extensions in place, all written in C and all black boxes; any of them could be
the culprit. Grepping through the likely candidates' source code for the string
`unknown` proves fruitless.
We try xdebug at this point. xdebug is a profiler for php which will create
cachegrind files for the running code. With cachegrind you can see every
function which was ever called, how long spent within each function, a full
call-graph, and lots more. Unfortunately xdebug outputs cachegrind files on a
per-php-fpm-process basis, and overwrites the previous file on each new request.
So xdebug is pretty much useless, since what is in the cachegrind file isn't
necessarily what spawned the DNS request.
## Gotcha (sorta)
We turn back to the tried and true method of dumping all the traffic using
tcpdump and perusing through that manually.
What we find is that nearly everytime there is a DNS request for `unknown`, if
we scroll up a bit there is (usually) a particular request to memcache. The
requested key is always in the style of `function-name:someid:otherstuff`. When
looking in the code around that function name we find this ominous looking call:
```php
$ipAddress = getIPAddress();
$geoipInfo = getCountryInfoFromIP($ipAddress);
```
This points us in the right direction. On a hunch we add some debug
logging to print out the `$ipAddress` variable, and sure enough it comes back as
`unknown`. AHA!
So what we surmise is happening is that for some reason our geoip extension,
which we use to get the location data of an IP address and which
`getCountryInfoFromIP` calls, is seeing something which is *not* an IP address
and trying to resolve it.
## Gotcha (for real)
So the question becomes: why are we getting the string `unknown` as an IP
address?
Adding some debug logging around the area we find before showed that
`$_SERVER['REMOTE_ADDR']`, which is the variable populated with the IP address
of the client, is sometimes `unknown`. We guess that this has something to do
with some magic we are doing on nginx's side to populate `REMOTE_ADDR` with the
real IP address of the client in the case of them going through a proxy.
Many proxies send along the header `X-Forwarded-For` to indicate the real IP of
the client they're proxying for, otherwise the server would only see the proxy's
IP. In our setup I decided that in those cases we should set the `REMOTE_ADDR`
to the real client IP so our application logic doesn't even have to worry about
it. There are a couple problems with this which render it a bad decision, one
being that if some misbahaving proxy was to, say, start sending
`X-Forwarded-For: unknown` then some written applications might mistake that to
mean the client's IP is `unknown`.
## The Fix
The fix here was two-fold:
1) We now always set `$_SERVER['REMOTE_ADDR']` to be the remote address of the
requests, regardless of if it's a proxy, and also send the application the
`X-Forwarded-For` header to do with as it pleases.
2) Inside our app we look at all the headers sent and do some processing to
decide what the actual client IP is. PHP can handle a lot more complex logic
than nginx can, so we can do things like check to make sure the IP is an IP, and
also that it's not some NAT'd internal ip, and so forth.
And that's it. From some weird log messages on our DNS servers to an nginx
mis-configuration on an almost unrelated set of servers, this is one of those
strange bugs that never has a nice solution and goes unsolved for a long time.
Spending the time to dive down the rabbit hole and find the answer is often
tedious, but also often very rewarding.
[skydns]: https://github.com/skynetservices/skydns

View File

@ -1,547 +0,0 @@
---
title: Go's http package by example
description: >-
The basics of using, testing, and composing apps built using go's net/http
package.
---
Go's [http](http://golang.org/pkg/net/http/) package has turned into one of my
favorite things about the Go programming language. Initially it appears to be
somewhat complex, but in reality it can be broken down into a couple of simple
components that are extremely flexible in how they can be used. This guide will
cover the basic ideas behind the http package, as well as examples in using,
testing, and composing apps built with it.
This guide assumes you have some basic knowledge of what an interface in Go is,
and some idea of how HTTP works and what it can do.
## Handler
The building block of the entire http package is the `http.Handler` interface,
which is defined as follows:
```go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
```
Once implemented the `http.Handler` can be passed to `http.ListenAndServe`,
which will call the `ServeHTTP` method on every incoming request.
`http.Request` contains all relevant information about an incoming http request
which is being served by your `http.Handler`.
The `http.ResponseWriter` is the interface through which you can respond to the
request. It implements the `io.Writer` interface, so you can use methods like
`fmt.Fprintf` to write a formatted string as the response body, or ones like
`io.Copy` to write out the contents of a file (or any other `io.Reader`). The
response code can be set before you begin writing data using the `WriteHeader`
method.
Here's an example of an extremely simple http server:
```go
package main
import (
"fmt"
"log"
"net/http"
)
type helloHandler struct{}
func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello, you've hit %s\n", r.URL.Path)
}
func main() {
err := http.ListenAndServe(":9999", helloHandler{})
log.Fatal(err)
}
```
`http.ListenAndServe` serves requests using the handler, listening on the given
address:port. It will block unless it encounters an error listening, in which
case we `log.Fatal`.
Here's an example of using this handler with curl:
```
~ $ curl localhost:9999/foo/bar
hello, you've hit /foo/bar
```
## HandlerFunc
Often defining a full type to implement the `http.Handler` interface is a bit
overkill, especially for extremely simple `ServeHTTP` functions like the one
above. The `http` package provides a helper function, `http.HandlerFunc`, which
wraps a function which has the signature
`func(w http.ResponseWriter, r *http.Request)`, returning an `http.Handler`
which will call it in all cases.
The following behaves exactly like the previous example, but uses
`http.HandlerFunc` instead of defining a new type.
```go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello, you've hit %s\n", r.URL.Path)
})
err := http.ListenAndServe(":9999", h)
log.Fatal(err)
}
```
## ServeMux
On their own, the previous examples don't seem all that useful. If we wanted to
have different behavior for different endpoints we would end up with having to
parse path strings as well as numerous `if` or `switch` statements. Luckily
we're provided with `http.ServeMux`, which does all of that for us. Here's an
example of it being used:
```go
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
h := http.NewServeMux()
h.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, you hit foo!")
})
h.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, you hit bar!")
})
h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
fmt.Fprintln(w, "You're lost, go home")
})
err := http.ListenAndServe(":9999", h)
log.Fatal(err)
}
```
The `http.ServeMux` is itself an `http.Handler`, so it can be passed into
`http.ListenAndServe`. When it receives a request it will check if the request's
path is prefixed by any of its known paths, choosing the longest prefix match it
can find. We use the `/` endpoint as a catch-all to catch any requests to
unknown endpoints. Here's some examples of it being used:
```
~ $ curl localhost:9999/foo
Hello, you hit foo!
~ $ curl localhost:9999/bar
Hello, you hit bar!
~ $ curl localhost:9999/baz
You're lost, go home
```
`http.ServeMux` has both `Handle` and `HandleFunc` methods. These do the same
thing, except that `Handle` takes in an `http.Handler` while `HandleFunc` merely
takes in a function, implicitly wrapping it just as `http.HandlerFunc` does.
### Other muxes
There are numerous replacements for `http.ServeMux` like
[gorilla/mux](http://www.gorillatoolkit.org/pkg/mux) which give you things like
automatically pulling variables out of paths, easily asserting what http methods
are allowed on an endpoint, and more. Most of these replacements will implement
`http.Handler` like `http.ServeMux` does, and accept `http.Handler`s as
arguments, and so are easy to use in conjunction with the rest of the things
I'm going to talk about in this post.
## Composability
When I say that the `http` package is composable I mean that it is very easy to
create re-usable pieces of code and glue them together into a new working
application. The `http.Handler` interface is the way all pieces communicate with
each other. Here's an example of where we use the same `http.Handler` to handle
multiple endpoints, each slightly differently:
```go
package main
import (
"fmt"
"log"
"net/http"
)
type numberDumper int
func (n numberDumper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Here's your number: %d\n", n)
}
func main() {
h := http.NewServeMux()
h.Handle("/one", numberDumper(1))
h.Handle("/two", numberDumper(2))
h.Handle("/three", numberDumper(3))
h.Handle("/four", numberDumper(4))
h.Handle("/five", numberDumper(5))
h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
fmt.Fprintln(w, "That's not a supported number!")
})
err := http.ListenAndServe(":9999", h)
log.Fatal(err)
}
```
`numberDumper` implements `http.Handler`, and can be passed into the
`http.ServeMux` multiple times to serve multiple endpoints. Here's it in action:
```
~ $ curl localhost:9999/one
Here's your number: 1
~ $ curl localhost:9999/five
Here's your number: 5
~ $ curl localhost:9999/bazillion
That's not a supported number!
```
## Testing
Testing http endpoints is extremely easy in Go, and doesn't even require you to
actually listen on any ports! The `httptest` package provides a few handy
utilities, including `NewRecorder` which implements `http.ResponseWriter` and
allows you to effectively make an http request by calling `ServeHTTP` directly.
Here's an example of a test for our previously implemented `numberDumper`,
commented with what exactly is happening:
```go
package main
import (
"fmt"
"net/http"
"net/http/httptest"
. "testing"
)
func TestNumberDumper(t *T) {
// We first create the http.Handler we wish to test
n := numberDumper(1)
// We create an http.Request object to test with. The http.Request is
// totally customizable in every way that a real-life http request is, so
// even the most intricate behavior can be tested
r, _ := http.NewRequest("GET", "/one", nil)
// httptest.Recorder implements the http.ResponseWriter interface, and as
// such can be passed into ServeHTTP to receive the response. It will act as
// if all data being given to it is being sent to a real client, when in
// reality it's being buffered for later observation
w := httptest.NewRecorder()
// Pass in our httptest.Recorder and http.Request to our numberDumper. At
// this point the numberDumper will act just as if it was responding to a
// real request
n.ServeHTTP(w, r)
// httptest.Recorder gives a number of fields and methods which can be used
// to observe the response made to our request. Here we check the response
// code
if w.Code != 200 {
t.Fatalf("wrong code returned: %d", w.Code)
}
// We can also get the full body out of the httptest.Recorder, and check
// that its contents are what we expect
body := w.Body.String()
if body != fmt.Sprintf("Here's your number: 1\n") {
t.Fatalf("wrong body returned: %s", body)
}
}
```
In this way it's easy to create tests for your individual components that you
are using to build your application, keeping the tests near to the functionality
they're testing.
Note: if you ever do need to spin up a test server in your tests, `httptest`
also provides a way to create a server listening on a random open port for use
in tests as well.
## Middleware
Serving endpoints is nice, but often there's functionality you need to run for
*every* request before the actual endpoint's handler is run. For example, access
logging. A middleware component is one which implements `http.Handler`, but will
actually pass the request off to another `http.Handler` after doing some set of
actions. The `http.ServeMux` we looked at earlier is actually an example of
middleware, since it passes the request off to another `http.Handler` for actual
processing. Here's an example of our previous example with some logging
middleware:
```go
package main
import (
"fmt"
"log"
"net/http"
)
type numberDumper int
func (n numberDumper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Here's your number: %d\n", n)
}
func logger(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s requested %s", r.RemoteAddr, r.URL)
h.ServeHTTP(w, r)
})
}
func main() {
h := http.NewServeMux()
h.Handle("/one", numberDumper(1))
h.Handle("/two", numberDumper(2))
h.Handle("/three", numberDumper(3))
h.Handle("/four", numberDumper(4))
h.Handle("/five", numberDumper(5))
h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
fmt.Fprintln(w, "That's not a supported number!")
})
hl := logger(h)
err := http.ListenAndServe(":9999", hl)
log.Fatal(err)
}
```
`logger` is a function which takes in an `http.Handler` called `h`, and returns
a new `http.Handler` which, when called, will log the request it was called with
and then pass off its arguments to `h`. To use it we pass in our
`http.ServeMux`, so all incoming requests will first be handled by the logging
middleware before being passed to the `http.ServeMux`.
Here's an example log entry which is output when the `/five` endpoint is hit:
```
2015/06/30 20:15:41 [::1]:34688 requested /five
```
## Middleware chaining
Being able to chain middleware together is an incredibly useful ability which we
get almost for free, as long as we use the signature
`func(http.Handler) http.Handler`. A middleware component returns the same type
which is passed into it, so simply passing the output of one middleware
component into the other is sufficient.
However, more complex behavior with middleware can be tricky. For instance, what
if you want a piece of middleware which takes in a parameter upon creation?
Here's an example of just that, with a piece of middleware which will set a
header and its value for all requests:
```go
package main
import (
"fmt"
"log"
"net/http"
)
type numberDumper int
func (n numberDumper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Here's your number: %d\n", n)
}
func logger(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s requested %s", r.RemoteAddr, r.URL)
h.ServeHTTP(w, r)
})
}
type headerSetter struct {
key, val string
handler http.Handler
}
func (hs headerSetter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(hs.key, hs.val)
hs.handler.ServeHTTP(w, r)
}
func newHeaderSetter(key, val string) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return headerSetter{key, val, h}
}
}
func main() {
h := http.NewServeMux()
h.Handle("/one", numberDumper(1))
h.Handle("/two", numberDumper(2))
h.Handle("/three", numberDumper(3))
h.Handle("/four", numberDumper(4))
h.Handle("/five", numberDumper(5))
h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
fmt.Fprintln(w, "That's not a supported number!")
})
hl := logger(h)
hhs := newHeaderSetter("X-FOO", "BAR")(hl)
err := http.ListenAndServe(":9999", hhs)
log.Fatal(err)
}
```
And here's the curl output:
```
~ $ curl -i localhost:9999/three
HTTP/1.1 200 OK
X-Foo: BAR
Date: Wed, 01 Jul 2015 00:39:48 GMT
Content-Length: 22
Content-Type: text/plain; charset=utf-8
Here's your number: 3
```
`newHeaderSetter` returns a function which accepts and returns an
`http.Handler`. Calling that returned function with an `http.Handler` then gets
you an `http.Handler` which will set the header given to `newHeaderSetter`
before continuing on to the given `http.Handler`.
This may seem like a strange way of organizing this; for this example the
signature for `newHeaderSetter` could very well have looked like this:
```
func newHeaderSetter(key, val string, h http.Handler) http.Handler
```
And that implementation would have worked fine. But it would have been more
difficult to compose going forward. In the next section I'll show what I mean.
## Composing middleware with alice
[Alice](https://github.com/justinas/alice) is a very simple and convenient
helper for working with middleware using the function signature we've been using
thusfar. Alice is used to create and use chains of middleware. Chains can even
be appended to each other, giving even further flexibility. Here's our previous
example with a couple more headers being set, but also using alice to manage the
added complexity.
```go
package main
import (
"fmt"
"log"
"net/http"
"github.com/justinas/alice"
)
type numberDumper int
func (n numberDumper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Here's your number: %d\n", n)
}
func logger(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s requested %s", r.RemoteAddr, r.URL)
h.ServeHTTP(w, r)
})
}
type headerSetter struct {
key, val string
handler http.Handler
}
func (hs headerSetter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(hs.key, hs.val)
hs.handler.ServeHTTP(w, r)
}
func newHeaderSetter(key, val string) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return headerSetter{key, val, h}
}
}
func main() {
h := http.NewServeMux()
h.Handle("/one", numberDumper(1))
h.Handle("/two", numberDumper(2))
h.Handle("/three", numberDumper(3))
h.Handle("/four", numberDumper(4))
fiveHS := newHeaderSetter("X-FIVE", "the best number")
h.Handle("/five", fiveHS(numberDumper(5)))
h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
fmt.Fprintln(w, "That's not a supported number!")
})
chain := alice.New(
newHeaderSetter("X-FOO", "BAR"),
newHeaderSetter("X-BAZ", "BUZ"),
logger,
).Then(h)
err := http.ListenAndServe(":9999", chain)
log.Fatal(err)
}
```
In this example all requests will have the headers `X-FOO` and `X-BAZ` set, but
the `/five` endpoint will *also* have the `X-FIVE` header set.
## Fin
Starting with a simple idea of an interface, the `http` package allows us to
create for ourselves an incredibly useful and flexible (yet still rather simple)
ecosystem for building web apps with re-usable components, all without breaking
our static checks.

View File

@ -1,235 +0,0 @@
---
title: Happy Trees
description: >-
Visualizing a forest of happy trees.
---
Source code related to this post is available [here](https://github.com/mediocregopher/happy-tree).
This project was inspired by [this video](https://www.youtube.com/watch?v=_DpzAvb3Vk4),
which you should watch first in order to really understand what's going on.
My inspiration came from his noting that happification could be done on numbers
in bases other than 10. I immediately thought of hexadecimal, base-16, since I'm
a programmer and that's what I think of. I also was trying to think of how one
would graphically represent a large happification tree, when I realized that
hexadecimal numbers are colors, and colors graphically represent things nicely!
## Colors
Colors to computers are represented using 3-bytes, encompassing red, green, and
blue. Each byte is represented by two hexadecimal digits, and they are appended
together. For example `FF0000` represents maximum red (`FF`) added to no green
and no blue. `FF5500` represents maximum red (`FF`), some green (`55`) and no
blue (`00`), which when added together results in kind of an orange color.
## Happifying colors
In base 10, happifying a number is done by splitting its digits, squaring each
one individually, and adding the resulting numbers. The principal works the same
for hexadecimal numbers:
```
A4F
A*A + 4*4 + F*F
64 + 10 + E1
155 // 341 in decimal
```
So if all colors are 6-digit hexadecimal numbers, they can be happified easily!
```
FF5500
F*F + F*F + 5*5 + 5*5 + 0*0 + 0*0
E1 + E1 + 19 + 19 + 0 + 0
0001F4
```
So `FF5500` (an orangish color) happifies to `0001F4` (a darker blue). Since
order of digits doesn't matter, `5F50F0` also happifies to `0001F4`. From this
fact, we can make a tree (hence the happification tree). I can do this process
on every color from `000000` (black) to `FFFFFF` (white), so I will!
## Representing the tree
So I know I can represent the tree using color, but there's more to decide on
than that. The easy way to represent a tree would be to simply draw a literal
tree graph, with a circle for each color and lines pointing to its parent and
children. But this is boring, and also if I want to represent *all* colors the
resulting image would be enormous and/or unreadable.
I decided on using a hollow, multi-level pie-chart. Using the example
of `000002`, it would look something like this:
![An example of a partial multi-level pie chart](/img/happy-tree/partial.png)
The inner arc represents the color `000002`. The second arc represents the 15
different colors which happify into `000002`, each of them may also have their
own outer arc of numbers which happify to them, and so on.
This representation is nice because a) It looks cool and b) it allows the
melancoils of the hexadecimals to be placed around the happification tree
(numbers which happify into `000001`), which is convenient. It's also somewhat
easier to code than a circle/branch based tree diagram.
An important feature I had to implement was proportional slice sizes. If I were
to give each child of a color an equal size on that arc's edge the image would
simply not work. Some branches of the tree are extremely deep, while others are
very shallow. If all were given the same space, those deep branches wouldn't
even be representable by a single pixel's width, and would simply fail to show
up. So I implemented proportional slice sizes, where the size of every slice is
determined to be proportional to how many total (recursively) children it has.
You can see this in the above example, where the second level arc is largely
comprised of one giant slice, with many smaller slices taking up the end.
## First attempt
My first attempt resulted in this image (click for 5000x5000 version):
[![Result of first attempt](/img/happy-tree/happy-tree-atmp1-small.png)](/img/happy-tree/happy-tree-atmp1.png)
The first thing you'll notice is that it looks pretty neat.
The second thing you'll notice is that there's actually only one melancoil in
the 6-digit hexadecimal number set. The innermost black circle is `000000` which
only happifies to itself, and nothing else will happify to it (sad `000000`).
The second circle represents `000001`, and all of its runty children. And
finally the melancoil, comprised of:
```
00000D -> 0000A9 -> 0000B5 -> 000092 -> 000055 -> 00003 -> ...
```
The final thing you'll notice (or maybe it was the first, since it's really
obvious) is that it's very blue. Non-blue colors are really only represented as
leaves on their trees and don't ever really have any children of their own, so
the blue and black sections take up vastly more space.
This makes sense. The number which should generate the largest happification
result, `FFFFFF`, only results in `000546`, which is primarily blue. So in effect
all colors happify to some shade of blue.
This might have been it, technically this is the happification tree and the
melancoil of 6 digit hexadecimal numbers represented as colors. But it's also
boring, and I wanted to do better.
## Second attempt
The root of the problem is that the definition of "happification" I used
resulted in not diverse enough results. I wanted something which would give me
numbers where any of the digits could be anything. Something more random.
I considered using a hash instead, like md5, but that has its own problems.
There's no gaurantee that any number would actually reach `000001`, which isn't
required but it's a nice feature that I wanted. It also would be unlikely that
there would be any melancoils that weren't absolutely gigantic.
I ended up redefining what it meant to happify a hexadecimal number. Instead of
adding all the digits up, I first split up the red, green, and blue digits into
their own numbers, happified those numbers, and finally reassembled the results
back into a single number. For example:
```
FF5500
FF, 55, 00
F*F + F*F, 5*5 + 5*5, 0*0 + 0*0
1C2, 32, 00
C23200
```
I drop that 1 on the `1C2`, because it has no place in this system. Sorry 1.
Simply replacing that function resulted in this image (click for 5000x5000) version:
[![Result of second attempt](/img/happy-tree/happy-tree-atmp2-small.png)](/img/happy-tree/happy-tree-atmp2.png)
The first thing you notice is that it's so colorful! So that goal was achieved.
The second thing you notice is that there's *significantly* more melancoils.
Hundreds, even. Here's a couple of the melancoils (each on its own line):
```
00000D -> 0000A9 -> 0000B5 -> 000092 -> 000055 -> 000032 -> ...
000D0D -> 00A9A9 -> 00B5B5 -> 009292 -> 005555 -> 003232 -> ...
0D0D0D -> A9A9A9 -> B5B5B5 -> 929292 -> 555555 -> 323232 -> ...
0D0D32 -> A9A90D -> B5B5A9 -> 9292B5 -> 555592 -> 323255 -> ...
...
```
And so on. You'll notice the first melancoil listed is the same as the one from
the first attempt. You'll also notice that the same numbers from the that
melancoil are "re-used" in the rest of them as well. The second coil listed is
the same as the first, just with the numbers repeated in the 3rd and 4th digits.
The third coil has those numbers repeated once more in the 1st and 2nd digits.
The final coil is the same numbers, but with the 5th and 6th digits offset one
place in the rotation.
The rest of the melancoils in this attempt work out to just be every conceivable
iteration of the above. This is simply a property of the algorithm chosen, and
there's not a whole lot we can do about it.
## Third attempt
After talking with [Mr. Marco](/members/#marcopolo) about the previous attempts
I got an idea that would lead me towards more attempts. The main issue I was
having in coming up with new happification algorithms was figuring out what to
do about getting a number greater than `FFFFFF`. Dropping the leading digits
just seemed.... lame.
One solution I came up with was to simply happify again. And again, and again.
Until I got a number less than or equal to `FFFFFF`.
With this new plan, I could increase the power by which I'm raising each
individual digit, and drop the strategy from the second attempt of splitting the
number into three parts. In the first attempt I was doing happification to the
power of 2, but what if I wanted to happify to the power of 6? It would look
something like this (starting with the number `34BEEF`):
```
34BEEF
3^6 + 4^6 + B^6 + E^6 + E^6 + E^6 + F^6
2D9 + 1000 + 1B0829 + 72E440 + 72E440 + ADCEA1
1AEB223
1AEB223 is greater than FFFFFF, so we happify again
1^6 + A^6 + E^6 + B^6 + 2^6 + 2^6 + 3^6
1 + F4240 + 72E440 + 1B0829 + 40 + 40 + 2D9
9D3203
```
So `34BEEF` happifies to `9D3203`, when happifying to the power of 6.
As mentioned before the first attempt in this blog was the 2nd power tree,
here's the trees for the 3rd, 4th, 5th, and 6th powers (each image is a link to
a larger version):
3rd power:
[![Third attempt, 3rd power](/img/happy-tree/happy-tree-atmp3-pow3-small.png)](/img/happy-tree/happy-tree-atmp3-pow3.png)
4th power:
[![Third attempt, 4th power](/img/happy-tree/happy-tree-atmp3-pow4-small.png)](/img/happy-tree/happy-tree-atmp3-pow4.png)
5th power:
[![Third attempt, 5th power](/img/happy-tree/happy-tree-atmp3-pow5-small.png)](/img/happy-tree/happy-tree-atmp3-pow5.png)
6th power:
[![Third attempt, 6th power](/img/happy-tree/happy-tree-atmp3-pow6-small.png)](/img/happy-tree/happy-tree-atmp3-pow6.png)
A couple things to note:
* 3-5 are still very blue. It's not till the 6th power that the distribution
becomes random enough to become very colorful.
* Some powers have more coils than others. Power of 3 has a lot, and actually a
lot of them aren't coils, but single narcissistic numbers. Narcissistic
numbers are those which happify to themselves. `000000` and `000001` are
narcissistic numbers in all powers, power of 3 has quite a few more.
* 4 looks super cool.
Using unsigned 64-bit integers I could theoretically go up to the power of 15.
But I hit a roadblock at power of 7, in that there's actually a melancoil which
occurs whose members are all greater than `FFFFFF`. This means that my strategy
of repeating happifying until I get under `FFFFFF` doesn't work for any numbers
which lead into that coil.

View File

@ -1,105 +0,0 @@
---
title: Brian Bars
description: >-
Cheap and easy to make, healthy, vegan, high-carb, high-protein. "The Good
Stuff".
updated: 2018-01-18
---
It actually blows my mind it's been 4 years since I used this blog. It was
previously a tech blog, but then I started putting all my tech-related posts on
[the cryptic blog](https://cryptic.io). As of now this is a lifestyle/travel
blog. The me of 4 years ago would be horrified.
Now I just have to come up with a lifestyle and do some traveling.
## Recipe
This isn't a real recipe because I'm not going to preface it with my entire
fucking life story. Let's talk about the food.
Brian bars:
* Are like Clif Bars, but with the simplicity of ingredients that Larabars have.
* Are easy to make, only needing a food processor (I use a magic bullet) and a
stovetop oven.
* Keep for a long time and don't really need refrigerating (but don't mind it
neither)
* Are paleo, vegan, gluten-free, free-range, grass-fed, whatever...
* Are really really filling.
* Are named after me, deal with it.
I've worked on this recipe for a bit, trying to make it workable, and will
probably keep adjusting it (and this post) as time goes on.
### Ingredients
Nuts and seeds. Most of this recipe is nuts and seeds. Here's the ones I used:
* 1 cup almonds
* 1 cup peanuts
* 1 cup walnuts
* 1 cup coconut flakes/shavings/whatever
* 1/2 cup flax seeds
* 1/2 cup sesame seeds
For all of those above it doesn't _really_ matter what nuts/seeds you use, it's
all gonna get ground up anyway. So whatever's cheap works fine. Also, avoid
salt-added ones if you can.
The other ingredients are:
* 1 cup raisins/currants
* 1.5 lbs of pitted dates (no added sugar! you don't need it!)
* 2 cups oats
### Grind up the nuts
Throw the nuts into the food processor and grind them into a powder. Then throw
that powder into a bowl along with the seeds, coconuts, raisins, and oats, and
mix em good.
I don't _completely_ grind up the nuts, instead leaving some chunks in it here
and there, but you do you.
### Prepare the dates
This is the harder part, and is what took me a couple tries to get right. The
best strategy I've found is to steam the dates a bit over a stove to soften
them. Then, about a cup at a time, you can throw them in the food processor and
turn them into a paste. You may have to add a little water if your processor is
having trouble.
Once processed you can add the dates to the mix from before and stir it all up.
It'll end up looking something like cookie dough. Except unlike cookie dough
it's completely safe to eat and maybe sorta healthy.
### Bake it, Finish it
Put the dough stuff in a pan of some sort, flatten it out, and stick it in the
oven at like 250 or 300 for a few hours. You're trying to cook out the water you
added earlier when you steamed the dates, as well as whatever little moisture
the dates had in the first place.
Once thoroughly baked you can stick the pan in the fridge to cool and keep,
and/or cut it up into individual bars. Keep in mind that the bars are super
filling and allow for pretty small portions. Wrap em in foil or plastic wrap and
take them to-go, or keep them around for a snack. Or both. Or whatever you want
to do, it's your food.
### Cleanup
Dates are simultaneously magical and the most annoying thing to work with, so
there's cleanup problems you may run into with them:
Protip #1: When cleaning your processed date slime off of your cooking utensils
I'd recommend just letting them soak in water for a while. Dry-ish date slime
will stick to everything, while soaked date slime will come right off.
Protip #2: Apparently if you want ants, dates are a great way to get ants. My
apartment has never had an ant problem until 3 hours after I made a batch of
these and didn't wipe down my counter enough. I'm still dealing with the ants.
Apparently there's enviromentally friendly ant poisons where the ants happily
carry the poison back into the nest and the whole nest eats it and dies. Which
feels kinda mean in some way, but is also pretty clever and they're just ants
anyway so fuck it.

View File

@ -1,292 +0,0 @@
---
title: Rethinking Identity
description: >-
A more useful way of thinking about identity on the internet, and using that
to build a service which makes our online life better.
---
In my view, the major social media platforms (Facebook, Twitter, Instagram,
etc...) are broken. They worked well at small scales, but billions of people are
now exposed to them, and [Murphy's Law][murphy] has come into effect. The weak
points in the platforms have been found and exploited, to the point where
they're barely usable for interacting with anyone you don't already know in
person.
[murphy]: https://en.wikipedia.org/wiki/Murphy%27s_law
On the other hand, social media, at its core, is a powerful tool that humans
have developed, and it's not one to be thrown away lightly (if it can be thrown
away at all). It's worthwhile to try and fix it. So that's what this post is
about.
A lot of moaning and groaning has already been done on how social media is toxic
for the average person. But the average person isn't doing anything more than
receiving and reacting to their environment. If that environment is toxic, the
person in it becomes so as well. It's certainly possible to filter the toxicity
out, and use a platform to your own benefit, but that takes work on the user's
part. It would be nice to think that people will do more than follow the path of
least resistance, but at scale that's simply not how reality is, and people
shouldn't be expected to do that work.
To identify what has become toxic about the platforms, first we need to identify
what a non-toxic platform would look like.
The ideal definition for social media is to give people a place to socialize
with friends, family, and the rest of the world. Defining "socialize" is tricky,
and probably an exercise only a socially awkward person who doesn't do enough
socializing would undertake. "Expressing one's feelings, knowledge, and
experiences to other people, and receiving theirs in turn" feels like a good
approximation. A platform where true socializing was the only activity would be
ideal.
Here are some trends on our social media which have nothing to do with
socializing: artificially boosted follower numbers on Instagram to obtain
product sponsors, shills in Reddit comments boosting a product or company,
russian trolls on Twitter spreading propaganda, trolls everywhere being dicks
and switching IPs when they get banned, and [that basketball president whose
wife used burner Twitter accounts to trash talk players][president].
[president]: https://www.nytimes.com/2018/06/07/sports/bryan-colangelo-sixers-wife.html
These are all examples of how anonymity can be abused on social media. I want
to say up front that I'm _not_ against anonymity on the internet, and that I
think we can have our cake and eat it too. But we _should_ acknowledge the
direct and indirect problems anonymity causes. We can't trust that anyone on
social media is being honest about who they are and what their motivation is.
This problem extends outside of social media too, to Amazon product reviews (and
basically any other review system), online polls and raffles, multiplayer games,
and surely many other other cases.
## Identity
To fix social media, and other large swaths of the internet, we need to rethink
identity. This process started for me a long time ago, when I watched [this TED
talk][identity], which discusses ways in which we misunderstand identity.
Crucially, David Birch points out that identity is not a name, it's more
fundamental than that.
[identity]: https://www.ted.com/talks/david_birch_identity_without_a_name
In the context of online platforms, where a user creates an account which
identifies them in some way, identity breaks down into 3 distinct problems
which are often conflated:
* Authentication: Is this identity owned by this person?
* Differentiation: Is this identity unique to this person?
* Authorization: Is this identity allowed to do X?
For internet platform developers, authentication has been given the full focus.
Blog posts, articles, guides, and services abound which deal with properly
hashing and checking passwords, two factor authentication, proper account
recovery procedure, etc... While authentication is not a 100% solved problem,
it's had the most work done on it, and the problems which this post deals with
are not affected by it.
The problem which should instead be focused on is differentiation.
## Differentiation
I want to make very clear, once more, that I am _not_ in favor of de-anonymizing
the web, and doing so is not what I'm proposing.
Differentiation is without a doubt the most difficult identity problem to solve.
It's not even clear that it's solvable offline. Take this situation: you are in
a room, and you are told that one person is going to walk in, then leave, then
another person will do the same. These two persons may or may not be the same
person. You're allowed to do anything you like to each person (with their
consent) in order to determine if they are the same person or not.
For the vast, vast majority of cases you can simply look with your eyeballs and
see if they are different people. But this will not work 100% of the time.
Identical twins are an obvious example of two persons looking like one, but a
malicious actor with a disguise might be one person posing as two. Biometrics
like fingerprints, iris scanning, and DNA testing fail for many reasons (the
identical twin case being one). You could attempt to give the first a unique
marking on their skin, but who's to say they don't have a solvent, which can
clean that marking off, waiting right outside the door?
The solutions and refutations can continue on pedantically for some time, but
the point is that there is likely not a 100% solution, and even the 90%
solutions require significant investment. Differentiation is a hard problem,
which most developers don't want to solve. Most are fine with surrogates like
checking that an email or phone number is unique to the platform, but these
aren't enough to stop a dedicated individual or organization.
### Roll Your Own Differentiation
If a platform wants to roll their own solution to the differentiation problem, a
proper solution, it might look something like this:
* Submit an image of your passport, or other government issued ID. This would
have to be checked against the appropriate government agency to ensure the
ID is legitimate.
* Submit an image of your face, alongside a written note containing a code given
by the platform. Software to detect manipulated images would need to be
employed, as well as reverse image searching to ensure the image isn't being
reused.
* Once completed, all data needs to be hashed/fingerprinted and then destroyed,
so sensitive data isn't sitting around on servers, but can still be checked
against future users signing up for the platform.
* A dedicated support team would be needed to handle edge-cases and mistakes.
None of these is trivial, nor would I trust an up-and-coming platform which is
being bootstrapped out of a basement to implement any of them correctly.
Additionally, going through with this process would be a _giant_ point of
friction for a user creating a new account; they likely would go use a different
platform instead, which didn't have all this nonsense required.
### Differentiation as a Service
This is the crux of this post.
Instead of each platform rolling their own differentiation, what if there was a
service for it. Users would still have to go through the hassle described above,
but only once forever, and on a more trustable site. Then platforms, no matter
what stage of development they're at, could use that service to ensure that
their community of users is free from the problems of fake accounts and trolls.
This is what the service would look like:
* A user would have to, at some point, have gone through the steps above to
create an account on the differentiation-as-a-service (DaaS) platform. This
account would have the normal authentication mechanisms that most platforms
do (password, two-factor, etc...).
* When creating an account on a new platform, the user would login to their DaaS
account (similar to the common "login with Google/Facebook/Twitter" buttons).
* The DaaS then returns an opaque token, an effectively random string which
uniquely identifies that user, to the platform. The platform can then check in
its own user database for any other users using that token, and know if the
user already has an account. All of this happens without any identifying
information being passed to the platform.
Similar to how many sites outsource to Cloudflare to handle DDoS protection,
which is better handled en masse by people familiar with the problem, the DaaS
allows for outsourcing the problem of differentiation. Users are more likely to
trust an established DaaS service than a random website they're signing up for.
And signing up for a DaaS is a one-time event, so if enough platforms are using
the DaaS it could become worthwhile for them to do so.
Finally, since the DaaS also handles authentication, a platform could outsource
that aspect of identity management to it as well. This is optional for the
platform, but for smaller platforms which are just starting up it might be
worthwhile to save that development time.
### Traits of a Successful DaaS
It's possible for me to imagine a world where use of DaaS' is common, but
bridging the gap between that world and this one is not as obvious. Still, I
think it's necessary if the internet is to ever evolve passed being, primarily,
a home for trolls. There are a number of traits of an up-and-coming DaaS which
would aid it in being accepted by the internet:
* **Patience**: there is a critical mass of users and platforms using DaaS'
where it becomes more advantageous for platforms to use the DaaS than not.
Until then, the DaaS and platforms using it need to take deliberate but small
steps. For example: making DaaS usage optional for platform users, and giving
their accounts special marks to indicate they're "authentic" (like Twitter's
blue checkmark); giving those users' activity higher weight in algorithms;
allowing others to filter out activity of non-"authentic" users; etc... These
are all preliminary steps which can be taken which encourage but don't require
platform users to use a DaaS.
* **User-friendly**: most likely the platforms using a DaaS are what are going
to be paying the bills. A successful DaaS will need to remember that, no
matter where the money comes from, if the users aren't happy they'll stop
using the DaaS, and platforms will be forced to switch to a different one or
stop using them altogether. User-friendliness means more than a nice
interface; it means actually caring for the users' interests, taking their
privacy and security seriously, and in all other aspects being on their side.
In that same vein, competition is important, and so...
* **No country/government affiliation**: If the DaaS was to be run by a
government agency it would have no incentive to provide a good user
experience, since the users aren't paying the bills (they might not even be in
that country). A DaaS shouldn't be exclusive to any one government or country
anyway. Perhaps it starts out that way, to get off the ground, but ultimately
the internet is a global institution, and is healthiest when it's connecting
individuals _around the world_. A successful DaaS will reach beyond borders
and try to connect everyone.
Obviously actually starting a DaaS would be a huge undertaking, and would
require proper management and good developers and all that, but such things
apply to most services.
## Authorization
The final aspect of identity management, which I haven't talked about yet, is
authorization. This aspect deals with what a particular identity is allowed to
do. For example, is an identity allowed to claim they have a particular name, or
are from a particular place, or are of a particular age? Other things like
administration and moderation privileges also fall under authorization, but they
are generally defined and managed within a platform.
A DaaS has the potential to help with authorization as well, though with a giant
caveat. If a DaaS were to not fingerprint and destroy the user's data, like
their name and birthday and whatnot, but instead store them, then the following
use-case could also be implemented:
* A platform wants to know if a user is above a certain age, let's say. It asks
the DaaS for that information.
* The DaaS asks the user, OAuth style, whether the user is ok with giving the
platform that information.
* If so, the platform is given that information.
This is a tricky situation. It adds a lot of liablity for the user, since their
raw data will be stored with the DaaS, ripe for hacking. It also places a lot of
trust with the DaaS to be responsible with users' data and not go giving it out
willy-nilly to others, and instead to only give out the bare-minimum that the
user allows. Since the user is not the DaaS' direct customer, this might be too
much to ask. Nevertheless, it's a use-case which is worth thinking about.
## Dapps
The idea of decentralized applications, or dapps, has begun to gain traction.
While not mainstream yet, I think they have potential, and it's necessary to
discuss how a DaaS would operate in a world where the internet is no longer
hosted in central datacenters.
Consider an Ethereum-based dapp. If a user were to register one ethereum address
(which are really public keys) with their DaaS account, the following use-case
could be implemented:
* A charity dapp has an ethereum contract, which receives a call from an
ethereum address asking for money. The dapp wants to ensure every person it
sends money to hasn't received any that day.
* The DaaS has a separate ethereum contract it manages, where it stores all
addresses which have been registered to a user. There is no need to keep any
other user information in the contract.
* The charity dapp's contract calls the DaaS' contract, asking it if the address
is one of its addresses. If so, and if the charity contract hasn't given to
that address yet today, it can send money to that address.
There would perhaps need to be some mechanism by which a user could change their
address, which would be complex since that address might be in use by a dapp
already, but it's likely a solvable problem.
A charity dapp is a bit of a silly example; ideally with a charity dapp there'd
also be some mechanism to ensure a person actually _needs_ the money. But
there's other dapp ideas which would become feasible, due to the inability of a
person to impersonate many people, if DaaS use becomes normal.
## Why Did I Write This?
Perhaps you've gotten this far and are asking: "Clearly you've thought about
this a lot, why don't you make this yourself and make some phat stacks of cash
with a startup?" The answer is that this project would need to be started and
run by serious people, who can be dedicated and thorough and responsible. I'm
not sure I'm one of those people; I get distracted easily. But I would like to
see this idea tried, and so I've written this up thinking maybe someone else
would take the reins.
I'm not asking for equity or anything, if you want to try; it's a free idea for
the taking. But if it turns out to be a bazillion dollar Good Idea™, I won't say
no to a donation...

View File

@ -1,54 +0,0 @@
---
title: >-
Visualization 1
description: >-
Using clojurescript and quil to generate interesting visuals
series: viz
git_repo: https://github.com/mediocregopher/viz.git
git_commit: v1
---
First I want to appologize if you've seen this already, I originally had this up
on my normal website, but I've decided to instead consolidate all my work to my
blog.
This is the first of a series of visualization posts I intend to work on, each
building from the previous one.
<script src="/assets/viz/1/goog/base.js"></script>
<script src="/assets/viz/1/cljs_deps.js"></script>
<script>goog.require("viz.core");</script>
<p align="center"><canvas id="viz"></canvas></p>
This visualization follows a few simple rules:
* Any point can only be occupied by a single node. A point may be alive (filled)
or dead (empty).
* On every tick each live point picks from 0 to N new points to spawn, where N is
the number of empty adjacent points to it. If it picks 0, it becomes dead.
* Each line indicates the parent of a point. Lines have an arbitrary lifetime of
a few ticks, and occupy the points they connect (so new points may not spawn
on top of a line).
* When a dead point has no lines it is cleaned up, and its point is no longer
occupied.
The resulting behavior is somewhere between [Conway's Game of
Life](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life) and white noise.
Though each point operates independently, they tend to move together in groups.
When two groups collide head on they tend to cancel each other out, killing most
of both. When they meet while both heading in a common direction they tend to
peacefully merge towards that direction.
Sometimes their world becomes so cluttered there's hardly room to move.
Sometimes a major coincidence of events leads to multiple groups canceling each
other at once, opening up the world and allowing for an explosion of new growth.
Some groups spiral about a single point, sustaining themselves and defending
from outside groups in the same movement. This doesn't last for very long.
The performance of this visualization is not very optimized, and will probably
eat up your CPU like nothing else. Most of the slowness comes from drawing the
lines; since there's so many individual small ones it's quite cumbersome to do.

View File

@ -1,49 +0,0 @@
---
title: >-
Visualization 2
description: >-
Now in glorious technicolor!
series: viz
git_repo: https://github.com/mediocregopher/viz.git
git_commit: v2
---
<script src="/assets/viz/2/goog/base.js"></script>
<script src="/assets/viz/2/cljs_deps.js"></script>
<script>goog.require("viz.core");</script>
<p align="center"><canvas id="viz"></canvas></p>
This visualization builds on the previous. Structurally the cartesian grid has
been turned into an isometric one, but this is more of an environmental change
than a behavioral one.
Behavioral changes which were made:
* When a live point is deciding its next spawn points, it first sorts the set of
empty adjacent points from closest-to-the-center to farthest. It then chooses
a number `n` between `0` to `N` (where `N` is the sorted set's size) and
spawns new points from the first `n` points of the sorted set. `n` is chosen
based on:
* The live point's linear distance from the center.
* A random multiplier.
* Each point is spawned with an attached color, where the color chosen is a
slightly different hue than its parent. The change is deterministic, so all
child points of the same generation have the same color.
The second change is purely cosmetic, but does create a mesmerizing effect. The
first change alters the behavior dramatically. Only the points which compete for
the center are able to reproduce, but by the same token are more likely to be
starved out by other points doing the same.
In the previous visualization the points moved around in groups aimlessly. Now
the groups are all competing for the same thing, the center. As a result they
congregate and are able to be viewed as a larger whole.
The constant churn of the whole takes many forms, from a spiral in the center,
to waves crashing against each other, to outright chaos, to random purges of
nearly all points. Each form lasts for only a few seconds before giving way to
another.

View File

@ -1,587 +0,0 @@
---
title: >-
Program Structure and Composability
description: >-
Discussing the nature of program structure, the problems presented by
complex structures, and a pattern that helps in solving those problems.
---
## Part 0: Introduction
This post is focused on a concept I call “program structure,” which I will try
to shed some light on before discussing complex program structures. I will then
discuss why complex structures can be problematic to deal with, and will finally
discuss a pattern for dealing with those problems.
My background is as a backend engineer working on large projects that have had
many moving parts; most had multiple programs interacting with each other, used
many different databases in various contexts, and faced large amounts of load
from millions of users. Most of this post will be framed from my perspective,
and will present problems in the way I have experienced them. I believe,
however, that the concepts and problems I discuss here are applicable to many
other domains, and I hope those with a foot in both backend systems and a second
domain can help to translate the ideas between the two.
Also note that I will be using Go as my example language, but none of the
concepts discussed here are specific to Go. To that end, Ive decided to favor
readable code over “correct” code, and so have elided things that most gophers
hold near-and-dear, such as error checking and proper documentation, in order to
make the code as accessible as possible to non-gophers as well. As with before,
I trust that someone with a foot in Go and another language can help me
translate between the two.
## Part 1: Program Structure
In this section I will discuss the difference between directory and program
structure, show how global state is antithetical to compartmentalization (and
therefore good program structure), and finally discuss a more effective way to
think about program structure.
### Directory Structure
For a long time, I thought about program structure in terms of the hierarchy
present in the filesystem. In my mind, a programs structure looked like this:
```
// The directory structure of a project called gobdns.
src/
config/
dns/
http/
ips/
persist/
repl/
snapshot/
main.go
```
What I grew to learn was that this conflation of “program structure” with
“directory structure” is ultimately unhelpful. While it cant be denied that
every program has a directory structure (and if not, it ought to), this does not
mean that the way the program looks in a filesystem in any way corresponds to
how it looks in our minds eye.
The most notable way to show this is to consider a library package. Here is the
structure of a simple web-app which uses redis (my favorite database) as a
backend:
```
src/
redis/
http/
main.go
```
If I were to ask you, based on that directory structure, what the program does
in the most abstract terms, you might say something like: “The program
establishes an http server that listens for requests. It also establishes a
connection to the redis server. The program then interacts with redis in
different ways based on the http requests that are received on the server.”
And that would be a good guess. Heres a diagram that depicts the program
structure, wherein the root node, `main.go`, takes in requests from `http` and
processes them using `redis`.
{% include image.html
dir="program-structure" file="diag1.jpg" width=519
descr="Example 1"
%}
This is certainly a viable guess for how a program with that directory
structure operates, but consider another answer: “A component of the program
called `server` establishes an http server that listens for requests. `server`
also establishes a connection to a redis server. `server` then interacts with
that redis connection in different ways based on the http requests that are
received on the http server. Additionally, `server` tracks statistics about
these interactions and makes them available to other components. The root
component of the program establishes a connection to a second redis server, and
stores those statistics in that redis server.” Heres another diagram to depict
_that_ program.
{% include image.html
dir="program-structure" file="diag2.jpg" width=712
descr="Example 2"
%}
The directory structure could apply to either description; `redis` is just a
library which allows for interaction with a redis server, but it doesnt
specify _which_ or _how many_ servers. However, those are extremely important
factors that are definitely reflected in our concept of the programs
structure, and not in the directory structure. **What the directory structure
reflects are the different _kinds_ of components available to use, but it does
not reflect how a program will use those components.**
### Global State vs Compartmentalization
The directory-centric view of structure often leads to the use of global
singletons to manage access to external resources like RPC servers and
databases. In examples 1 and 2 the `redis` library might contain code which
looks something like this:
```go
// A mapping of connection names to redis connections.
var globalConns = map[string]*RedisConn{}
func Get(name string) *RedisConn {
if globalConns[name] == nil {
globalConns[name] = makeRedisConnection(name)
}
return globalConns[name]
}
```
Even though this pattern would work, it breaks with our conception of the
program structure in more complex cases like example 2. Rather than the `redis`
component being owned by the `server` component, which actually uses it, it
would be practically owned by _all_ components, since all are able to use it.
Compartmentalization has been broken, and can only be held together through
sheer human discipline.
**This is the problem with all global state. It is shareable among all
components of a program, and so is accountable to none of them.** One must look
at an entire codebase to understand how a globally held component is used,
which might not even be possible for a large codebase. Therefore, the
maintainers of these shared components rely entirely on the discipline of their
fellow coders when making changes, usually discovering where that discipline
broke down once the changes have been pushed live.
Global state also makes it easier for disparate programs/components to share
datastores for completely unrelated tasks. In example 2, rather than creating a
new redis instance for the root components statistics storage, the coder might
have instead said, “well, theres already a redis instance available, Ill just
use that.” And so, compartmentalization would have been broken further. Perhaps
the two instances _could_ be coalesced into the same instance for the sake of
resource efficiency, but that decision would be better made at runtime via the
configuration of the program, rather than being hardcoded into the code.
From the perspective of team management, global state-based patterns do nothing
except slow teams down. The person/team responsible for maintaining the central
library in which shared components live (`redis`, in the above examples)
becomes the bottleneck for creating new instances for new components, which
will further lead to re-using existing instances rather than creating new ones,
further breaking compartmentalization. Additionally the person/team responsible
for the central library, rather than the team using it, often finds themselves
as the maintainers of the shared resource.
### Component Structure
So what does proper program structure look like? In my mind the structure of a
program is a hierarchy of components, or, in other words, a tree. The leaf
nodes of the tree are almost _always_ IO related components, e.g., database
connections, RPC server frameworks or clients, message queue consumers, etc.
The non-leaf nodes will _generally_ be components that bring together the
functionalities of their children in some useful way, though they may also have
some IO functionality of their own.
Let's look at an even more complex structure, still only using the `redis` and
`http` component types:
{% include image.html
dir="program-structure" file="diag3.jpg" width=729
descr="Example 3"
%}
This component structure contains the addition of the `debug` component.
Clearly the `http` and `redis` components are reusable in different contexts,
but for this example the `debug` endpoint is as well. It creates a separate
http server that can be queried to perform runtime debugging of the program,
and can be tacked onto virtually any program. The `rest-api` component is
specific to this program and is therefore not reusable. Lets dive into it a
bit to see how it might be implemented:
```go
// RestAPI is very much not thread-safe, hopefully it doesn't have to handle
// more than one request at once.
type RestAPI struct {
redisConn *redis.RedisConn
httpSrv *http.Server
// Statistics exported for other components to see
RequestCount int
FooRequestCount int
BarRequestCount int
}
func NewRestAPI() *RestAPI {
r := new(RestAPI)
r.redisConn := redis.NewConn("127.0.0.1:6379")
// mux will route requests to different handlers based on their URL path.
mux := http.NewServeMux()
mux.HandleFunc("/foo", r.fooHandler)
mux.HandleFunc("/bar", r.barHandler)
r.httpSrv := http.NewServer(mux)
// Listen for requests and serve them in the background.
go r.httpSrv.Listen(":8000")
return r
}
func (r *RestAPI) fooHandler(rw http.ResponseWriter, r *http.Request) {
r.redisConn.Command("INCR", "fooKey")
r.RequestCount++
r.FooRequestCount++
}
func (r *RestAPI) barHandler(rw http.ResponseWriter, r *http.Request) {
r.redisConn.Command("INCR", "barKey")
r.RequestCount++
r.BarRequestCount++
}
```
In that snippet `rest-api` coalesced `http` and `redis` into a simple REST-like
api using pre-made library components. `main.go`, the root component, does much
the same:
```go
func main() {
// Create debug server and start listening in the background
debugSrv := debug.NewServer()
// Set up the RestAPI, this will automatically start listening
restAPI := NewRestAPI()
// Create another redis connection and use it to store statistics
statsRedisConn := redis.NewConn("127.0.0.1:6380")
for {
time.Sleep(1 * time.Second)
statsRedisConn.Command("SET", "numReqs", restAPI.RequestCount)
statsRedisConn.Command("SET", "numFooReqs", restAPI.FooRequestCount)
statsRedisConn.Command("SET", "numBarReqs", restAPI.BarRequestCount)
}
}
```
One thing that is clearly missing in this program is proper configuration,
whether from command-line or environment variables, etc. As it stands, all
configuration parameters, such as the redis addresses and http listen
addresses, are hardcoded. Proper configuration actually ends up being somewhat
difficult, as the ideal case would be for each component to set up its own
configuration variables without its parent needing to be aware. For example,
`redis` could set up `addr` and `pool-size` parameters. The problem is that there
are two `redis` components in the program, and their parameters would therefore
conflict with each other. An elegant solution to this problem is discussed in
the next section.
## Part 2: Components, Configuration, and Runtime
The key to the configuration problem is to recognize that, even if there are
two of the same component in a program, they cant occupy the same place in the
programs structure. In the above example, there are two `http` components: one
under `rest-api` and the other under `debug`. Because the structure is
represented as a tree of components, the “path” of any node in the tree
uniquely represents it in the structure. For example, the two `http` components
in the previous example have these paths:
```
root -> rest-api -> http
root -> debug -> http
```
If each component were to know its place in the component tree, then it would
easily be able to ensure that its configuration and initialization didnt
conflict with other components of the same type. If the `http` component sets
up a command-line parameter to know what address to listen on, the two `http`
components in that program would set up:
```
--rest-api-listen-addr
--debug-listen-addr
```
So how can we enable each component to know its path in the component structure?
To answer this, well have to take a detour through a type, called `Component`.
### Component and Configuration
The `Component` type is a made-up type (though youll be able to find an
implementation of it at the end of this post). It has a single primary purpose,
and that is to convey the programs structure to new components.
To see how this is done, let's look at a couple of `Component`'s methods:
```go
// Package mcmp
// New returns a new Component which has no parents or children. It is therefore
// the root component of a component hierarchy.
func New() *Component
// Child returns a new child of the called upon Component.
func (*Component) Child(name string) *Component
// Path returns the Component's path in the component hierarchy. It will return
// an empty slice if the Component is the root component.
func (*Component) Path() []string
```
`Child` is used to create a new `Component`, corresponding to a new child node
in the component structure, and `Path` is used retrieve the path of any
`Component` within that structure. For the sake of keeping the examples simple,
lets pretend these functions have been implemented in a package called `mcmp`.
Heres an example of how `Component` might be used in the `redis` components
code:
```go
// Package redis
func NewConn(cmp *mcmp.Component, defaultAddr string) *RedisConn {
cmp = cmp.Child("redis")
paramPrefix := strings.Join(cmp.Path(), "-")
addrParam := flag.String(paramPrefix+"-addr", defaultAddr, "Address of redis instance to connect to")
// finish setup
return redisConn
}
```
In our above example, the two `redis` components' parameters would be:
```
// This first parameter is for the stats redis, whose parent is the root and
// therefore doesn't have a prefix. Perhaps stats should be broken into its own
// component in order to fix this.
--redis-addr
--rest-api-redis-addr
```
`Component` definitely makes it easier to instantiate multiple redis components
in our program, since it allows them to know their place in the component
structure.
Having to construct the prefix for the parameters ourselves is pretty annoying,
so lets introduce a new package, `mcfg`, which acts like `flag` but is aware
of `Component`. Then `redis.NewConn` is reduced to:
```go
// Package redis
func NewConn(cmp *mcmp.Component, defaultAddr string) *RedisConn {
cmp = cmp.Child("redis")
addrParam := mcfg.String(cmp, "addr", defaultAddr, "Address of redis instance to connect to")
// finish setup
return redisConn
}
```
Easy-peasy.
#### But What About Parse?
Sharp-eyed gophers will notice that there is a key piece missing: When is
`flag.Parse`, or its `mcfg` counterpart, called? When does `addrParam` actually
get populated? It cant happen inside `redis.NewConn` because there might be
other components after `redis.NewConn` that want to set up parameters. To
illustrate the problem, lets look at a simple program that wants to set up two
`redis` components:
```go
func main() {
// Create the root Component, an empty Component.
cmp := mcmp.New()
// Create the Components for two sub-components, foo and bar.
cmpFoo := cmp.Child("foo")
cmpBar := cmp.Child("bar")
// Now we want to try to create a redis sub-component for each component.
// This will set up the parameter "--foo-redis-addr", but bar hasn't had a
// chance to set up its corresponding parameter, so the command-line can't
// be parsed yet.
fooRedis := redis.NewConn(cmpFoo, "127.0.0.1:6379")
// This will set up the parameter "--bar-redis-addr", but, as mentioned
// before, redis.NewConn can't parse command-line.
barRedis := redis.NewConn(cmpBar, "127.0.0.1:6379")
// It is only after all components have been instantiated that the
// command-line arguments can be parsed
mcfg.Parse()
}
```
While this solves our argument parsing problem, fooRedis and barRedis are not
usable yet because the actual connections have not been made. This is a classic
chicken and the egg problem. The func `redis.NewConn` needs to make a connection
which it cannot do until _after_ `mcfg.Parse` is called, but `mcfg.Parse` cannot
be called until after `redis.NewConn` has returned. We will solve this problem
in the next section.
### Instantiation vs Initialization
Lets break down `redis.NewConn` into two phases: instantiation and
initialization. Instantiation refers to creating the component on the component
structure and having it declare what it needs in order to initialize (e.g.,
configuration parameters). During instantiation, nothing external to the
program is performed; no IO, no reading of the command-line, no logging, etc.
All thats happened is that the empty template of a `redis` component has been
created.
Initialization is the phase during which the template is filled in.
Configuration parameters are read, startup actions like the creation of database
connections are performed, and logging is output for informational and debugging
purposes.
The key to making effective use of this dichotomy is to allow _all_ components
to instantiate themselves before they initialize themselves. By doing this we
can ensure, for example, that all components have had the chance to declare
their configuration parameters before configuration parsing is done.
So lets modify `redis.NewConn` so that it follows this dichotomy. It makes
sense to leave instantiation-related code where it is, but we need a mechanism
by which we can declare initialization code before actually calling it. For
this, I will introduce the idea of a “hook.”
#### But First: Augment Component
In order to support hooks, however, `Component` will need to be augmented with
a few new methods. Right now, it can only carry with it information about the
component structure, but here we will add the ability to carry arbitrary
key/value information as well:
```go
// Package mcmp
// SetValue sets the given key to the given value on the Component, overwriting
// any previous value for that key.
func (*Component) SetValue(key, value interface{})
// Value returns the value which has been set for the given key, or nil if the
// key was never set.
func (*Component) Value(key interface{}) interface{}
// Children returns the Component's children in the order they were created.
func (*Component) Children() []*Component
```
The final method allows us to, starting at the root `Component`, traverse the
component structure and interact with each `Component`s key/value store. This
will be useful for implementing hooks.
#### Hooks
A hook is simply a function that will run later. We will declare a new package,
calling it `mrun`, and say that it has two new functions:
```go
// Package mrun
// InitHook registers the given hook to the given Component.
func InitHook(cmp *mcmp.Component, hook func())
// Init runs all hooks registered using InitHook. Hooks are run in the order
// they were registered.
func Init(cmp *mcmp.Component)
```
With these two functions, we are able to defer the initialization phase of
startup by using the same `Components` we were passing around for the purpose
of denoting component structure.
Now, with these few extra pieces of functionality in place, lets reconsider the
most recent example, and make a program that creates two redis components which
exist independently of each other:
```go
// Package redis
// NOTE that NewConn has been renamed to InstConn, to reflect that the returned
// *RedisConn is merely instantiated, not initialized.
func InstConn(cmp *mcmp.Component, defaultAddr string) *RedisConn {
cmp = cmp.Child("redis")
// we instantiate an empty RedisConn instance and parameters for it. Neither
// has been initialized yet. They will remain empty until initialization has
// occurred.
redisConn := new(RedisConn)
addrParam := mcfg.String(cmp, "addr", defaultAddr, "Address of redis instance to connect to")
mrun.InitHook(cmp, func() {
// This hook will run after parameter initialization has happened, and
// so addrParam will be usable. Once this hook as run, redisConn will be
// usable as well.
*redisConn = makeRedisConnection(*addrParam)
})
// Now that cmp has had configuration parameters and intialization hooks
// set into it, return the empty redisConn instance back to the parent.
return redisConn
}
```
```go
// Package main
func main() {
// Create the root Component, an empty Component.
cmp := mcmp.New()
// Create the Components for two sub-components, foo and bar.
cmpFoo := cmp.Child("foo")
cmpBar := cmp.Child("bar")
// Add redis components to each of the foo and bar sub-components.
redisFoo := redis.InstConn(cmpFoo, "127.0.0.1:6379")
redisBar := redis.InstConn(cmpBar, "127.0.0.1:6379")
// Parse will descend into the Component and all of its children,
// discovering all registered configuration parameters and filling them from
// the command-line.
mcfg.Parse(cmp)
// Now that configuration parameters have been initialized, run the Init
// hooks for all Components.
mrun.Init(cmp)
// At this point the redis components have been fully initialized and may be
// used. For this example we'll copy all keys from one to the other.
keys := redisFoo.Command("KEYS", "*")
for i := range keys {
val := redisFoo.Command("GET", keys[i])
redisBar.Command("SET", keys[i], val)
}
}
```
## Conclusion
While the examples given here are fairly simplistic, the pattern itself is quite
powerful. Codebases naturally accumulate small, domain-specific behaviors and
optimizations over time, especially around the IO components of the program.
Databases are used with specific options that an organization finds useful,
logging is performed in particular places, metrics are counted around certain
pieces of code, etc.
By programming with component structure in mind, we are able to keep these
optimizations while also keeping the clarity and compartmentalization of the
code intact. We can keep our code flexible and configurable, while also
re-usable and testable. Also, the simplicity of the tools involved means they
can be extended and retrofitted for nearly any situation or use-case.
Overall, this is a powerful pattern that Ive found myself unable to do without
once I began using it.
### Implementation
As a final note, you can find an example implementation of the packages
described in this post here:
* [mcmp](https://godoc.org/github.com/mediocregopher/mediocre-go-lib/mcmp)
* [mcfg](https://godoc.org/github.com/mediocregopher/mediocre-go-lib/mcfg)
* [mrun](https://godoc.org/github.com/mediocregopher/mediocre-go-lib/mrun)
The packages are not stable and are likely to change frequently. Youll also
find that they have been extended quite a bit from the simple descriptions found
here, based on what Ive found useful as Ive implemented programs using
component structures. With these two points in mind, I would encourage you to
look and take whatever functionality you find useful for yourself, and not use
the packages directly. The core pieces are not different from what has been
described in this post.

View File

@ -1,55 +0,0 @@
---
title: >-
Trading in the Rain
description: >-
All those... gains... will be lost like... tears...
---
<!-- MIDI.js -->
<!-- polyfill -->
<script src="/assets/trading-in-the-rain/MIDI.js/inc/shim/Base64.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/MIDI.js/inc/shim/Base64binary.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/MIDI.js/inc/shim/WebAudioAPI.js" type="text/javascript"></script>
<!-- MIDI.js package -->
<script src="/assets/trading-in-the-rain/MIDI.js/js/midi/audioDetect.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/MIDI.js/js/midi/gm.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/MIDI.js/js/midi/loader.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/MIDI.js/js/midi/plugin.audiotag.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/MIDI.js/js/midi/plugin.webaudio.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/MIDI.js/js/midi/plugin.webmidi.js" type="text/javascript"></script>
<!-- utils -->
<script src="/assets/trading-in-the-rain/MIDI.js/js/util/dom_request_xhr.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/MIDI.js/js/util/dom_request_script.js" type="text/javascript"></script>
<!-- / MIDI.js -->
<script src="/assets/trading-in-the-rain/Distributor.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/MusicBox.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/RainCanvas.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/CW.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/SeriesComposer.js" type="text/javascript"></script>
<script src="/assets/trading-in-the-rain/main.js" type="text/javascript"></script>
<div id="tradingInRainModal">
For each pair listed below, live trade data will be pulled down from the
<a href="https://docs.cryptowat.ch/websocket-api/">Cryptowat.ch Websocket
API</a> and used to generate musical rain drops. The price of each trade
determines both the musical note and position of the rain drop on the screen,
while the volume of each trade determines how long the note is held and how big
the rain drop is.
<p id="markets">Pairs to be generated, by color:<br/><br/></p>
<button id="button" onclick="run()">Click Here to Begin</button>
<p id="progress"></p>
<script type="text/javascript">
fillMarketP();
if (window.addEventListener) window.addEventListener("load", autorun, false);
else if (window.attachEvent) window.attachEvent("onload", autorun);
else window.onload = autorun;
</script>
</div>
<canvas id="rainCanvas" style=""></canvas>

View File

@ -1,161 +0,0 @@
---
title: >-
Denver Protests
description: >-
Craziness
---
# Saturday, May 30th
We went to the May 30th protest at Civic Center Park. We were there for a few
hours during the day, leaving around 4pm. I would describe the character of the
protest as being energetic, angry, but contained. A huge crowd moved in and
around civic center, chanting and being rowdy, but clearly was being led.
After a last hurrah at the pavilion it seemed that the organized event was
"over". We stayed a while longer, and eventually headed back home. I don't feel
that people really left the park at the same time we did; mostly everyone just
dispersed around the park and found somewhere to keep hanging out.
Tonight there has been an 8pm curfew. The police lined up on the north side of
the park, armored and clearly ready for action. We watched all of this on the
live news stations, gritting our teeth through the comentary of their reporters.
As the police stood there, the clock counting down to 8, the protesters grew
more and more irritated. They taunted the police, and formed a line of their
own. The braver (or more dramatic) protesters walked around in the no-man's land
between them, occasionally earning themselves some teargas.
The police began pushing forward just before 8 a little, but began pushing in
earnest just after 8, after the howling. They would advance, wait, advance, wait
again. An armada of police cars, ambulance, and fire trucks followed the line as
it advanced.
The police did not give the protesters anywhere to go except into Capital Hill,
southeast of Civic Center Park. We watched as a huge crowd marched past the
front of our house, chanting their call and response: "What's his name?" "GEORGE
FLOYD". The feeling wasn't of violence still, just anger. Indignant at a curfew
aimed at quelling a movement, the protesters simply kept moving. The police were
never far behind.
We sat on our front stoop with our neighbors and watched the night unfold. I
don't think a single person in our building or the buildings to the left and
right of us hadn't gone to protest today in some capacity. We came back from our
various outings and sat out front, watching the crowds and patrolling up and
down the street to keep tabs on things.
Around 9pm the fires started. We saw them on the news, and in person. They were
generally dumpster fires, generally placed such that they were away from
buildings, clearly being done more to be annoying than to accomplish anything
specific. A very large set of fires was started a block south of us, in the
middle of the street. The fire department was there within a few minutes to put
those out, before moving on.
From the corner of my eye, sitting back on the stoop, I noticed our neighbors
running into their backyard. We ran after them, and they told us there was a
dumpster fire in our alley. They were running with fire extinguishers, and we
ran inside to grab some of our own. By the time we got to the backyard the fire
was only smouldering, and the fire department was coming down the alley. We
scurried back into the backyard. A few minutes later I peeked my head around the
corner, into the alley, to see what happening. I was greeted by at least two
police in riot gear, guarding the dumpster as the fire department worked. They
saw me but didn't move, and I quickly retreated back to the yard.
Talking to our neighbor later we found out she had seen a group of about 10
people back there, and watched them jump the fence into another backyard in
order to escape the alley. She thinks they, or some subset of them, started the
fire. She looked one in the eye, she says, and didn't get the impression they
were trying to cause damage, just to make a statement.
The fires stopped not long after that, it seems. We're pretty sure the fire
trucks were just driving up and down the main roads, looking into alleys and
stopping all fires they could find. In all this time the police didn't do much.
They would hold a line, but never chase anyone. Even now, as I write this around
midnight, people are still out, meandering around in small groups, and police
are present but not really doing anything.
It's hard to get a good view of everything though. All we have is livestreams on
youtube to go on at this point. There's a couple intrepid amateur reporters out
there, getting into the crowds and streaming events as they happen. Right now
we're watching people moving down Lincoln towards Civic Center Park, some of
them trying to smash windows of buildings as they go.
The violence of these protests is going to be the major story of tonight, I know
that already. That I know of there's been 3 police injured, some broken
windows, and quite a bit of graffiti. I do believe the the tactic of pushing
everyone into Cap Hill had the desired effect of reducing looting (again, as far
as I can tell so far), but at that expense of those who live here who have to
endure latent tear gas, dumpster fires, and sirens all through the night.
Even now, at midnight, from what I can see from my porch and from these live
streams, the protesters are not violent. At worst they are guilty of a lot of
loitering. The graffiti, the smashed windows, the injured officers, all of these
things will be held up as examples of the anarchy and violence inherent to the
protesters. But I don't think that's an honest picture. The vast, vast majority
of those out right now are civily disobeying an unjust curfew, trying to keep
the energy of the movement alive.
My thoughts about these things are complicated. When turning a corner on the
street I'm far more afraid to see the police than to see other protesters. The
fires have been annoying, and stupid, and unhelpful, but were never threatening.
The violence is stupid, though I don't shed many tears for a looted Chili's or
Papa Johns. The police have actually shown more restraint than I expected in all
of this, though funneling the protest into a residential neighborhood was an
incredibly stupid move. Could the protesters not have just stayed in the park?
Yes, the park would likely have been turned into an encampment, but it was
already heading into that direction due to Covid-19. Overall, this night didn't
need to be so hard, but Denver handled this well.
But, it's only 1am, and the night has a long way to go. Things could still get
worse. Even now I'm watching people trying to break into the supreme court
building. Civic Center Park appears to be very populated again, and the police
are very present there again. It's possible I may eat my words.
# Monday, June 1st
Yesterday was quite a bit more tame than the craziness Saturday. I woke up
Sunday morning feeling antsy, and rode my bike around to see the damage. I had a
long conversation with a homeless man named Gary in Civic Center Park. He was
pissed, and had a lot to say about the "suburban kids" destroying the park he
and many others live in, causing it to be shut down and tear gassed. The
protesters saw it as a game, according to him, but it was life and death for the
homeless; three of his guys got beat up in the street, and neither police nor
protesters stopped it.
Many people had shown up to the park early to help clean it up. Apart from the
graffiti, which was also in the process of being cleaned, it was hard to tell
anything had actually happened. Gary had some words about them as well, that
they were only there for the gram and some pats on the back, but once they left
his life would be back as it was. I could feel that, but I also appreciated that
people were cognizant that damage was being done and were willing to do
something about it.
I rode around 16th street mall, down colfax, and back up 13th, looking to see if
anything had happened. For the most part there was no damage, save the graffiti.
A mediterranean restaurant got its windows smashed, as well as the Office Depot.
The restaurant was unfortunate, Office Depot will be ok.
The protest yesterday was much more peaceful. The cops were nowhere to be found
when curfew hit, but did eventually show up when the protest moved down Colfax.
They had lined the streets around their precinct building there, but for the
most part the protesters just kept walking. This is when the "violence" started.
The cops moved into the street, forming a line across Colfax behind the
protesters. Police cars and vans started moving. As the protest turned back,
presumably to head back to the capitol lawn, it ran into the riot line.
Predictably, everyone scattered. The cat-and-mouse game had begun, which meant
dumpster fires, broken windows, tear gas, and all the rest. Watching the whole
thing it was extremely clear to us, though not the news casters, unfortunately,
that if the police hadn't moved out into Colfax nothing would have ever
happened. Instead, the news casters lamented that people were bringing things
like helmets, gas masks, traffic cones, shields, etc... and so were clearly not there
"for the right reasons".
The thing that the news casters couldn't seem to grasp was that the police
attempting to control these situations are what are catalyzing them in the first
place. These are protests _against_ the police, they cannot take place under the
terms the police choose. If the police were not here setting terms, but instead
working with the peaceful protesters (the vast, vast majority) to quell the
violence, no one would be here with helmets, gas masks, traffic cones,
shields... But instead the protesters feel they need to protect themselves in
order to be heard, and the police feel they have to exercise their power to
maintain control, and so the situation degrades.

View File

@ -1,154 +0,0 @@
---
title: >-
Visualization 3
description: >-
All the pixels.
series: viz
---
<canvas id="canvas" style="padding-bottom: 2rem;"></canvas>
This visualization is built from the ground up. On every frame a random set of
pixels is chosen. Each chosen pixel calculates the average of its color and the
color of a random neighbor. Some random color drift is added in as well. It
replaces its own color with that calculated color.
Choosing a neighbor is done using the "asteroid rule", ie a pixel at the very
top row is considered to be the neighbor of the pixel on the bottom row of the
same column.
Without the asteroid rule the pixels would all eventually converge into a single
uniform color, generally a light blue, due to the colors at the edge, the reds,
being quickly averaged away. With the asteroid rule in place the canvas has no
edges, thus no position on the canvas is favored and balance can be maintained.
<script type="text/javascript">
let rectSize = 12;
function randn(n) {
return Math.floor(Math.random() * n);
}
let canvas = document.getElementById("canvas");
canvas.width = window.innerWidth - (window.innerWidth % rectSize);
canvas.height = window.innerHeight- (window.innerHeight % rectSize);
let ctx = canvas.getContext("2d");
let w = canvas.width / rectSize;
let h = canvas.height / rectSize;
let matrices = new Array(2);
matrices[0] = new Array(w);
matrices[1] = new Array(w);
for (let x = 0; x < w; x++) {
matrices[0][x] = new Array(h);
matrices[1][x] = new Array(h);
for (let y = 0; y < h; y++) {
let el = {
h: 360 * (x / w),
s: "100%",
l: "50%",
};
matrices[0][x][y] = el;
matrices[1][x][y] = el;
}
}
// draw initial canvas, from here on out only individual rectangles will be
// filled as they get updated.
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
let el = matrices[0][x][y];
ctx.fillStyle = `hsl(${el.h}, ${el.s}, ${el.l})`;
ctx.fillRect(x * rectSize, y * rectSize, rectSize, rectSize);
}
}
let requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
let neighbors = [
[-1, -1], [0, -1], [1, -1],
[-1, 0], [1, 0],
[-1, 1], [0, 1], [1, 1],
];
function randNeighborAsteroid(matrix, x, y) {
let neighborCoord = neighbors[randn(neighbors.length)];
let neighborX = x+neighborCoord[0];
let neighborY = y+neighborCoord[1];
neighborX = (neighborX + w) % w;
neighborY = (neighborY + h) % h;
return matrix[neighborX][neighborY];
}
function randNeighbor(matrix, x, y) {
while (true) {
let neighborCoord = neighbors[randn(neighbors.length)];
let neighborX = x+neighborCoord[0];
let neighborY = y+neighborCoord[1];
if (neighborX < 0 || neighborX >= w || neighborY < 0 || neighborY >= h) {
continue;
}
return matrix[neighborX][neighborY];
}
}
let drift = 10;
function genChildH(elA, elB) {
// set the two h values, h1 <= h2
let h1 = elA.h;
let h2 = elB.h;
if (h1 > h2) {
h1 = elB.h;
h2 = elA.h;
}
// diff must be between 0 (inclusive) and 360 (exclusive). If it's greater
// than 180 then it's not the shortest path around, that must be the other
// way around the circle.
let hChild;
let diff = h2 - h1;
if (diff > 180) {
diff = 360 - diff;
hChild = h2 + (diff / 2);
} else {
hChild = h1 + (diff / 2);
}
hChild += (Math.random() * drift * 2) - drift;
hChild = (hChild + 360) % 360;
return hChild;
}
let tick = 0;
function doTick() {
tick++;
let currI = tick % 2;
let curr = matrices[currI];
let lastI = (tick - 1) % 2;
let last = matrices[lastI];
for (let i = 0; i < (w * h / 2); i++) {
let x = randn(w);
let y = randn(h);
if (curr[x][y].lastTick == tick) continue;
let neighbor = randNeighborAsteroid(last, x, y);
curr[x][y].h = genChildH(curr[x][y], neighbor);
curr[x][y].lastTick = tick;
ctx.fillStyle = `hsl(${curr[x][y].h}, ${curr[x][y].s}, ${curr[x][y].l})`;
ctx.fillRect(x * rectSize, y * rectSize, rectSize, rectSize);
}
matrices[currI] = curr;
requestAnimationFrame(doTick);
}
requestAnimationFrame(doTick);
</script>

View File

@ -1,352 +0,0 @@
---
title: >-
Component-Oriented Programming
description: >-
A concise description of.
---
[A previous post in this
blog](/2019/08/02/program-structure-and-composability.html) focused on a
framework developed to make designing component-based programs easier. In
retrospect, the proposed pattern/framework was over-engineered. This post
attempts to present the same ideas in a more distilled form, as a simple
programming pattern and without the unnecessary framework.
## Components
Many languages, libraries, and patterns make use of a concept called a
"component," but in each case the meaning of "component" might be slightly
different. Therefore, to begin talking about components, it is necessary to first
describe what is meant by "component" in this post.
For the purposes of this post, the properties of components include the
following.
&nbsp;1... **Abstract**: A component is an interface consisting of one or more
methods.
&nbsp;&nbsp;&nbsp;1a... A function might be considered a single-method component
_if_ the language supports first-class functions.
&nbsp;&nbsp;&nbsp;1b... A component, being an interface, may have one or more
implementations. Generally, there will be a primary implementation, which is
used during a program's runtime, and secondary "mock" implementations, which are
only used when testing other components.
&nbsp;2... **Instantiatable**: An instance of a component, given some set of
parameters, can be instantiated as a standalone entity. More than one of the
same component can be instantiated, as needed.
&nbsp;3... **Composable**: A component may be used as a parameter of another
component's instantiation. This would make it a child component of the one being
instantiated (the parent).
&nbsp;4... **Pure**: A component may not use mutable global variables (i.e.,
singletons) or impure global functions (e.g., system calls). It may only use
constants and variables/components given to it during instantiation.
&nbsp;5... **Ephemeral**: A component may have a specific method used to clean
up all resources that it's holding (e.g., network connections, file handles,
language-specific lightweight threads, etc.).
&nbsp;&nbsp;&nbsp;5a... This cleanup method should _not_ clean up any child
components given as instantiation parameters.
&nbsp;&nbsp;&nbsp;5b... This cleanup method should not return until the
component's cleanup is complete.
&nbsp;&nbsp;&nbsp;5c... A component should not be cleaned up until all its
parent components are cleaned up.
Components are composed together to create component-oriented programs. This is
done by passing components as parameters to other components during
instantiation. The `main` procedure of the program is responsible for
instantiating and composing the components of the program.
## Example
It's easier to show than to tell. This section posits a simple program and then
describes how it would be implemented in a component-oriented way. The program
chooses a random number and exposes an HTTP interface that allows users to try
and guess that number. The following are requirements of the program:
* A guess consists of a name that identifies the user performing the guess and
the number that is being guessed;
* A score is kept for each user who has performed a guess;
* Upon an incorrect guess, the user should be informed of whether they guessed
too high or too low, and 1 point should be deducted from their score;
* Upon a correct guess, the program should pick a new random number against
which to check subsequent guesses, and 1000 points should be added to the
user's score;
* The HTTP interface should have two endpoints: one for users to submit guesses,
and another that lists out user scores from highest to lowest;
* Scores should be saved to disk so they survive program restarts.
It seems clear that there will be two major areas of functionality for our
program: score-keeping and user interaction via HTTP. Each of these can be
encapsulated into components called `scoreboard` and `httpHandlers`,
respectively.
`scoreboard` will need to interact with a filesystem component to save/restore
scores (because it can't use system calls directly; see property 4). It would be
wasteful for `scoreboard` to save the scores to disk on every score update, so
instead it will do so every 5 seconds. A time component will be required to
support this.
`httpHandlers` will be choosing the random number which is being guessed, and
will therefore need a component that produces random numbers. `httpHandlers`
will also be recording score changes to `scoreboard`, so it will need access to
`scoreboard`.
The example implementation will be written in go, which makes differentiating
HTTP handler functionality from the actual HTTP server quite easy; thus, there
will be an `httpServer` component that uses `httpHandlers`.
Finally, a `logger` component will be used in various places to log useful
information during runtime.
[The example implementation can be found
here.](/assets/component-oriented-design/v1/main.html) While most of it can be
skimmed, it is recommended to at least read through the `main` function to see
how components are composed together. Note that `main` is where all components
are instantiated, and that all components' take in their child components as
part of their instantiation.
## DAG
One way to look at a component-oriented program is as a directed acyclic graph
(DAG), where each node in the graph represents a component, and each edge
indicates that one component depends upon another component for instantiation.
For the previous program, it's quite easy to construct such a DAG just by
looking at `main`, as in the following:
```
net.Listener rand.Rand os.File
^ ^ ^
| | |
httpServer --> httpHandlers --> scoreboard --> time.Ticker
| | |
+---------------+---------------+--> log.Logger
```
Note that all the leaves of the DAG (i.e., nodes with no children) describe the
points where the program meets the operating system via system calls. The leaves
are, in essence, the program's interface with the outside world.
While it's not necessary to actually draw out the DAG for every program one
writes, it can be helpful to at least think about the program's structure in
these terms.
## Benefits
Looking at the previous example implementation, one would be forgiven for having
the immediate reaction of "This seems like a lot of extra work for little gain.
Why can't I just make the system calls where I need to, and not bother with
wrapping them in interfaces and all these other rules?"
The following sections will answer that concern by showing the benefits gained
by following a component-oriented pattern.
### Testing
Testing is important, that much is being assumed.
A distinction to be made with testing is between unit and non-unit tests. Unit
tests are those for which there are no requirements for the environment outside
the test, such as the existence of global variables, running databases,
filesystems, or network services. Unit tests do not interact with the world
outside the testing procedure, but instead use mocks in place of the
functionality that would be expected by that world.
Unit tests are important because they are faster to run and more consistent than
non-unit tests. Unit tests also force the programmer to consider different
possible states of a component's dependencies during the mocking process.
Unit tests are often not employed by programmers, because they are difficult to
implement for code that does not expose any way to swap out dependencies for
mocks of those dependencies. The primary culprit of this difficulty is the
direct usage of singletons and impure global functions. For component-oriented
programs, all components inherently allow for the swapping out of any
dependencies via their instantiation parameters, so there's no extra effort
needed to support unit tests.
[Tests for the example implementation can be found
here.](/assets/component-oriented-design/v1/main_test.html) Note that all
dependencies of each component being tested are mocked/stubbed next to them.
### Configuration
Practically all programs require some level of runtime configuration. This may
take the form of command-line arguments, environment variables, configuration
files, etc.
For a component-oriented program, all components are instantiated in the same
place, `main`, so it's very easy to expose any arbitrary parameter to the user
via configuration. For any component that is affected by a configurable
parameter, that component merely needs to take an instantiation parameter for
that configurable parameter; `main` can connect the two together. This accounts
for the unit testing of a component with different configurations, while still
allowing for the configuration of any arbitrary internal functionality.
For more complex configuration systems, it is also possible to implement a
`configuration` component that wraps whatever configuration-related
functionality is needed, which other components use as a sub-component. The
effect is the same.
To demonstrate how configuration works in a component-oriented program, the
example program's requirements will be augmented to include the following:
* The point change values for both correct and incorrect guesses (currently
hardcoded at 1000 and 1, respectively) should be configurable on the
command-line;
* The save file's path, HTTP listen address, and save interval should all be
configurable on the command-line.
[The new implementation, with newly configurable parameters, can be found
here.](/assets/component-oriented-design/v2/main.html) Most of the program has
remained the same, and all unit tests from before remain valid. The primary
difference is that `scoreboard` takes in two new parameters for the point change
values, and configuration is set up inside `main` using the `flags` package.
### Setup/Runtime/Cleanup
A program can be split into three stages: setup, runtime, and cleanup. Setup is
the stage during which the internal state is assembled to make runtime possible.
Runtime is the stage during which a program's actual function is being
performed. Cleanup is the stage during which the runtime stops and internal
state is disassembled.
A graceful (i.e., reliably correct) setup is quite natural to accomplish for
most. On the other hand, a graceful cleanup is, unfortunately, not a programmer's
first concern (if it is a concern at all).
When building reliable and correct programs, a graceful cleanup is as important
as a graceful setup and runtime. A program is still running while it is being
cleaned up, and it's possibly still acting on the outside world. Shouldn't
it behave correctly during that time?
Achieving a graceful setup and cleanup with components is quite simple.
During setup, a single-threaded procedure (`main`) first constructs the leaf
components, then the components that take those leaves as parameters, then the
components that take _those_ as parameters, and so on, until the component DAG
is fully constructed.
At this point, the program's runtime has begun.
Once the runtime is over, signified by a process signal or some other mechanism,
it's only necessary to call each component's cleanup method (if any; see
property 5) in the reverse of the order in which the components were
instantiated. This order is inherently deterministic, as the components were
instantiated by a single-threaded procedure.
Inherent to this pattern is the fact that each component will certainly be
cleaned up before any of its child components, as its child components must have
been instantiated first, and a component will not clean up child components
given as parameters (properties 5a and 5c). Therefore, the pattern avoids
use-after-cleanup situations.
To demonstrate a graceful cleanup in a component-oriented program, the example
program's requirements will be augmented to include the following:
* The program will terminate itself upon an interrupt signal;
* During termination (cleanup), the program will save the latest set of scores
to disk one final time.
[The new implementation that accounts for these new requirements can be found
here.](/assets/component-oriented-design/v3/main.html) For this example, go's
`defer` feature could have been used instead, which would have been even
cleaner, but was omitted for the sake of those using other languages.
## Conclusion
The component pattern helps make programs more reliable with only a small amount
of extra effort incurred. In fact, most of the pattern has to do with
establishing sensible abstractions around global functionality and remembering
certain idioms for how those abstractions should be composed together, something
most of us already do to some extent anyway.
While beneficial in many ways, component-oriented programming is merely a tool
that can be applied in many cases. It is certain that there are cases where it
is not the right tool for the job, so apply it deliberately and intelligently.
## Criticisms/Questions
In lieu of a FAQ, I will attempt to premeditate questions and criticisms of the
component-oriented programming pattern laid out in this post.
**This seems like a lot of extra work.**
Building reliable programs is a lot of work, just as building a
reliable _anything_ is a lot of work. Many of us work in an industry that likes
to balance reliability (sometimes referred to by the more specious "quality")
with malleability and deliverability, which naturally leads to skepticism of any
suggestions requiring more time spent on reliability. This is not necessarily a
bad thing, it's just how the industry functions.
All that said, a pattern need not be followed perfectly to be worthwhile, and
the amount of extra work incurred by it can be decided based on practical
considerations. I merely maintain that code which is (mostly) component-oriented
is easier to maintain in the long run, even if it might be harder to get off the
ground initially.
**My language makes this difficult.**
I don't know of any language which makes this pattern particularly easier than
others, so, unfortunately, we're all in the same boat to some extent (though I
recognize that some languages, or their ecosystems, make it more difficult than
others). It seems to me that this pattern shouldn't be unbearably difficult for
anyone to implement in any language either, however, as the only language
feature required is abstract typing.
It would be nice to one day see a language that explicitly supports this
pattern by baking the component properties in as compiler-checked rules.
**My `main` is too big**
There's no law saying all component construction needs to happen in `main`,
that's just the most sensible place for it. If there are large sections of your
program that are independent of each other, then they could each have their own
construction functions that `main` then calls.
Other questions that are worth asking include: Can my program be split up
into multiple programs? Can the responsibilities of any of my components be
refactored to reduce the overall complexity of the component DAG? Can the
instantiation of any components be moved within their parent's
instantiation function?
(This last suggestion may seem to be disallowed, but is fine as long as the
parent's instantiation function remains pure.)
**Won't this will result in over-abstraction?**
Abstraction is a necessary tool in a programmer's toolkit, there is simply no
way around it. The only questions are "how much?" and "where?"
The use of this pattern does not affect how those questions are answered, in my
opinion, but instead aims to more clearly delineate the relationships and
interactions between the different abstracted types once they've been
established using other methods. Over-abstraction is possible and avoidable
regardless of which language, pattern, or framework is being used.
**Does CoP conflict with object-oriented or functional programming?**
I don't think so. OoP languages will have abstract types as part of their core
feature-set; most difficulties are going to be with deliberately _not_ using
other features of an OoP language, and with imported libraries in the language
perhaps making life inconvenient by not following CoP (specifically regarding
cleanup and the use of singletons).
For functional programming, it may well be that, depending on the language, CoP
is technically being used, as functional languages are already generally
antagonistic toward globals and impure functions, which is most of the battle.
If anything, the transition from functional to component-oriented programming
will generally be an organizational task.

View File

@ -1,50 +0,0 @@
---
title: >-
New Year, New Resolution
description: >-
This blog is about to get some action.
---
At this point I'm fairly well known amongst friends and family for my new year's
resolutions, to the point that earlier this month a friend of mine asked me
"What's it going to be this year?". In the past I've done things like no
chocoloate, no fast food, no added sugar (see a theme?), and no social media.
They've all been of the "I won't do this" sort, because it's a lot easier to
stop doing something than to start doing something new. Doing something new
inherently means _also_ not doing something else; there's only so many hours in
the day, afterall.
## This Year
This year I'm going to shake things up, I'm going to do something new. My
resolution is to have published 52 posts on this blog by Jan 1, 2022, 00:00 UTC.
Only one post per day can count towards the 52. A post must be "substantial" to
count towards the 52. A non-substantial post would be something like the 100
word essay about my weekend that I wrote in first grade, which went something
like "My weekend was really really really ('really' 96 more times) really really
boring".
Other than that, it's pretty open-ended.
## Why
My hope is that I'll get more efficient at writing these things. Usually I take
a lot of time to craft a post, weeks in some cases. I really appreciate those of
you that have taken the time to read them, but to be frank the time commitment
just isn't worth it. With practice I can hopefully learn what exactly I have to
say that others are interested in, and then go back to spending a lot of time
crafting the things being said.
Another part of this is going to be learning how to market myself properly,
something I've always been reticent to do. Our world is filled with people
shouting into the void of the internet, each with their own reasons for wanting
to be heard. Does it need another? Probably not. But here I am. I guess what I'm
really going to be doing is learning _why_ I want to do this; I know I want to
have others read what I write, but is it possible that that desire isn't
entirely selfish? Is it ok if it is?
Once I'm comfortable with why I'm doing this it will, hopefully, be easier to
figure out a marketing avenue I feel comfortable with putting a lot of energy
towards. There must be at least _one_...
So consider this #1, world. Only 51 to go.

View File

@ -1,352 +0,0 @@
---
title: >-
Ginger
description: >-
Yes, it does exist.
---
This post is about a programming language that's been bouncing around in my head
for a _long_ time. I've tried to actually implement the language three or more
times now, but everytime I get stuck or run out of steam. It doesn't help that
everytime I try again the form of the language changes significantly. But all
throughout the name of the language has always been "Ginger". It's a good name.
In the last few years the form of the language has somewhat solidified in my
head, so in lieu of actually working on it I'm going to talk about what it
currently looks like.
## Abstract Syntax Lists
_In the beginning_ there was assembly. Well, really in the beginning there were
punchcards, and probably something even more esoteric before that, but it was
all effectively the same thing: a list of commands the computer would execute
sequentially, with the ability to jump to odd places in the sequence depending
on conditions at runtime. For the purpose of this post, we'll call this class of
languages "abstract syntax list" (ASL) languages.
Here's a hello world program in my favorite ASL language, brainfuck:
```
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.++
+.------.--------.>>+.>++.
```
(If you've never seen brainfuck, it's deliberately unintelligible. But it _is_
an ASL, each character representing a single command, executed by the brainfuck
runtime from left to right.)
ASLs did the job at the time, but luckily we've mostly moved on past them.
## Abstract Syntax Trees
Eventually programmers upgraded to C-like languages. Rather than a sequence of
commands, these languages were syntactically represented by an "abstract syntax
tree" (AST). Rather than executing commands in essentially the same order they
are written, an AST language compiler reads the syntax into a tree of syntax
nodes. What it then does with the tree is language dependent.
Here's a program which outputs all numbers from 0 to 9 to stdout, written in
(slightly non-idiomatic) Go:
```go
i := 0
for {
if i == 10 {
break
}
fmt.Println(i)
i++
}
```
When the Go compiler sees this, it's going to first parse the syntax into an
AST. The AST might look something like this:
```
(root)
|-(:=)
| |-(i)
| |-(0)
|
|-(for)
|-(if)
| |-(==)
| | |-(i)
| | |-(10)
| |
| |-(break)
|
|-(fmt.Println)
| |-(i)
|
|-(++)
|-(i)
```
Each of the non-leaf nodes in the tree represents an operation, and the children
of the node represent the arguments to that operation, if any. From here the
compiler traverses the tree depth-first in order to turn each operation it finds
into the appropriate machine code.
There's a sub-class of AST languages called the LISP ("LISt Processor")
languages. In a LISP language the AST is represented using lists of elements,
where the first element in each list denotes the operation and the rest of the
elements in the list (if any) represent the arguments. Traditionally each list
is represented using parenthesis. For example `(+ 1 1)` represents adding 1 and
1 together.
As a more complex example, here's how to print numbers 0 through 9 to stdout
using my favorite (and, honestly, only) LISP, Clojure:
```clj
(doseq
[n (range 10)]
(println n))
```
Much smaller, but the idea is there. In LISPs there is no differentiation
between the syntax, the AST, and the language's data structures; they are all
one and the same. For this reason LISPs generally have very powerful macro
support, wherein one uses code written in the language to transform code written
in that same language. With macros users can extend a language's functionality
to support nearly anything they need to, but because macro generation happens
_before_ compilation they can still reap the benefits of compiler optimizations.
### AST Pitfalls
The ASL (assembly) is essentially just a thin layer of human readability on top
of raw CPU instructions. It does nothing in the way of representing code in the
way that humans actually think about it (relationships of types, flow of data,
encapsulation of behavior). The AST is a step towards expressing code in human
terms, but it isn't quite there in my opinion. Let me show why by revisiting the
Go example above:
```go
i := 0
for {
if i > 9 {
break
}
fmt.Println(i)
i++
}
```
When I understand this code I don't understand it in terms of its syntax. I
understand it in terms of what it _does_. And what it does is this:
* with a number starting at 0, start a loop.
* if the number is greater than 9, stop the loop.
* otherwise, print the number.
* add one to the number.
* go to start of loop.
This behavior could be further abstracted into the original problem statement,
"it prints numbers 0 through 9 to stdout", but that's too general, as there
are different ways for that to be accomplished. The Clojure example first
defines a list of numbers 0 through 9 and then iterates over that, rather than
looping over a single number. These differences are important when understanding
what code is doing.
So what's the problem? My problem with ASTs is that the syntax I've written down
does _not_ reflect the structure of the code or the flow of data which is in my
head. In the AST representation if you want to follow the flow of data (a single
number) you _have_ to understand the semantic meaning of `i` and `:=`; the AST
structure itself does not convey how data is being moved or modified.
Essentially, there's an extra implicit transformation that must be done to
understand the code in human terms.
## Ginger: An Abstract Syntax Graph Language
In my view the next step is towards using graphs rather than trees for
representing our code. A graph has the benefit of being able to reference
"backwards" into itself, where a tree cannot, and so can represent the flow of
data much more directly.
I would like Ginger to be an ASG language where the language is the graph,
similar to a LISP. But what does this look like exactly? Well, I have a good
idea about what the graph _structure_ will be like and how it will function, but
the syntax is something I haven't bothered much with yet. Representing graph
structures in a text file is a problem to be tackled all on its own. For this
post we'll use a made-up, overly verbose, and probably non-usable syntax, but
hopefully it will convey the graph structure well enough.
### Nodes, Edges, and Tuples
All graphs have nodes, where each node contains a value. A single unique value
can only have a single node in a graph. Nodes are connected by edges, where
edges have a direction and can contain a value themselves.
In the context of Ginger, a node represents a value as expected, and the value
on an edge represents an operation to take on that value. For example:
```
5 -incr-> n
```
`5` and `n` are both nodes in the graph, with an edge going from `5` to `n` that
has the value `incr`. When it comes time to interpret the graph we say that the
value of `n` can be calculated by giving `5` as the input to the operation
`incr` (increment). In other words, the value of `n` is `6`.
What about operations which have more than one input value? For this Ginger
introduces the tuple to its graph type. A tuple is like a node, except that it's
anonymous, which allows more than one to exist within the same graph, as they do
not share the same value. For the purposes of this blog post we'll represent
tuples like this:
```
1 -> } -add-> t
2 -> }
```
`t`'s value is the result of passing a tuple of two values, `1` and `2`, as
inputs to the operation `add`. In other words, the value of `t` is `3`.
For the syntax being described in this post we allow that a single contiguous
graph can be represented as multiple related sections. This can be done because
each node's value is unique, so when the same value is used in disparate
sections we can merge the two sections on that value. For example, the following
two graphs are exactly equivalent (note the parenthesis wrapping the graph which
has been split):
```
1 -> } -add-> t -incr-> tt
2 -> }
```
```
(
1 -> } -add-> t
2 -> }
t -incr-> tt
)
```
(`tt` is `4` in both cases.)
A tuple with only one input edge, a 1-tuple, is a no-op, semantically, but can
be useful structurally to chain multiple operations together without defining
new value names. In the above example the `t` value can be eliminated using a
1-tuple.
```
1 -> } -add-> } -incr-> tt
2 -> }
```
When an integer is used as an operation on a tuple value then the effect is to
output the value in the tuple at that index. For example:
```
1 -> } -0-> } -incr-> t
2 -> }
```
(`t` is `2`.)
### Operations
When a value sits on an edge it is used as an operation on the input of that
edge. Some operations will no doubt be builtin, like `add`, but users should be
able to define their own operations. This can be done using the `in` and `out`
special values. When a graph is used as an operation it is scanned for both `in`
and `out` values. `in` is set to the input value of the operation, and the value
of `out` is used as the output of the operation.
Here we will define the `incr` operation and then use it. Note that we set the
`incr` value to be an entire sub-graph which represents the operation's body.
```
( in -> } -add-> out
1 -> } ) -> incr
5 -incr-> n
```
(`n` is `6`.)
The output of an operation may itself be a tuple. Here's an implementation and
usage of `double-incr`, which increments two values at once.
```
( in -0-> } -incr-> } -> out
}
in -1-> } -incr-> } ) -> double-incr
1 -> } -double-incr-> t -add-> tt
2 -> }
```
(`t` is a 2-tuple with values `2`, and `3`, `tt` is `5.)
### Conditionals
The conditional is a bit weird, and I'm not totally settled on it yet. For now
we'll use this. The `if` operation expects as an input a 2-tuple whose first
value is a boolean and whose second value will be passed along. The `if`
operation is special in that it has _two_ output edges. The first will be taken
if the boolean is true, the second if the boolean is false. The second value in
the input tuple, the one to be passed along, is used as the input to whichever
branch is taken.
Here is an implementation and usage of `max`, which takes two numbers and
outputs the greater of the two. Note that the `if` operation has two output
edges, but our syntax doesn't represent that very cleanly.
```
( in -gt-> } -if-> } -0-> out
in -> } -> } -1-> out ) -> max
1 -> } -max-> t
2 -> }
```
(`t` is `2`.)
It would be simple enough to create a `switch` macro on top of `if`, to allow
for multiple conditionals to be tested at once.
### Loops
Loops are tricky, and I have two thoughts about how they might be accomplished.
One is to literally draw an edge from the right end of the graph back to the
left, at the point where the loop should occur, as that's conceptually what's
happening. But representing that in a text file is difficult. For now I'll
introduce the special `recur` value, and leave this whole section as TBD.
`recur` is cousin of `in` and `out`, in that it's a special value and not an
operation. It takes whatever value it's set to and calls the current operation
with that as input. As an example, here is our now classic 0 through 9 printer
(assume `println` outputs whatever it was input):
```
// incr-1 is an operation which takes a 2-tuple and returns the same 2-tuple
// with the first element incremented.
( in -0-> } -incr-> } -> out
in -1-> } ) -> incr-1
( in -eq-> } -if-> out
in -> } -> } -0-> } -println-> } -incr-1-> } -> recur ) -> print-range
0 -> } -print-range-> }
10 -> }
```
## Next Steps
This post is long enough, and I think gives at least a basic idea of what I'm
going for. The syntax presented here is _extremely_ rudimentary, and is almost
definitely not what any final version of the syntax would look like. But the
general idea behind the structure is sound, I think.
I have a lot of further ideas for Ginger I haven't presented here. Hopefully as
time goes on and I work on the language more some of those ideas can start
taking a more concrete shape and I can write about them.
The next thing I need to do for Ginger is to implement (again) the graph type
for it, since the last one I implemented didn't include tuples. Maybe I can
extend it instead of re-writing it. After that it will be time to really buckle
down and figure out a syntax. Once a syntax is established then it's time to
start on the compiler!

View File

@ -1,239 +0,0 @@
---
title: >-
The Web
description: >-
What is it good for?
---
With the recent crisis in the US's democratic process, there's been much abuzz
in the world about social media's undoubted role in the whole debacle. The
extent to which the algorithms of Facebook, Twitter, Youtube, TikTok, etc, have
played a role in the radicalization of large segments of the world's population
is one popular topic. Another is the tactics those same companies are now
employing to try and euthanize the monster they made so much ad money in
creating.
I don't want to talk about any of that; there is more to the web than
social media. I want to talk about what the web could be, and to do that I want
to first talk about what it has been.
## Web 1.0
In the 1950's computers were generally owned by large organizations like
companies, universities, and governments. They were used to compute and manage
large amounts of data, and each existed independently of the other.
In the 60's protocols began to be developed which would allow them to
communicate over large distances, and thereby share resources (both
computational and informational).
The funding of ARPANET by the US DoD led to the initial versions of the TCP/IP
protocol in the 70's, still used today as the backbone of virtually all internet
communication. Email also came about from ARPANET around this time.
The 80s saw the growth of the internet across the world, as ARPANET gave way to
NSFNET. It was during this time that the domain name system we use today was
developed. At this point the internet use was still mostly for large
non-commercial organizations; there was little commercial footprint, and little
private access. The first commercially available ISP, which allowed access to
the internet from private homes via dialup, wasn't launched until 1989.
And so we find ourselves in the year 1989, when Tim Berners-Lee (TBL) first
proposed the World-Wide Web (WWW, or "the web"). You can find the original
proposal, which is surprisingly short and non-technical,
[here](https://www.w3.org/Proposal.html).
From reading TBL's proposal it's clear that what he was after was some mechanism
for hosting information on his machine in such a way that others could find and
view it, without it needing to be explicitly sent to them. He includes the
following under the "Applications" header:
> The application of a universal hypertext system, once in place, will cover
> many areas such as document registration, on-line help, project documentation,
> news schemes and so on.
But out of such a humble scope grew one of the most powerful forces of the 21st
century. By the end of 1990 TBL had written the first HTML/HTTP browser and
server. By the end of 1994 sites like IMDB, Yahoo, and Bianca's Smut Shack were
live and being accessed by consumers. The web grew that fast.
In my view the characteristic of the web which catalyzed its adoption so quickly
was the place-ness of it. The web is not just a protocol for transferring
information, like email, but instead is a _place_ where that information lives.
Any one place could be freely linked to any other place, and so complex and
interesting relations could be formed between people and ideas. The
contributions people make on the web can reverberate farther than they would or
could in any other medium precisely because those contributions aren't tied to
some one-off event or a deteriorating piece of physical infrastructure, but are
instead given a home which is both permanent and everywhere.
The other advantage of the web, at the time, was its simplicity. HTML was so
simple it was basically human-readable. A basic HTTP server could be implemented
as a hobby project by anyone in any language. Hosting your own website was a
relatively straightforward task which anyone with a computer and an ISP could
undertake.
This was the environment early adopters of the web found themselves in.
## Web 2.0
The infamous dot-com boom took place in 2001. I don't believe this was a failure
inherent in the principles of the web itself, but instead was a product of
people investing in a technology they didn't fully understand. The web, as it
was then, wasn't really designed with money-making in mind. It certainly allowed
for it, but that wasn't the use-case being addressed.
But of course, in this world we live in, if there's money to be made, it will
certainly be made.
By 2003 the phrase "Web 2.0" started popping up. I remember this. To me "Web
2.0" meant a new aesthetic on the web, complete with bubble buttons and centered
fix-width paragraph boxes. But what "Web 2.0" actually signified wasn't related
to any new technology or aesthetic. It was a new strategy for how companies
could enable use of the web by non-expert users, i.e. users who don't have the
inclination or means to host their own website. Web 2.0 was a strategy for
giving everyone a _place_ of their own on the web.
"Web 2.0" was merely a label given to a movement which had already been in
motion for years. I think the following Wikipedia excerpt describes this period
best:
> In 2004, the term ["Web 2.0"] began its rise in popularity when O'Reilly Media
and MediaLive hosted the first Web 2.0 conference. In their opening remarks,
John Battelle and Tim O'Reilly outlined their definition of the "Web as
Platform", where software applications are built upon the Web as opposed to upon
the desktop. The unique aspect of this migration, they argued, is that
"customers are building your business for you". They argued that the
activities of users generating content (in the form of ideas, text, videos, or
pictures) could be "harnessed" to create value.
In other words, Web 2.0 turned the place-ness of the web into a commodity.
Rather than expect everyone to host, or arrange for the hosting, of their own
corner of the web, the technologists would do it for them for "free"! This
coincided with the increasing complexity of the underlying technology of the
web; websites grew to be flashy, interactive, and stateful applications which
_did_ things rather than be places which _held_ things. The idea of a hyperlink,
upon which the success of the web had been founded, became merely an
implementation detail.
And so the walled gardens began to be built. Myspace was founded in 2003,
Facebook opened to the public in 2006, Digg (the precursor to reddit) was
launched in 2004, Flickr launched in 2004 (and was bought by Yahoo in 2005),
Google bought Blogger in 2003, and Twitter launched in 2006. In effect this
period both opened the web up to everyone and established the way we still use
it today.
It's upon these foundations that current events unfold. We have platforms whose
only incentive is towards capturing new users and holding their attention, to
the exclusion of other platforms, so they can be advertised to. Users are
enticed in because they are being offered a place on the web, a place of their
own to express themselves from, in order to find out the worth of their
expressions to the rest of the world. But they aren't expressing to the world at
large, they are expressing to a social media platform, a business, and so only
the most lucrative of voices are heard.
So much for not wanting to talk about social media.
## Web 3.0
The new hot topic in crypto and hacker circles is "Web 3.0", or the
decentralized web (dweb). The idea is that we can have all the good of the
current web (the accessibility, utility, permanency, etc) without all the bad
(the centralized platforms, censorship, advertising, etc). The way forward to
this utopian dream is by building decentralized applications (dApps).
dApps are constructed in a way where all the users of an application help to
host all the stateful content of that application. If I, as a user, post an
image to a dApp, the idea is that other users of that same dApp would lend their
meager computer resources to ensure my image is never forgotten, and in turn I
would lend mine for theirs.
In practice building successful dApps is enormously difficult for many reasons,
and really I'm not sure there _are_ any successful ones (to date). While I
support the general sentiment behind them, I sometimes wonder about the
efficacy. What people want from the web is a place they can call their own, a
place from which they can express themselves and share their contributions with
others with all the speed and pervasiveness that the internet offers. A dApp is
just another walled garden with specific capabilities; it offers only free
hosting, not free expression.
## Web 2.0b
I'm not here solely to complain (just mostly).
Thinking back to Web 1.0, and specifically to the turning point between 1.0 and
2.0, I'd like to propose that maybe we made a wrong turn. The issue at hand was
that hosting one's own site was still too much of a technical burden, and the
direction we went was towards having businesses host them for us. Perhaps there
was another way.
What are the specific difficulties with hosting one's own site? Here are the
ones I can think of:
* Bad tooling: basically none of the tools you're required to use (web server,
TLS, DNS, your home router) are designed for the average person.
* Aggregiously complex languages: making a site which looks half decent and can
do the things you want requires a _lot_ of knowledge about the underlying
languages (CSS, HTML, Javascript, and whatever your server is written in).
* Single point-of-failure: if your machine is off, your site is down.
* Security: it's important to stay ahead of the hackers, but it takes time to
do so.
* Hostile environment: this is separate from security, and includes difficulties
like dynamic home IPs and bad ISP policies (such as asymetric upload/download
speeds).
These are each separate avenues of attack.
Bad tooling is a result of the fact that devs generally build technology for
themselves or their fellow devs, and only build for others when they're being
paid to do it. This is merely an attitude problem.
Complex languages are really a sub-category of bad tooling. The concesus seems
to be that the average person isn't interested or capable of working in
HTML/CSS/JS. This may be true today, but it wasn't always. Most of my friends in
middle and high school were well within their interest and capability to create
the most heinous MySpace pages the world has ever seen, using nothing but CSS
generators and scraps of shitty JS they found lying around. So what changed? The
tools we use to build those pages did.
A hostile environment is not something any individual can do anything about, but
in the capitalist system we exist in we can at least hold in faith the idea that
eventually us customers will get what we want. It may take a long time, but all
monopolies break eventually, and someone will eventually sell us the internet
access we're asking for. If all other pieces are in place I think we'll have
enough people asking to make a difference.
For single point-of-failure we have to grant that more than one person will be
involved, since the vast majority of people aren't going to be able to keep one
machine online consistently, let alone two or more machines. But I think we all
know at least one person who could keep a machine online with some reliability,
and they probably know a couple of other people who could do so as well. What
I'm proposing is that, rather than building tools for global decentralization,
we need tools for local decentralization, aka federation. We can make it
possible for a group of people to have their presence managed by a subset of
themselves. Those with the ability could help to host the online presence of
their family, friends, churches, etc, if given the right tools.
Security is the hard one, but also in many ways isn't. What most people want
from the web is a place from which to express themselves. Expression doesn't
take much more than a static page, usually, and there's not much attacking one
can do against a static page. Additionally, we've already established that
there's going to be at least a _couple_ of technically minded people involved in
hosting this thing.
So that's my idea that I'd like to build towards. First among these ideas is
that we need tools which can help people help each other host their content, and
on top of that foundation a new web can be built which values honest expression
rather than the lucrative madness which our current algorithms love so much.
This project was already somewhat started by
[Cryptorado](https://github.com/Cryptorado-Community/Cryptorado-Node) while I
was a regular attendee, but since COVID started my attendance has fallen off.
Hopefully one day it can resume. In the meantime I'm going to be working on
setting up these tools for myself, and see how far I can get.

View File

@ -1,314 +0,0 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"os"
"sort"
"strconv"
"sync"
"time"
)
// Logger describes a simple component used for printing log lines.
type Logger interface {
Printf(string, ...interface{})
}
////////////////////////////////////////////////////////////////////////////////
// The scoreboard component
// File wraps the standard os.File type.
type File interface {
io.ReadWriter
Truncate(int64) error
Seek(int64, int) (int64, error)
}
// scoreboard loads player scores from a save file, tracks score updates, and
// periodically saves those scores back to the save file.
type scoreboard struct {
file File
scoresM map[string]int
scoresLock sync.Mutex
// this field will only be set in tests, and is used to synchronize with the
// the for-select loop in saveLoop.
saveLoopWaitCh chan struct{}
}
// newScoreboard initializes a scoreboard using scores saved in the given File
// (which may be empty). The scoreboard will rewrite the save file with the
// latest scores everytime saveTicker is written to.
func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger) (*scoreboard, error) {
fileBody, err := ioutil.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("reading saved scored: %w", err)
}
scoresM := map[string]int{}
if len(fileBody) > 0 {
if err := json.Unmarshal(fileBody, &scoresM); err != nil {
return nil, fmt.Errorf("decoding saved scores: %w", err)
}
}
scoreboard := &scoreboard{
file: file,
scoresM: scoresM,
saveLoopWaitCh: make(chan struct{}),
}
go scoreboard.saveLoop(saveTicker, logger)
return scoreboard, nil
}
func (s *scoreboard) guessedCorrect(name string) int {
s.scoresLock.Lock()
defer s.scoresLock.Unlock()
s.scoresM[name] += 1000
return s.scoresM[name]
}
func (s *scoreboard) guessedIncorrect(name string) int {
s.scoresLock.Lock()
defer s.scoresLock.Unlock()
s.scoresM[name] -= 1
return s.scoresM[name]
}
func (s *scoreboard) scores() map[string]int {
s.scoresLock.Lock()
defer s.scoresLock.Unlock()
scoresCp := map[string]int{}
for name, score := range s.scoresM {
scoresCp[name] = score
}
return scoresCp
}
func (s *scoreboard) save() error {
scores := s.scores()
if _, err := s.file.Seek(0, 0); err != nil {
return fmt.Errorf("seeking to start of save file: %w", err)
} else if err := s.file.Truncate(0); err != nil {
return fmt.Errorf("truncating save file: %w", err)
} else if err := json.NewEncoder(s.file).Encode(scores); err != nil {
return fmt.Errorf("encoding scores to save file: %w", err)
}
return nil
}
func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) {
for {
select {
case <-ticker:
if err := s.save(); err != nil {
logger.Printf("error saving scoreboard to file: %v", err)
}
case <-s.saveLoopWaitCh:
// test will unblock, nothing to do here.
}
}
}
////////////////////////////////////////////////////////////////////////////////
// The httpHandlers component
// Scoreboard describes the scoreboard component from the point of view of the
// httpHandler component (which only needs a subset of scoreboard's methods).
type Scoreboard interface {
guessedCorrect(name string) int
guessedIncorrect(name string) int
scores() map[string]int
}
// RandSrc describes a randomness component which can produce random integers.
type RandSrc interface {
Int() int
}
// httpHandlers implements the http.HandlerFuncs used by the httpServer.
type httpHandlers struct {
scoreboard Scoreboard
randSrc RandSrc
logger Logger
mux *http.ServeMux
n int
nLock sync.Mutex
}
func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers {
n := randSrc.Int()
logger.Printf("first n is %v", n)
httpHandlers := &httpHandlers{
scoreboard: scoreboard,
randSrc: randSrc,
logger: logger,
mux: http.NewServeMux(),
n: n,
}
httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess)
httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores)
return httpHandlers
}
func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(rw, r)
}
func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "text/plain")
name := r.FormValue("name")
nStr := r.FormValue("n")
if name == "" || nStr == "" {
http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest)
return
}
n, err := strconv.Atoi(nStr)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
h.nLock.Lock()
defer h.nLock.Unlock()
if h.n == n {
newScore := h.scoreboard.guessedCorrect(name)
h.n = h.randSrc.Int()
h.logger.Printf("new n is %v", h.n)
rw.WriteHeader(http.StatusOK)
fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore)
return
}
hint := "higher"
if h.n < n {
hint = "lower"
}
newScore := h.scoreboard.guessedIncorrect(name)
rw.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore)
}
func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "text/plain")
h.nLock.Lock()
defer h.nLock.Unlock()
type scoreTup struct {
name string
score int
}
scores := h.scoreboard.scores()
scoresTups := make([]scoreTup, 0, len(scores))
for name, score := range scores {
scoresTups = append(scoresTups, scoreTup{name, score})
}
sort.Slice(scoresTups, func(i, j int) bool {
return scoresTups[i].score > scoresTups[j].score
})
for _, scoresTup := range scoresTups {
fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score)
}
}
////////////////////////////////////////////////////////////////////////////////
// The httpServer component.
type httpServer struct {
httpServer *http.Server
errCh chan error
}
func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer {
loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String())
httpHandlers.ServeHTTP(rw, r)
})
server := &httpServer{
httpServer: &http.Server{
Handler: loggingHandler,
},
errCh: make(chan error, 1),
}
go func() {
err := server.httpServer.Serve(listener)
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
server.errCh <- err
}()
return server
}
////////////////////////////////////////////////////////////////////////////////
// main
const (
saveFilePath = "./save.json"
listenAddr = ":8888"
saveInterval = 5 * time.Second
)
func main() {
logger := log.New(os.Stdout, "", log.LstdFlags)
logger.Printf("opening scoreboard save file %q", saveFilePath)
file, err := os.OpenFile(saveFilePath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
logger.Fatalf("failed to open file %q: %v", saveFilePath, err)
}
saveTicker := time.NewTicker(saveInterval)
randSrc := rand.New(rand.NewSource(time.Now().UnixNano()))
logger.Printf("initializing scoreboard")
scoreboard, err := newScoreboard(file, saveTicker.C, logger)
if err != nil {
logger.Fatalf("failed to initialize scoreboard: %v", err)
}
logger.Printf("listening on %q", listenAddr)
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
logger.Fatalf("failed to listen on %q: %v", listenAddr, err)
}
logger.Printf("setting up HTTP handlers")
httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger)
logger.Printf("serving HTTP requests")
newHTTPServer(listener, httpHandlers, logger)
logger.Printf("initialization done")
select {} // block forever
}

View File

@ -1,4 +0,0 @@
---
layout: code
include: main.go
---

View File

@ -1,167 +0,0 @@
package main
import (
"bytes"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"time"
)
type nullLogger struct{}
func (nullLogger) Printf(string, ...interface{}) {}
////////////////////////////////////////////////////////////////////////////////
// Test scoreboard component
type fileStub struct {
*bytes.Buffer
}
func newFileStub(init string) *fileStub {
return &fileStub{Buffer: bytes.NewBufferString(init)}
}
func (fs *fileStub) Truncate(i int64) error {
fs.Buffer.Truncate(int(i))
return nil
}
func (fs *fileStub) Seek(i int64, whence int) (int64, error) {
return i, nil
}
func TestScoreboard(t *testing.T) {
newScoreboard := func(t *testing.T, fileStub *fileStub, saveTicker <-chan time.Time) *scoreboard {
t.Helper()
scoreboard, err := newScoreboard(fileStub, saveTicker, nullLogger{})
if err != nil {
t.Errorf("unexpected error checking saved scored: %v", err)
}
return scoreboard
}
assertScores := func(t *testing.T, expScores, gotScores map[string]int) {
t.Helper()
if !reflect.DeepEqual(expScores, gotScores) {
t.Errorf("expected scores of %+v, but instead got %+v", expScores, gotScores)
}
}
assertSavedScores := func(t *testing.T, expScores map[string]int, fileStub *fileStub) {
t.Helper()
fileStubCp := newFileStub(fileStub.String())
tmpScoreboard := newScoreboard(t, fileStubCp, nil)
assertScores(t, expScores, tmpScoreboard.scores())
}
t.Run("loading", func(t *testing.T) {
// make sure loading scoreboards with various file contents works
assertSavedScores(t, map[string]int{}, newFileStub(""))
assertSavedScores(t, map[string]int{"foo": 1}, newFileStub(`{"foo":1}`))
assertSavedScores(t, map[string]int{"foo": 1, "bar": -2}, newFileStub(`{"foo":1,"bar":-2}`))
})
t.Run("tracking", func(t *testing.T) {
scoreboard := newScoreboard(t, newFileStub(""), nil)
assertScores(t, map[string]int{}, scoreboard.scores()) // sanity check
scoreboard.guessedCorrect("foo")
assertScores(t, map[string]int{"foo": 1000}, scoreboard.scores())
scoreboard.guessedIncorrect("bar")
assertScores(t, map[string]int{"foo": 1000, "bar": -1}, scoreboard.scores())
scoreboard.guessedIncorrect("foo")
assertScores(t, map[string]int{"foo": 999, "bar": -1}, scoreboard.scores())
})
t.Run("saving", func(t *testing.T) {
// this test tests scoreboard's periodic save feature using a ticker
// channel which will be written to manually. The saveLoopWaitCh is used
// here to ensure that each ticker has been fully processed.
ticker := make(chan time.Time)
fileStub := newFileStub("")
scoreboard := newScoreboard(t, fileStub, ticker)
tick := func() {
ticker <- time.Time{}
scoreboard.saveLoopWaitCh <- struct{}{}
}
// this should not effect the save file at first
scoreboard.guessedCorrect("foo")
assertSavedScores(t, map[string]int{}, fileStub)
// after the ticker the new score should get saved
tick()
assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
// ticker again after no changes should save the same thing as before
tick()
assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
// buffer a bunch of changes, shouldn't get saved till after tick
scoreboard.guessedCorrect("foo")
scoreboard.guessedCorrect("bar")
scoreboard.guessedCorrect("bar")
assertSavedScores(t, map[string]int{"foo": 1000}, fileStub)
tick()
assertSavedScores(t, map[string]int{"foo": 2000, "bar": 2000}, fileStub)
})
}
////////////////////////////////////////////////////////////////////////////////
// Test httpHandlers component
type mockScoreboard map[string]int
func (mockScoreboard) guessedCorrect(name string) int { return 1 }
func (mockScoreboard) guessedIncorrect(name string) int { return -1 }
func (m mockScoreboard) scores() map[string]int { return m }
type mockRandSrc struct{}
func (m mockRandSrc) Int() int { return 666 }
func TestHTTPHandlers(t *testing.T) {
mockScoreboard := mockScoreboard{"foo": 1, "bar": 2}
httpHandlers := newHTTPHandlers(mockScoreboard, mockRandSrc{}, nullLogger{})
assertRequest := func(t *testing.T, expCode int, expBody string, r *http.Request) {
t.Helper()
rw := httptest.NewRecorder()
httpHandlers.ServeHTTP(rw, r)
if rw.Code != expCode {
t.Errorf("expected HTTP response code %d, got %d", expCode, rw.Code)
} else if rw.Body.String() != expBody {
t.Errorf("expected HTTP response body %q, got %q", expBody, rw.Body.String())
}
}
r := httptest.NewRequest("GET", "/guess?name=foo&n=665", nil)
assertRequest(t, 400, "Try higher. Your score is now -1\n", r)
r = httptest.NewRequest("GET", "/guess?name=foo&n=667", nil)
assertRequest(t, 400, "Try lower. Your score is now -1\n", r)
r = httptest.NewRequest("GET", "/guess?name=foo&n=666", nil)
assertRequest(t, 200, "Correct! Your score is now 1\n", r)
r = httptest.NewRequest("GET", "/scores", nil)
assertRequest(t, 200, "bar: 2\nfoo: 1\n", r)
}
////////////////////////////////////////////////////////////////////////////////
//
// httpServer is NOT tested, for the following reasons:
// * It depends on a `net.Listener`, which is not trivial to mock.
// * It does very little besides passing an httpHandlers along to an http.Server
// and managing cleanup.
// * It isn't likely to be changed often.
// * If it were to break it would be very apparent in subsequent testing stages.
//

View File

@ -1,4 +0,0 @@
---
layout: code
include: main_test.go
---

View File

@ -1,320 +0,0 @@
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"os"
"sort"
"strconv"
"sync"
"time"
)
// Logger describes a simple component used for printing log lines.
type Logger interface {
Printf(string, ...interface{})
}
////////////////////////////////////////////////////////////////////////////////
// The scoreboard component
// File wraps the standard os.File type.
type File interface {
io.ReadWriter
Truncate(int64) error
Seek(int64, int) (int64, error)
}
// scoreboard loads player scores from a save file, tracks score updates, and
// periodically saves those scores back to the save file.
type scoreboard struct {
file File
scoresM map[string]int
scoresLock sync.Mutex
pointsOnCorrect, pointsOnIncorrect int
// this field will only be set in tests, and is used to synchronize with the
// the for-select loop in saveLoop.
saveLoopWaitCh chan struct{}
}
// newScoreboard initializes a scoreboard using scores saved in the given File
// (which may be empty). The scoreboard will rewrite the save file with the
// latest scores everytime saveTicker is written to.
func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger, pointsOnCorrect, pointsOnIncorrect int) (*scoreboard, error) {
fileBody, err := ioutil.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("reading saved scored: %w", err)
}
scoresM := map[string]int{}
if len(fileBody) > 0 {
if err := json.Unmarshal(fileBody, &scoresM); err != nil {
return nil, fmt.Errorf("decoding saved scores: %w", err)
}
}
scoreboard := &scoreboard{
file: file,
scoresM: scoresM,
pointsOnCorrect: pointsOnCorrect,
pointsOnIncorrect: pointsOnIncorrect,
saveLoopWaitCh: make(chan struct{}),
}
go scoreboard.saveLoop(saveTicker, logger)
return scoreboard, nil
}
func (s *scoreboard) guessedCorrect(name string) int {
s.scoresLock.Lock()
defer s.scoresLock.Unlock()
s.scoresM[name] += s.pointsOnCorrect
return s.scoresM[name]
}
func (s *scoreboard) guessedIncorrect(name string) int {
s.scoresLock.Lock()
defer s.scoresLock.Unlock()
s.scoresM[name] += s.pointsOnIncorrect
return s.scoresM[name]
}
func (s *scoreboard) scores() map[string]int {
s.scoresLock.Lock()
defer s.scoresLock.Unlock()
scoresCp := map[string]int{}
for name, score := range s.scoresM {
scoresCp[name] = score
}
return scoresCp
}
func (s *scoreboard) save() error {
scores := s.scores()
if _, err := s.file.Seek(0, 0); err != nil {
return fmt.Errorf("seeking to start of save file: %w", err)
} else if err := s.file.Truncate(0); err != nil {
return fmt.Errorf("truncating save file: %w", err)
} else if err := json.NewEncoder(s.file).Encode(scores); err != nil {
return fmt.Errorf("encoding scores to save file: %w", err)
}
return nil
}
func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) {
for {
select {
case <-ticker:
if err := s.save(); err != nil {
logger.Printf("error saving scoreboard to file: %v", err)
}
case <-s.saveLoopWaitCh:
// test will unblock, nothing to do here.
}
}
}
////////////////////////////////////////////////////////////////////////////////
// The httpHandlers component
// Scoreboard describes the scoreboard component from the point of view of the
// httpHandler component (which only needs a subset of scoreboard's methods).
type Scoreboard interface {
guessedCorrect(name string) int
guessedIncorrect(name string) int
scores() map[string]int
}
// RandSrc describes a randomness component which can produce random integers.
type RandSrc interface {
Int() int
}
// httpHandlers implements the http.HandlerFuncs used by the httpServer.
type httpHandlers struct {
scoreboard Scoreboard
randSrc RandSrc
logger Logger
mux *http.ServeMux
n int
nLock sync.Mutex
}
func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers {
n := randSrc.Int()
logger.Printf("first n is %v", n)
httpHandlers := &httpHandlers{
scoreboard: scoreboard,
randSrc: randSrc,
logger: logger,
mux: http.NewServeMux(),
n: n,
}
httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess)
httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores)
return httpHandlers
}
func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(rw, r)
}
func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "text/plain")
name := r.FormValue("name")
nStr := r.FormValue("n")
if name == "" || nStr == "" {
http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest)
return
}
n, err := strconv.Atoi(nStr)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
h.nLock.Lock()
defer h.nLock.Unlock()
if h.n == n {
newScore := h.scoreboard.guessedCorrect(name)
h.n = h.randSrc.Int()
h.logger.Printf("new n is %v", h.n)
rw.WriteHeader(http.StatusOK)
fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore)
return
}
hint := "higher"
if h.n < n {
hint = "lower"
}
newScore := h.scoreboard.guessedIncorrect(name)
rw.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore)
}
func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "text/plain")
h.nLock.Lock()
defer h.nLock.Unlock()
type scoreTup struct {
name string
score int
}
scores := h.scoreboard.scores()
scoresTups := make([]scoreTup, 0, len(scores))
for name, score := range scores {
scoresTups = append(scoresTups, scoreTup{name, score})
}
sort.Slice(scoresTups, func(i, j int) bool {
return scoresTups[i].score > scoresTups[j].score
})
for _, scoresTup := range scoresTups {
fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score)
}
}
////////////////////////////////////////////////////////////////////////////////
// The httpServer component.
type httpServer struct {
httpServer *http.Server
errCh chan error
}
func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer {
loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String())
httpHandlers.ServeHTTP(rw, r)
})
server := &httpServer{
httpServer: &http.Server{
Handler: loggingHandler,
},
errCh: make(chan error, 1),
}
go func() {
err := server.httpServer.Serve(listener)
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
server.errCh <- err
}()
return server
}
////////////////////////////////////////////////////////////////////////////////
// main
func main() {
saveFilePath := flag.String("save-file", "./save.json", "File used to save scores")
listenAddr := flag.String("listen-addr", ":8888", "Address to listen for HTTP requests on")
saveInterval := flag.Duration("save-interval", 5*time.Second, "How often to resave scores")
pointsOnCorrect := flag.Int("points-on-correct", 1000, "Amount to change a user's score by upon a correct score")
pointsOnIncorrect := flag.Int("points-on-incorrect", -1, "Amount to change a user's score by upon an incorrect score")
flag.Parse()
logger := log.New(os.Stdout, "", log.LstdFlags)
logger.Printf("opening scoreboard save file %q", *saveFilePath)
file, err := os.OpenFile(*saveFilePath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
logger.Fatalf("failed to open file %q: %v", *saveFilePath, err)
}
saveTicker := time.NewTicker(*saveInterval)
randSrc := rand.New(rand.NewSource(time.Now().UnixNano()))
logger.Printf("initializing scoreboard")
scoreboard, err := newScoreboard(file, saveTicker.C, logger, *pointsOnCorrect, *pointsOnIncorrect)
if err != nil {
logger.Fatalf("failed to initialize scoreboard: %v", err)
}
logger.Printf("listening on %q", *listenAddr)
listener, err := net.Listen("tcp", *listenAddr)
if err != nil {
logger.Fatalf("failed to listen on %q: %v", *listenAddr, err)
}
logger.Printf("setting up HTTP handlers")
httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger)
logger.Printf("serving HTTP requests")
newHTTPServer(listener, httpHandlers, logger)
logger.Printf("initialization done")
select {} // block forever
}

View File

@ -1,4 +0,0 @@
---
layout: code
include: main.go
---

View File

@ -1,390 +0,0 @@
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"os"
"os/signal"
"sort"
"strconv"
"sync"
"time"
)
// Logger describes a simple component used for printing log lines.
type Logger interface {
Printf(string, ...interface{})
}
////////////////////////////////////////////////////////////////////////////////
// The scoreboard component
// File wraps the standard os.File type.
type File interface {
io.ReadWriter
Truncate(int64) error
Seek(int64, int) (int64, error)
}
// scoreboard loads player scores from a save file, tracks score updates, and
// periodically saves those scores back to the save file.
type scoreboard struct {
file File
scoresM map[string]int
scoresLock sync.Mutex
pointsOnCorrect, pointsOnIncorrect int
// The cleanup method closes cleanupCh to signal to all scoreboard's running
// go-routines to clean themselves up, and cleanupWG is then used to wait
// for those goroutines to do so.
cleanupCh chan struct{}
cleanupWG sync.WaitGroup
// this field will only be set in tests, and is used to synchronize with the
// the for-select loop in saveLoop.
saveLoopWaitCh chan struct{}
}
// newScoreboard initializes a scoreboard using scores saved in the given File
// (which may be empty). The scoreboard will rewrite the save file with the
// latest scores everytime saveTicker is written to.
func newScoreboard(file File, saveTicker <-chan time.Time, logger Logger, pointsOnCorrect, pointsOnIncorrect int) (*scoreboard, error) {
fileBody, err := ioutil.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("reading saved scored: %w", err)
}
scoresM := map[string]int{}
if len(fileBody) > 0 {
if err := json.Unmarshal(fileBody, &scoresM); err != nil {
return nil, fmt.Errorf("decoding saved scores: %w", err)
}
}
scoreboard := &scoreboard{
file: file,
scoresM: scoresM,
pointsOnCorrect: pointsOnCorrect,
pointsOnIncorrect: pointsOnIncorrect,
cleanupCh: make(chan struct{}),
saveLoopWaitCh: make(chan struct{}),
}
scoreboard.cleanupWG.Add(1)
go func() {
scoreboard.saveLoop(saveTicker, logger)
scoreboard.cleanupWG.Done()
}()
return scoreboard, nil
}
func (s *scoreboard) cleanup() error {
close(s.cleanupCh)
s.cleanupWG.Wait()
if err := s.save(); err != nil {
return fmt.Errorf("saving scores during cleanup: %w", err)
}
return nil
}
func (s *scoreboard) guessedCorrect(name string) int {
s.scoresLock.Lock()
defer s.scoresLock.Unlock()
s.scoresM[name] += s.pointsOnCorrect
return s.scoresM[name]
}
func (s *scoreboard) guessedIncorrect(name string) int {
s.scoresLock.Lock()
defer s.scoresLock.Unlock()
s.scoresM[name] += s.pointsOnIncorrect
return s.scoresM[name]
}
func (s *scoreboard) scores() map[string]int {
s.scoresLock.Lock()
defer s.scoresLock.Unlock()
scoresCp := map[string]int{}
for name, score := range s.scoresM {
scoresCp[name] = score
}
return scoresCp
}
func (s *scoreboard) save() error {
scores := s.scores()
if _, err := s.file.Seek(0, 0); err != nil {
return fmt.Errorf("seeking to start of save file: %w", err)
} else if err := s.file.Truncate(0); err != nil {
return fmt.Errorf("truncating save file: %w", err)
} else if err := json.NewEncoder(s.file).Encode(scores); err != nil {
return fmt.Errorf("encoding scores to save file: %w", err)
}
return nil
}
func (s *scoreboard) saveLoop(ticker <-chan time.Time, logger Logger) {
for {
select {
case <-ticker:
if err := s.save(); err != nil {
logger.Printf("error saving scoreboard to file: %v", err)
}
case <-s.cleanupCh:
return
case <-s.saveLoopWaitCh:
// test will unblock, nothing to do here.
}
}
}
////////////////////////////////////////////////////////////////////////////////
// The httpHandlers component
// Scoreboard describes the scoreboard component from the point of view of the
// httpHandler component (which only needs a subset of scoreboard's methods).
type Scoreboard interface {
guessedCorrect(name string) int
guessedIncorrect(name string) int
scores() map[string]int
}
// RandSrc describes a randomness component which can produce random integers.
type RandSrc interface {
Int() int
}
// httpHandlers implements the http.HandlerFuncs used by the httpServer.
type httpHandlers struct {
scoreboard Scoreboard
randSrc RandSrc
logger Logger
mux *http.ServeMux
n int
nLock sync.Mutex
}
func newHTTPHandlers(scoreboard Scoreboard, randSrc RandSrc, logger Logger) *httpHandlers {
n := randSrc.Int()
logger.Printf("first n is %v", n)
httpHandlers := &httpHandlers{
scoreboard: scoreboard,
randSrc: randSrc,
logger: logger,
mux: http.NewServeMux(),
n: n,
}
httpHandlers.mux.HandleFunc("/guess", httpHandlers.handleGuess)
httpHandlers.mux.HandleFunc("/scores", httpHandlers.handleScores)
return httpHandlers
}
func (h *httpHandlers) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
h.mux.ServeHTTP(rw, r)
}
func (h *httpHandlers) handleGuess(rw http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "text/plain")
name := r.FormValue("name")
nStr := r.FormValue("n")
if name == "" || nStr == "" {
http.Error(rw, `"name" and "n" GET args are required`, http.StatusBadRequest)
return
}
n, err := strconv.Atoi(nStr)
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
h.nLock.Lock()
defer h.nLock.Unlock()
if h.n == n {
newScore := h.scoreboard.guessedCorrect(name)
h.n = h.randSrc.Int()
h.logger.Printf("new n is %v", h.n)
rw.WriteHeader(http.StatusOK)
fmt.Fprintf(rw, "Correct! Your score is now %d\n", newScore)
return
}
hint := "higher"
if h.n < n {
hint = "lower"
}
newScore := h.scoreboard.guessedIncorrect(name)
rw.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(rw, "Try %s. Your score is now %d\n", hint, newScore)
}
func (h *httpHandlers) handleScores(rw http.ResponseWriter, r *http.Request) {
r.Header.Set("Content-Type", "text/plain")
h.nLock.Lock()
defer h.nLock.Unlock()
type scoreTup struct {
name string
score int
}
scores := h.scoreboard.scores()
scoresTups := make([]scoreTup, 0, len(scores))
for name, score := range scores {
scoresTups = append(scoresTups, scoreTup{name, score})
}
sort.Slice(scoresTups, func(i, j int) bool {
return scoresTups[i].score > scoresTups[j].score
})
for _, scoresTup := range scoresTups {
fmt.Fprintf(rw, "%s: %d\n", scoresTup.name, scoresTup.score)
}
}
////////////////////////////////////////////////////////////////////////////////
// The httpServer component.
type httpServer struct {
httpServer *http.Server
errCh chan error
}
func newHTTPServer(listener net.Listener, httpHandlers *httpHandlers, logger Logger) *httpServer {
loggingHandler := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
logger.Printf("HTTP request -> %s %s %s", ip, r.Method, r.URL.String())
httpHandlers.ServeHTTP(rw, r)
})
server := &httpServer{
httpServer: &http.Server{
Handler: loggingHandler,
},
errCh: make(chan error, 1),
}
go func() {
err := server.httpServer.Serve(listener)
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
server.errCh <- err
}()
return server
}
func (s *httpServer) cleanup() error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := s.httpServer.Shutdown(ctx); err != nil {
return fmt.Errorf("shutting down http server: %w", err)
}
return <-s.errCh
}
////////////////////////////////////////////////////////////////////////////////
// main
func main() {
saveFilePath := flag.String("save-file", "./save.json", "File used to save scores")
listenAddr := flag.String("listen-addr", ":8888", "Address to listen for HTTP requests on")
saveInterval := flag.Duration("save-interval", 5*time.Second, "How often to resave scores")
pointsOnCorrect := flag.Int("points-on-correct", 1000, "Amount to change a user's score by upon a correct score")
pointsOnIncorrect := flag.Int("points-on-incorrect", -1, "Amount to change a user's score by upon an incorrect score")
flag.Parse()
logger := log.New(os.Stdout, "", log.LstdFlags)
logger.Printf("opening scoreboard save file %q", *saveFilePath)
file, err := os.OpenFile(*saveFilePath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
logger.Fatalf("failed to open file %q: %v", *saveFilePath, err)
}
saveTicker := time.NewTicker(*saveInterval)
randSrc := rand.New(rand.NewSource(time.Now().UnixNano()))
logger.Printf("initializing scoreboard")
scoreboard, err := newScoreboard(file, saveTicker.C, logger, *pointsOnCorrect, *pointsOnIncorrect)
if err != nil {
logger.Fatalf("failed to initialize scoreboard: %v", err)
}
logger.Printf("listening on %q", *listenAddr)
listener, err := net.Listen("tcp", *listenAddr)
if err != nil {
logger.Fatalf("failed to listen on %q: %v", *listenAddr, err)
}
logger.Printf("setting up HTTP handlers")
httpHandlers := newHTTPHandlers(scoreboard, randSrc, logger)
logger.Printf("serving HTTP requests")
httpServer := newHTTPServer(listener, httpHandlers, logger)
logger.Printf("initialization done, waiting for interrupt signal")
sigCh := make(chan os.Signal)
signal.Notify(sigCh, os.Interrupt)
<-sigCh
logger.Printf("interrupt signal received, cleaning up")
go func() {
<-sigCh
log.Fatalf("interrupt signal received again, forcing shutdown")
}()
if err := httpServer.cleanup(); err != nil {
logger.Fatalf("cleaning up http server: %v", err)
}
// NOTE go's builtin http server does not follow component property 5a, and
// instead closes the net.Listener given to it as a parameter when Shutdown
// is called. Because of that inconsistency this Close would error if it
// were called.
//
// While there are ways to work around this, it's instead highlighted in
// this example as an instance of a language making the component-oriented
// pattern more difficult.
//
//if err := listener.Close(); err != nil {
// logger.Fatalf("closing listener %q: %v", listenAddr, err)
//}
if err := scoreboard.cleanup(); err != nil {
logger.Fatalf("cleaning up scoreboard: %v", err)
}
saveTicker.Stop()
if err := file.Close(); err != nil {
logger.Fatalf("closing file %q: %v", *saveFilePath, err)
}
os.Stdout.Sync()
}

View File

@ -1,4 +0,0 @@
---
layout: code
include: main.go
---

View File

@ -1,69 +0,0 @@
.highlight .hll { background-color: #ffffcc }
.highlight { background: #f0f0f0; }
.highlight .c { color: #60a0b0; font-style: italic } /* Comment */
.highlight .err { border: 1px solid #FF0000 } /* Error */
.highlight .k { color: #007020; font-weight: bold } /* Keyword */
.highlight .o { color: #666666 } /* Operator */
.highlight .ch { color: #60a0b0; font-style: italic } /* Comment.Hashbang */
.highlight .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */
.highlight .cp { color: #007020 } /* Comment.Preproc */
.highlight .cpf { color: #60a0b0; font-style: italic } /* Comment.PreprocFile */
.highlight .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */
.highlight .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #A00000 } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #FF0000 } /* Generic.Error */
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.highlight .gi { color: #00A000 } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.highlight .gt { color: #0044DD } /* Generic.Traceback */
.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #007020 } /* Keyword.Pseudo */
.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #902000 } /* Keyword.Type */
.highlight .m { color: #40a070 } /* Literal.Number */
.highlight .s { color: #4070a0 } /* Literal.String */
.highlight .na { color: #4070a0 } /* Name.Attribute */
.highlight .nb { color: #007020 } /* Name.Builtin */
.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */
.highlight .no { color: #60add5 } /* Name.Constant */
.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */
.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */
.highlight .ne { color: #007020 } /* Name.Exception */
.highlight .nf { color: #06287e } /* Name.Function */
.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */
.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #bb60d5 } /* Name.Variable */
.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #40a070 } /* Literal.Number.Bin */
.highlight .mf { color: #40a070 } /* Literal.Number.Float */
.highlight .mh { color: #40a070 } /* Literal.Number.Hex */
.highlight .mi { color: #40a070 } /* Literal.Number.Integer */
.highlight .mo { color: #40a070 } /* Literal.Number.Oct */
.highlight .sa { color: #4070a0 } /* Literal.String.Affix */
.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */
.highlight .sc { color: #4070a0 } /* Literal.String.Char */
.highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */
.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */
.highlight .s2 { color: #4070a0 } /* Literal.String.Double */
.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */
.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */
.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */
.highlight .sx { color: #c65d09 } /* Literal.String.Other */
.highlight .sr { color: #235388 } /* Literal.String.Regex */
.highlight .s1 { color: #4070a0 } /* Literal.String.Single */
.highlight .ss { color: #517918 } /* Literal.String.Symbol */
.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #06287e } /* Name.Function.Magic */
.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */
.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */
.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */
.highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */
.highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */

View File

@ -1,137 +0,0 @@
ul {
list-style: circle;
}
.light {
color: #666;
}
/* when in doubt, important it out */
.light a {
color: #666 !important;
}
.light a:hover {
color: #222 !important;
}
/* for making all content within a row be vertically centered
.row-vertically-centered .columns {
display: inline-block;
float: none;
margin-left: 0;
vertical-align: middle;
}
*/
#title-header {
border-bottom: 1px solid #666;
padding-top: 2rem;
padding-bottom: 2rem;
margin-bottom: 4rem;
}
#title-header .title {
margin: 0;
font-size: 4.4rem; /* magic number yooooo */
}
#title-header .title a {
color: #222;
text-decoration: none;
}
#title-header .social span {
margin-right: 0.5em;
white-space: nowrap;
}
#title-header .social .author-icons a {
margin-right: 0.5em;
}
#title-header .social a {
text-decoration: none;
}
footer {
margin-top: 4rem;
border-top: 1px solid #666;
padding-top: 2rem;
margin-bottom: 50vh;
}
#posts-list {
list-style: none;
}
#posts-list h2 {
margin: 0;
}
#posts-list h2 a {
color: #222;
text-decoration: none;
}
#post-header {
margin-top: 2rem;
margin-bottom: 2rem;
}
#post-headline {
margin: 0;
font-weight: bold;
}
#post-content ul {
margin-left: 1em;
}
#post-content img,
#post-content canvas
{
max-width: 100%;
}
/* The Modal (background) */
#modal {
display: none; /* Hidden by default */
position: fixed; /* Stay in place */
z-index: 1; /* Sit on top */
left: 0;
top: 0;
width: 100%; /* Full width */
height: 100%; /* Full height */
background-color: rgb(0,0,0); /* Fallback color */
background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}
/* Modal Content/Box */
#modal-body {
position: relative;
background-color: #fefefe;
margin: 10rem auto; /* 15% from the top and centered */
padding: 5rem;
border: 1px solid #888;
max-width: 30%; /* Could be more or less, depending on screen size */
}
/* The Close Button */
#modal-close {
position: absolute;
top: 1rem;
right: 1rem;
line-height: 1rem;
color: #aaa;
font-size: 28px;
font-weight: bold;
}
#modal-close:hover,
#modal-close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}

View File

@ -1,48 +0,0 @@
// showModal will create the modal structure the first time it is called.
var modal, modalContent;
const showModal = function() {
if (!modal) {
// make the modal
const modalClose = document.createElement('span');
modalClose.id = 'modal-close';
modalClose.innerHTML = '&times;';
modalContent = document.createElement('div');
modalContent.id = 'modal-content';
const modalBody = document.createElement('div');
modalBody.id = 'modal-body';
modalBody.appendChild(modalContent);
modalBody.appendChild(modalClose);
modal = document.createElement('div');
modal.id = 'modal';
modal.appendChild(modalBody);
// add the modal to the document
document.getElementsByTagName('body')[0].appendChild(modal);
// setup modal functionality
modalClose.onclick = function() {
modal.style.display = "none";
}
}
modalContent.innerHTML = '';
for (var i = 0; i < arguments.length; i++) {
modalContent.appendChild(arguments[i]);
}
modal.style.display = "block";
// When the user clicks anywhere outside of the modal, close it
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = "none";
window.onclick = undefined;
}
}
}
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM loaded");
})

427
assets/normalize.css vendored
View File

@ -1,427 +0,0 @@
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
/**
* 1. Set default font family to sans-serif.
* 2. Prevent iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-ms-text-size-adjust: 100%; /* 2 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/**
* Remove default margin.
*/
body {
margin: 0;
}
/* HTML5 display definitions
========================================================================== */
/**
* Correct `block` display not defined for any HTML5 element in IE 8/9.
* Correct `block` display not defined for `details` or `summary` in IE 10/11
* and Firefox.
* Correct `block` display not defined for `main` in IE 11.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
main,
menu,
nav,
section,
summary {
display: block;
}
/**
* 1. Correct `inline-block` display not defined in IE 8/9.
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
*/
audio,
canvas,
progress,
video {
display: inline-block; /* 1 */
vertical-align: baseline; /* 2 */
}
/**
* Prevent modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/**
* Address `[hidden]` styling not present in IE 8/9/10.
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
*/
[hidden],
template {
display: none;
}
/* Links
========================================================================== */
/**
* Remove the gray background color from active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* Improve readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* Text-level semantics
========================================================================== */
/**
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/**
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/**
* Address styling not present in Safari and Chrome.
*/
dfn {
font-style: italic;
}
/**
* Address variable `h1` font-size and margin within `section` and `article`
* contexts in Firefox 4+, Safari, and Chrome.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/**
* Address styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/**
* Address inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* Embedded content
========================================================================== */
/**
* Remove border when inside `a` element in IE 8/9/10.
*/
img {
border: 0;
}
/**
* Correct overflow not hidden in IE 9/10/11.
*/
svg:not(:root) {
overflow: hidden;
}
/* Grouping content
========================================================================== */
/**
* Address margin not present in IE 8/9 and Safari.
*/
figure {
margin: 1em 40px;
}
/**
* Address differences between Firefox and other browsers.
*/
hr {
-moz-box-sizing: content-box;
box-sizing: content-box;
height: 0;
}
/**
* Contain overflow in all browsers.
*/
pre {
overflow: auto;
}
/**
* Address odd `em`-unit font size rendering in all browsers.
*/
code,
kbd,
pre,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
/* Forms
========================================================================== */
/**
* Known limitation: by default, Chrome and Safari on OS X allow very limited
* styling of `select`, unless a `border` property is set.
*/
/**
* 1. Correct color not being inherited.
* Known issue: affects color of disabled elements.
* 2. Correct font properties not being inherited.
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
*/
button,
input,
optgroup,
select,
textarea {
color: inherit; /* 1 */
font: inherit; /* 2 */
margin: 0; /* 3 */
}
/**
* Address `overflow` set to `hidden` in IE 8/9/10/11.
*/
button {
overflow: visible;
}
/**
* Address inconsistent `text-transform` inheritance for `button` and `select`.
* All other form control elements do not inherit `text-transform` values.
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
* Correct `select` style inheritance in Firefox.
*/
button,
select {
text-transform: none;
}
/**
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Correct inability to style clickable `input` types in iOS.
* 3. Improve usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/**
* Re-set default cursor for disabled elements.
*/
button[disabled],
html input[disabled] {
cursor: default;
}
/**
* Remove inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/**
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
input {
line-height: normal;
}
/**
* It's recommended that you don't attempt to style these elements.
* Firefox's implementation doesn't respect box-sizing, padding, or width.
*
* 1. Address box sizing set to `content-box` in IE 8/9/10.
* 2. Remove excess padding in IE 8/9/10.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
* `font-size` values of the `input`, it causes the cursor style of the
* decrement button to change from `default` to `text`.
*/
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/**
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
* Safari (but not Chrome) clips the cancel button when the search input has
* padding (and `textfield` appearance).
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/**
* 1. Correct `color` not being inherited in IE 8/9/10/11.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/**
* Remove default vertical scrollbar in IE 8/9/10/11.
*/
textarea {
overflow: auto;
}
/**
* Don't inherit the `font-weight` (applied by a rule above).
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
*/
optgroup {
font-weight: bold;
}
/* Tables
========================================================================== */
/**
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}
td,
th {
padding: 0;
}

File diff suppressed because one or more lines are too long

418
assets/skeleton.css vendored
View File

@ -1,418 +0,0 @@
/*
* Skeleton V2.0.4
* Copyright 2014, Dave Gamache
* www.getskeleton.com
* Free to use under the MIT license.
* http://www.opensource.org/licenses/mit-license.php
* 12/29/2014
*/
/* Table of contents
- Grid
- Base Styles
- Typography
- Links
- Buttons
- Forms
- Lists
- Code
- Tables
- Spacing
- Utilities
- Clearing
- Media Queries
*/
/* Grid
*/
.container {
position: relative;
width: 100%;
max-width: 960px;
margin: 0 auto;
padding: 0 20px;
box-sizing: border-box; }
.column,
.columns {
width: 100%;
float: left;
box-sizing: border-box; }
/* For devices larger than 400px */
@media (min-width: 400px) {
.container {
width: 85%;
padding: 0; }
}
/* For devices larger than 550px */
@media (min-width: 550px) {
.container {
width: 80%; }
.column,
.columns {
margin-left: 4%; }
.column:first-child,
.columns:first-child {
margin-left: 0; }
.one.column,
.one.columns { width: 4.66666666667%; }
.two.columns { width: 13.3333333333%; }
.three.columns { width: 22%; }
.four.columns { width: 30.6666666667%; }
.five.columns { width: 39.3333333333%; }
.six.columns { width: 48%; }
.seven.columns { width: 56.6666666667%; }
.eight.columns { width: 65.3333333333%; }
.nine.columns { width: 74.0%; }
.ten.columns { width: 82.6666666667%; }
.eleven.columns { width: 91.3333333333%; }
.twelve.columns { width: 100%; margin-left: 0; }
.one-third.column { width: 30.6666666667%; }
.two-thirds.column { width: 65.3333333333%; }
.one-half.column { width: 48%; }
/* Offsets */
.offset-by-one.column,
.offset-by-one.columns { margin-left: 8.66666666667%; }
.offset-by-two.column,
.offset-by-two.columns { margin-left: 17.3333333333%; }
.offset-by-three.column,
.offset-by-three.columns { margin-left: 26%; }
.offset-by-four.column,
.offset-by-four.columns { margin-left: 34.6666666667%; }
.offset-by-five.column,
.offset-by-five.columns { margin-left: 43.3333333333%; }
.offset-by-six.column,
.offset-by-six.columns { margin-left: 52%; }
.offset-by-seven.column,
.offset-by-seven.columns { margin-left: 60.6666666667%; }
.offset-by-eight.column,
.offset-by-eight.columns { margin-left: 69.3333333333%; }
.offset-by-nine.column,
.offset-by-nine.columns { margin-left: 78.0%; }
.offset-by-ten.column,
.offset-by-ten.columns { margin-left: 86.6666666667%; }
.offset-by-eleven.column,
.offset-by-eleven.columns { margin-left: 95.3333333333%; }
.offset-by-one-third.column,
.offset-by-one-third.columns { margin-left: 34.6666666667%; }
.offset-by-two-thirds.column,
.offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
.offset-by-one-half.column,
.offset-by-one-half.columns { margin-left: 52%; }
}
/* Base Styles
*/
/* NOTE
html is set to 62.5% so that all the REM measurements throughout Skeleton
are based on 10px sizing. So basically 1.5rem = 15px :) */
html {
font-size: 62.5%; }
body {
font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
line-height: 1.6;
font-weight: 400;
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #222; }
/* Typography
*/
h1, h2, h3, h4, h5, h6 {
margin-top: 0;
margin-bottom: 2rem;
font-weight: 300; }
h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
/* Larger than phablet */
@media (min-width: 550px) {
h1 { font-size: 5.0rem; }
h2 { font-size: 4.2rem; }
h3 { font-size: 3.6rem; }
h4 { font-size: 3.0rem; }
h5 { font-size: 2.4rem; }
h6 { font-size: 1.5rem; }
}
p {
margin-top: 0; }
/* Links
*/
a {
color: #1EAEDB; }
a:hover {
color: #0FA0CE; }
/* Buttons
*/
.button,
button,
input[type="submit"],
input[type="reset"],
input[type="button"] {
display: inline-block;
height: 38px;
padding: 0 30px;
color: #555;
text-align: center;
font-size: 11px;
font-weight: 600;
line-height: 38px;
letter-spacing: .1rem;
text-transform: uppercase;
text-decoration: none;
white-space: nowrap;
background-color: transparent;
border-radius: 4px;
border: 1px solid #bbb;
cursor: pointer;
box-sizing: border-box; }
.button:hover,
button:hover,
input[type="submit"]:hover,
input[type="reset"]:hover,
input[type="button"]:hover,
.button:focus,
button:focus,
input[type="submit"]:focus,
input[type="reset"]:focus,
input[type="button"]:focus {
color: #333;
border-color: #888;
outline: 0; }
.button.button-primary,
button.button-primary,
input[type="submit"].button-primary,
input[type="reset"].button-primary,
input[type="button"].button-primary {
color: #FFF;
background-color: #33C3F0;
border-color: #33C3F0; }
.button.button-primary:hover,
button.button-primary:hover,
input[type="submit"].button-primary:hover,
input[type="reset"].button-primary:hover,
input[type="button"].button-primary:hover,
.button.button-primary:focus,
button.button-primary:focus,
input[type="submit"].button-primary:focus,
input[type="reset"].button-primary:focus,
input[type="button"].button-primary:focus {
color: #FFF;
background-color: #1EAEDB;
border-color: #1EAEDB; }
/* Forms
*/
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea,
select {
height: 38px;
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
background-color: #fff;
border: 1px solid #D1D1D1;
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
/* Removes awkward default styles on some inputs for iOS */
input[type="email"],
input[type="number"],
input[type="search"],
input[type="text"],
input[type="tel"],
input[type="url"],
input[type="password"],
textarea {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none; }
textarea {
min-height: 65px;
padding-top: 6px;
padding-bottom: 6px; }
input[type="email"]:focus,
input[type="number"]:focus,
input[type="search"]:focus,
input[type="text"]:focus,
input[type="tel"]:focus,
input[type="url"]:focus,
input[type="password"]:focus,
textarea:focus,
select:focus {
border: 1px solid #33C3F0;
outline: 0; }
label,
legend {
display: block;
margin-bottom: .5rem;
font-weight: 600; }
fieldset {
padding: 0;
border-width: 0; }
input[type="checkbox"],
input[type="radio"] {
display: inline; }
label > .label-body {
display: inline-block;
margin-left: .5rem;
font-weight: normal; }
/* Lists
*/
ul {
list-style: circle inside; }
ol {
list-style: decimal inside; }
ol, ul {
padding-left: 0;
margin-top: 0; }
ul ul,
ul ol,
ol ol,
ol ul {
margin: 1.5rem 0 1.5rem 3rem;
font-size: 90%; }
li {
margin-bottom: 1rem; }
/* Code
*/
code {
/*padding: .2rem .5rem;*/
/*margin: 0 .2rem;*/
font-size: 90%;
white-space: nowrap;
/*background: #F1F1F1;
border: 1px solid #E1E1E1;*/
border-radius: 4px; }
pre > code {
display: block;
padding: 1rem 1.5rem;
white-space: pre; }
/* Tables
*/
th,
td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #E1E1E1; }
th:first-child,
td:first-child {
padding-left: 0; }
th:last-child,
td:last-child {
padding-right: 0; }
/* Spacing
*/
button,
.button {
margin-bottom: 1rem; }
input,
textarea,
select,
fieldset {
margin-bottom: 1.5rem; }
pre,
blockquote,
dl,
figure,
table,
p,
ul,
ol,
form {
margin-bottom: 2.5rem; }
/* Utilities
*/
.u-full-width {
width: 100%;
box-sizing: border-box; }
.u-max-full-width {
max-width: 100%;
box-sizing: border-box; }
.u-pull-right {
float: right; }
.u-pull-left {
float: left; }
/* Misc
*/
hr {
margin-top: 3rem;
margin-bottom: 3.5rem;
border-width: 0;
border-top: 1px solid #E1E1E1; }
/* Clearing
*/
/* Self Clearing Goodness */
.container:after,
.row:after,
.u-cf {
content: "";
display: table;
clear: both; }
/* Media Queries
*/
/*
Note: The best way to structure the use of media queries is to create the queries
near the relevant code. For example, if you wanted to change the styles for buttons
on small devices, paste the mobile query code up in the buttons section and style it
there.
*/
/* Larger than mobile */
@media (min-width: 400px) {}
/* Larger than phablet (also point when grid becomes active) */
@media (min-width: 550px) {}
/* Larger than tablet */
@media (min-width: 750px) {}
/* Larger than desktop */
@media (min-width: 1000px) {}
/* Larger than Desktop HD */
@media (min-width: 1200px) {}

View File

@ -1,43 +0,0 @@
function CW(resource) {
this.conn = new WebSocket('wss://stream.cryptowat.ch/connect?apikey=GPDLXH702E1NAD96OSBO');
this.conn.binaryType = "arraybuffer";
this.conn.onopen = () => {
console.log("CW websocket connected");
if (this.onconnect) this.onconnect();
}
let decoder = new TextDecoder();
this.conn.onmessage = (msg) => {
let d = JSON.parse(decoder.decode(msg.data));
// The server will always send an AUTHENTICATED signal when you establish a valid connection
// At this point you can subscribe to resources
if (d.authenticationResult && d.authenticationResult.status === 'AUTHENTICATED') {
if (this.onauth) this.onauth();
this.conn.send(JSON.stringify({
subscribe: {
subscriptions: [
{streamSubscription: {resource: resource}},
],
}
}));
return;
}
// Market data comes in a marketUpdate
// In this case, we're expecting trades so we look for marketUpdate.tradesUpdate
if (!d.marketUpdate || !d.marketUpdate.tradesUpdate) {
return;
}
let trades = d.marketUpdate.tradesUpdate.trades;
for (let i in trades) {
trades[i].price = parseFloat(trades[i].priceStr);
trades[i].volume = parseFloat(trades[i].amountStr);
}
if (this.ontrades) this.ontrades(trades);
}
this.close = () => this.conn.close();
}

View File

@ -1,42 +0,0 @@
function distribute(val, minOld, maxOld, minNew, maxNew) {
let scalar = (val - minOld) / (maxOld - minOld);
return minNew + ((maxNew - minNew) * scalar);
}
function Distributor(capacity) {
this.cap = capacity;
this.reset = () => {
this.arr = [];
this.arrSorted = [];
this.length = 0;
};
this.reset();
// add adds the given value into the series, shifting off the oldest value if
// the series is at capacity.
this.add = (val) => {
this.arr.push(val);
if (this.arr.length >= this.cap) this.arr.shift();
this.arrSorted = this.arr.slice(); // copy array
this.arrSorted.sort();
this.length = this.arr.length;
};
// distribute finds where the given value falls within the series, and then
// scales that into the given range (inclusive).
this.distribute = (val, min, max) => {
if (this.length == 0) throw "cannot locate within empty Distributor";
let idx = this.length;
for (i in this.arrSorted) {
if (val < this.arrSorted[i]) {
idx = i;
break;
}
}
return distribute(idx, 0, this.length, min, max);
};
}

View File

@ -1,21 +0,0 @@
The MIT License
Copyright (c) 2010-2013 MIDI.js Authors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,61 +0,0 @@
//https://github.com/davidchambers/Base64.js
;(function () {
var object = typeof exports != 'undefined' ? exports : this; // #8: web workers
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
function InvalidCharacterError(message) {
this.message = message;
}
InvalidCharacterError.prototype = new Error;
InvalidCharacterError.prototype.name = 'InvalidCharacterError';
// encoder
// [https://gist.github.com/999166] by [https://github.com/nignag]
object.btoa || (
object.btoa = function (input) {
for (
// initialize result and counter
var block, charCode, idx = 0, map = chars, output = '';
// if the next input index does not exist:
// change the mapping table to "="
// check if d has no fractional digits
input.charAt(idx | 0) || (map = '=', idx % 1);
// "8 - idx % 1 * 8" generates the sequence 2, 4, 6, 8
output += map.charAt(63 & block >> 8 - idx % 1 * 8)
) {
charCode = input.charCodeAt(idx += 3/4);
if (charCode > 0xFF) {
throw new InvalidCharacterError("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
}
block = block << 8 | charCode;
}
return output;
});
// decoder
// [https://gist.github.com/1020396] by [https://github.com/atk]
object.atob || (
object.atob = function (input) {
input = input.replace(/=+$/, '')
if (input.length % 4 == 1) {
throw new InvalidCharacterError("'atob' failed: The string to be decoded is not correctly encoded.");
}
for (
// initialize result and counters
var bc = 0, bs, buffer, idx = 0, output = '';
// get next character
buffer = input.charAt(idx++);
// character found in table? initialize bit storage and add its ascii value;
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf(buffer);
}
return output;
});
}());

View File

@ -1,81 +0,0 @@
/**
* @license -------------------------------------------------------------------
* module: Base64Binary
* src: http://blog.danguer.com/2011/10/24/base64-binary-decoding-in-javascript/
* license: Simplified BSD License
* -------------------------------------------------------------------
* Copyright 2011, Daniel Guerrero. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL DANIEL GUERRERO BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
var Base64Binary = {
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
/* will return a Uint8Array type */
decodeArrayBuffer: function(input) {
var bytes = Math.ceil( (3*input.length) / 4.0);
var ab = new ArrayBuffer(bytes);
this.decode(input, ab);
return ab;
},
decode: function(input, arrayBuffer) {
//get last chars to see if are valid
var lkey1 = this._keyStr.indexOf(input.charAt(input.length-1));
var lkey2 = this._keyStr.indexOf(input.charAt(input.length-1));
var bytes = Math.ceil( (3*input.length) / 4.0);
if (lkey1 == 64) bytes--; //padding chars, so skip
if (lkey2 == 64) bytes--; //padding chars, so skip
var uarray;
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
var j = 0;
if (arrayBuffer)
uarray = new Uint8Array(arrayBuffer);
else
uarray = new Uint8Array(bytes);
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
for (i=0; i<bytes; i+=3) {
//get the 3 octects in 4 ascii chars
enc1 = this._keyStr.indexOf(input.charAt(j++));
enc2 = this._keyStr.indexOf(input.charAt(j++));
enc3 = this._keyStr.indexOf(input.charAt(j++));
enc4 = this._keyStr.indexOf(input.charAt(j++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
uarray[i] = chr1;
if (enc3 != 64) uarray[i+1] = chr2;
if (enc4 != 64) uarray[i+2] = chr3;
}
return uarray;
}
};

View File

@ -1,111 +0,0 @@
/**
* @license -------------------------------------------------------------------
* module: WebAudioShim - Fix naming issues for WebAudioAPI supports
* src: https://github.com/Dinahmoe/webaudioshim
* author: Dinahmoe AB
* -------------------------------------------------------------------
* Copyright (c) 2012 DinahMoe AB
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
window.AudioContext = window.AudioContext || window.webkitAudioContext || null;
window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext || null;
(function (Context) {
var isFunction = function (f) {
return Object.prototype.toString.call(f) === "[object Function]" ||
Object.prototype.toString.call(f) === "[object AudioContextConstructor]";
};
var contextMethods = [
["createGainNode", "createGain"],
["createDelayNode", "createDelay"],
["createJavaScriptNode", "createScriptProcessor"]
];
///
var proto;
var instance;
var sourceProto;
///
if (!isFunction(Context)) {
return;
}
instance = new Context();
if (!instance.destination || !instance.sampleRate) {
return;
}
proto = Context.prototype;
sourceProto = Object.getPrototypeOf(instance.createBufferSource());
if (!isFunction(sourceProto.start)) {
if (isFunction(sourceProto.noteOn)) {
sourceProto.start = function (when, offset, duration) {
switch (arguments.length) {
case 0:
throw new Error("Not enough arguments.");
case 1:
this.noteOn(when);
break;
case 2:
if (this.buffer) {
this.noteGrainOn(when, offset, this.buffer.duration - offset);
} else {
throw new Error("Missing AudioBuffer");
}
break;
case 3:
this.noteGrainOn(when, offset, duration);
}
};
}
}
if (!isFunction(sourceProto.noteOn)) {
sourceProto.noteOn = sourceProto.start;
}
if (!isFunction(sourceProto.noteGrainOn)) {
sourceProto.noteGrainOn = sourceProto.start;
}
if (!isFunction(sourceProto.stop)) {
sourceProto.stop = sourceProto.noteOff;
}
if (!isFunction(sourceProto.noteOff)) {
sourceProto.noteOff = sourceProto.stop;
}
contextMethods.forEach(function (names) {
var name1;
var name2;
while (names.length) {
name1 = names.pop();
if (isFunction(this[name1])) {
this[names.pop()] = this[name1];
} else {
name2 = names.pop();
this[name1] = this[name2];
}
}
}, proto);
})(window.AudioContext);

View File

@ -1,421 +0,0 @@
/* Copyright 2013 Chris Wilson
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Initialize the MIDI library.
(function (global) {
'use strict';
var midiIO, _requestMIDIAccess, MIDIAccess, _onReady, MIDIPort, MIDIInput, MIDIOutput, _midiProc;
function Promise() {
}
Promise.prototype.then = function(accept, reject) {
this.accept = accept;
this.reject = reject;
}
Promise.prototype.succeed = function(access) {
if (this.accept)
this.accept(access);
}
Promise.prototype.fail = function(error) {
if (this.reject)
this.reject(error);
}
function _JazzInstance() {
this.inputInUse = false;
this.outputInUse = false;
// load the Jazz plugin
var o1 = document.createElement("object");
o1.id = "_Jazz" + Math.random() + "ie";
o1.classid = "CLSID:1ACE1618-1C7D-4561-AEE1-34842AA85E90";
this.activeX = o1;
var o2 = document.createElement("object");
o2.id = "_Jazz" + Math.random();
o2.type="audio/x-jazz";
o1.appendChild(o2);
this.objRef = o2;
var e = document.createElement("p");
e.appendChild(document.createTextNode("This page requires the "));
var a = document.createElement("a");
a.appendChild(document.createTextNode("Jazz plugin"));
a.href = "http://jazz-soft.net/";
e.appendChild(a);
e.appendChild(document.createTextNode("."));
o2.appendChild(e);
var insertionPoint = document.getElementById("MIDIPlugin");
if (!insertionPoint) {
// Create hidden element
var insertionPoint = document.createElement("div");
insertionPoint.id = "MIDIPlugin";
insertionPoint.style.position = "absolute";
insertionPoint.style.visibility = "hidden";
insertionPoint.style.left = "-9999px";
insertionPoint.style.top = "-9999px";
document.body.appendChild(insertionPoint);
}
insertionPoint.appendChild(o1);
if (this.objRef.isJazz)
this._Jazz = this.objRef;
else if (this.activeX.isJazz)
this._Jazz = this.activeX;
else
this._Jazz = null;
if (this._Jazz) {
this._Jazz._jazzTimeZero = this._Jazz.Time();
this._Jazz._perfTimeZero = window.performance.now();
}
}
_requestMIDIAccess = function _requestMIDIAccess() {
var access = new MIDIAccess();
return access._promise;
};
// API Methods
MIDIAccess = function() {
this._jazzInstances = new Array();
this._jazzInstances.push( new _JazzInstance() );
this._promise = new Promise;
if (this._jazzInstances[0]._Jazz) {
this._Jazz = this._jazzInstances[0]._Jazz;
window.setTimeout( _onReady.bind(this), 3 );
} else {
window.setTimeout( _onNotReady.bind(this), 3 );
}
};
_onReady = function _onReady() {
if (this._promise)
this._promise.succeed(this);
};
function _onNotReady() {
if (this._promise)
this._promise.fail( { code: 1 } );
};
MIDIAccess.prototype.inputs = function( ) {
if (!this._Jazz)
return null;
var list=this._Jazz.MidiInList();
var inputs = new Array( list.length );
for ( var i=0; i<list.length; i++ ) {
inputs[i] = new MIDIInput( this, list[i], i );
}
return inputs;
}
MIDIAccess.prototype.outputs = function( ) {
if (!this._Jazz)
return null;
var list=this._Jazz.MidiOutList();
var outputs = new Array( list.length );
for ( var i=0; i<list.length; i++ ) {
outputs[i] = new MIDIOutput( this, list[i], i );
}
return outputs;
};
MIDIInput = function MIDIInput( midiAccess, name, index ) {
this._listeners = [];
this._midiAccess = midiAccess;
this._index = index;
this._inLongSysexMessage = false;
this._sysexBuffer = new Uint8Array();
this.id = "" + index + "." + name;
this.manufacturer = "";
this.name = name;
this.type = "input";
this.version = "";
this.onmidimessage = null;
var inputInstance = null;
for (var i=0; (i<midiAccess._jazzInstances.length)&&(!inputInstance); i++) {
if (!midiAccess._jazzInstances[i].inputInUse)
inputInstance=midiAccess._jazzInstances[i];
}
if (!inputInstance) {
inputInstance = new _JazzInstance();
midiAccess._jazzInstances.push( inputInstance );
}
inputInstance.inputInUse = true;
this._jazzInstance = inputInstance._Jazz;
this._input = this._jazzInstance.MidiInOpen( this._index, _midiProc.bind(this) );
};
// Introduced in DOM Level 2:
MIDIInput.prototype.addEventListener = function (type, listener, useCapture ) {
if (type !== "midimessage")
return;
for (var i=0; i<this._listeners.length; i++)
if (this._listeners[i] == listener)
return;
this._listeners.push( listener );
};
MIDIInput.prototype.removeEventListener = function (type, listener, useCapture ) {
if (type !== "midimessage")
return;
for (var i=0; i<this._listeners.length; i++)
if (this._listeners[i] == listener) {
this._listeners.splice( i, 1 ); //remove it
return;
}
};
MIDIInput.prototype.preventDefault = function() {
this._pvtDef = true;
};
MIDIInput.prototype.dispatchEvent = function (evt) {
this._pvtDef = false;
// dispatch to listeners
for (var i=0; i<this._listeners.length; i++)
if (this._listeners[i].handleEvent)
this._listeners[i].handleEvent.bind(this)( evt );
else
this._listeners[i].bind(this)( evt );
if (this.onmidimessage)
this.onmidimessage( evt );
return this._pvtDef;
};
MIDIInput.prototype.appendToSysexBuffer = function ( data ) {
var oldLength = this._sysexBuffer.length;
var tmpBuffer = new Uint8Array( oldLength + data.length );
tmpBuffer.set( this._sysexBuffer );
tmpBuffer.set( data, oldLength );
this._sysexBuffer = tmpBuffer;
};
MIDIInput.prototype.bufferLongSysex = function ( data, initialOffset ) {
var j = initialOffset;
while (j<data.length) {
if (data[j] == 0xF7) {
// end of sysex!
j++;
this.appendToSysexBuffer( data.slice(initialOffset, j) );
return j;
}
j++;
}
// didn't reach the end; just tack it on.
this.appendToSysexBuffer( data.slice(initialOffset, j) );
this._inLongSysexMessage = true;
return j;
};
_midiProc = function _midiProc( timestamp, data ) {
// Have to use createEvent/initEvent because IE10 fails on new CustomEvent. Thanks, IE!
var length = 0;
var i,j;
var isSysexMessage = false;
// Jazz sometimes passes us multiple messages at once, so we need to parse them out
// and pass them one at a time.
for (i=0; i<data.length; i+=length) {
if (this._inLongSysexMessage) {
i = this.bufferLongSysex(data,i);
if ( data[i-1] != 0xf7 ) {
// ran off the end without hitting the end of the sysex message
return;
}
isSysexMessage = true;
} else {
isSysexMessage = false;
switch (data[i] & 0xF0) {
case 0x80: // note off
case 0x90: // note on
case 0xA0: // polyphonic aftertouch
case 0xB0: // control change
case 0xE0: // channel mode
length = 3;
break;
case 0xC0: // program change
case 0xD0: // channel aftertouch
length = 2;
break;
case 0xF0:
switch (data[i]) {
case 0xf0: // variable-length sysex.
i = this.bufferLongSysex(data,i);
if ( data[i-1] != 0xf7 ) {
// ran off the end without hitting the end of the sysex message
return;
}
isSysexMessage = true;
break;
case 0xF1: // MTC quarter frame
case 0xF3: // song select
length = 2;
break;
case 0xF2: // song position pointer
length = 3;
break;
default:
length = 1;
break;
}
break;
}
}
var evt = document.createEvent( "Event" );
evt.initEvent( "midimessage", false, false );
evt.receivedTime = parseFloat( timestamp.toString()) + this._jazzInstance._perfTimeZero;
if (isSysexMessage || this._inLongSysexMessage) {
evt.data = new Uint8Array( this._sysexBuffer );
this._sysexBuffer = new Uint8Array(0);
this._inLongSysexMessage = false;
} else
evt.data = new Uint8Array(data.slice(i, length+i));
this.dispatchEvent( evt );
}
};
MIDIOutput = function MIDIOutput( midiAccess, name, index ) {
this._listeners = [];
this._midiAccess = midiAccess;
this._index = index;
this.id = "" + index + "." + name;
this.manufacturer = "";
this.name = name;
this.type = "output";
this.version = "";
var outputInstance = null;
for (var i=0; (i<midiAccess._jazzInstances.length)&&(!outputInstance); i++) {
if (!midiAccess._jazzInstances[i].outputInUse)
outputInstance=midiAccess._jazzInstances[i];
}
if (!outputInstance) {
outputInstance = new _JazzInstance();
midiAccess._jazzInstances.push( outputInstance );
}
outputInstance.outputInUse = true;
this._jazzInstance = outputInstance._Jazz;
this._jazzInstance.MidiOutOpen(this.name);
};
function _sendLater() {
this.jazz.MidiOutLong( this.data ); // handle send as sysex
}
MIDIOutput.prototype.send = function( data, timestamp ) {
var delayBeforeSend = 0;
if (data.length === 0)
return false;
if (timestamp)
delayBeforeSend = Math.floor( timestamp - window.performance.now() );
if (timestamp && (delayBeforeSend>1)) {
var sendObj = new Object();
sendObj.jazz = this._jazzInstance;
sendObj.data = data;
window.setTimeout( _sendLater.bind(sendObj), delayBeforeSend );
} else {
this._jazzInstance.MidiOutLong( data );
}
return true;
};
//init: create plugin
if (!window.navigator.requestMIDIAccess)
window.navigator.requestMIDIAccess = _requestMIDIAccess;
}(window));
// Polyfill window.performance.now() if necessary.
(function (exports) {
var perf = {}, props;
function findAlt() {
var prefix = ['moz', 'webkit', 'o', 'ms'],
i = prefix.length,
//worst case, we use Date.now()
props = {
value: (function (start) {
return function () {
return Date.now() - start;
};
}(Date.now()))
};
//seach for vendor prefixed version
for (; i >= 0; i--) {
if ((prefix[i] + "Now") in exports.performance) {
props.value = function (method) {
return function () {
exports.performance[method]();
}
}(prefix[i] + "Now");
return props;
}
}
//otherwise, try to use connectionStart
if ("timing" in exports.performance && "connectStart" in exports.performance.timing) {
//this pretty much approximates performance.now() to the millisecond
props.value = (function (start) {
return function() {
Date.now() - start;
};
}(exports.performance.timing.connectStart));
}
return props;
}
//if already defined, bail
if (("performance" in exports) && ("now" in exports.performance))
return;
if (!("performance" in exports))
Object.defineProperty(exports, "performance", {
get: function () {
return perf;
}});
//otherwise, performance is there, but not "now()"
props = findAlt();
Object.defineProperty(exports.performance, "now", props);
}(window));

View File

@ -1,101 +0,0 @@
/*
----------------------------------------------------------
MIDI.audioDetect : 0.3.2 : 2015-03-26
----------------------------------------------------------
https://github.com/mudcube/MIDI.js
----------------------------------------------------------
Probably, Maybe, No... Absolutely!
Test to see what types of <audio> MIME types are playable by the browser.
----------------------------------------------------------
*/
if (typeof MIDI === 'undefined') MIDI = {};
(function(root) { 'use strict';
var supports = {}; // object of supported file types
var pending = 0; // pending file types to process
var canPlayThrough = function (src) { // check whether format plays through
pending ++;
var body = document.body;
var audio = new Audio();
var mime = src.split(';')[0];
audio.id = 'audio';
audio.setAttribute('preload', 'auto');
audio.setAttribute('audiobuffer', true);
audio.addEventListener('error', function() {
body.removeChild(audio);
supports[mime] = false;
pending --;
}, false);
audio.addEventListener('canplaythrough', function() {
body.removeChild(audio);
supports[mime] = true;
pending --;
}, false);
audio.src = 'data:' + src;
body.appendChild(audio);
};
root.audioDetect = function(onsuccess) {
/// detect jazz-midi plugin
if (navigator.requestMIDIAccess) {
var isNative = Function.prototype.toString.call(navigator.requestMIDIAccess).indexOf('[native code]');
if (isNative) { // has native midiapi support
supports['webmidi'] = true;
} else { // check for jazz plugin midiapi support
for (var n = 0; navigator.plugins.length > n; n ++) {
var plugin = navigator.plugins[n];
if (plugin.name.indexOf('Jazz-Plugin') >= 0) {
supports['webmidi'] = true;
}
}
}
}
/// check whether <audio> tag is supported
if (typeof(Audio) === 'undefined') {
return onsuccess({});
} else {
supports['audiotag'] = true;
}
/// check for webaudio api support
if (window.AudioContext || window.webkitAudioContext) {
supports['webaudio'] = true;
}
/// check whether canPlayType is supported
var audio = new Audio();
if (typeof(audio.canPlayType) === 'undefined') {
return onsuccess(supports);
}
/// see what we can learn from the browser
var vorbis = audio.canPlayType('audio/ogg; codecs="vorbis"');
vorbis = (vorbis === 'probably' || vorbis === 'maybe');
var mpeg = audio.canPlayType('audio/mpeg');
mpeg = (mpeg === 'probably' || mpeg === 'maybe');
// maybe nothing is supported
if (!vorbis && !mpeg) {
onsuccess(supports);
return;
}
/// or maybe something is supported
if (vorbis) canPlayThrough('audio/ogg;base64,T2dnUwACAAAAAAAAAADqnjMlAAAAAOyyzPIBHgF2b3JiaXMAAAAAAUAfAABAHwAAQB8AAEAfAACZAU9nZ1MAAAAAAAAAAAAA6p4zJQEAAAANJGeqCj3//////////5ADdm9yYmlzLQAAAFhpcGguT3JnIGxpYlZvcmJpcyBJIDIwMTAxMTAxIChTY2hhdWZlbnVnZ2V0KQAAAAABBXZvcmJpcw9CQ1YBAAABAAxSFCElGVNKYwiVUlIpBR1jUFtHHWPUOUYhZBBTiEkZpXtPKpVYSsgRUlgpRR1TTFNJlVKWKUUdYxRTSCFT1jFloXMUS4ZJCSVsTa50FkvomWOWMUYdY85aSp1j1jFFHWNSUkmhcxg6ZiVkFDpGxehifDA6laJCKL7H3lLpLYWKW4q91xpT6y2EGEtpwQhhc+211dxKasUYY4wxxsXiUyiC0JBVAAABAABABAFCQ1YBAAoAAMJQDEVRgNCQVQBABgCAABRFcRTHcRxHkiTLAkJDVgEAQAAAAgAAKI7hKJIjSZJkWZZlWZameZaouaov+64u667t6roOhIasBACAAAAYRqF1TCqDEEPKQ4QUY9AzoxBDDEzGHGNONKQMMogzxZAyiFssLqgQBKEhKwKAKAAAwBjEGGIMOeekZFIi55iUTkoDnaPUUcoolRRLjBmlEluJMYLOUeooZZRCjKXFjFKJscRUAABAgAMAQICFUGjIigAgCgCAMAYphZRCjCnmFHOIMeUcgwwxxiBkzinoGJNOSuWck85JiRhjzjEHlXNOSuekctBJyaQTAAAQ4AAAEGAhFBqyIgCIEwAwSJKmWZomipamiaJniqrqiaKqWp5nmp5pqqpnmqpqqqrrmqrqypbnmaZnmqrqmaaqiqbquqaquq6nqrZsuqoum65q267s+rZru77uqapsm6or66bqyrrqyrbuurbtS56nqqKquq5nqq6ruq5uq65r25pqyq6purJtuq4tu7Js664s67pmqq5suqotm64s667s2rYqy7ovuq5uq7Ks+6os+75s67ru2rrwi65r66os674qy74x27bwy7ouHJMnqqqnqq7rmarrqq5r26rr2rqmmq5suq4tm6or26os67Yry7aumaosm64r26bryrIqy77vyrJui67r66Ys67oqy8Lu6roxzLat+6Lr6roqy7qvyrKuu7ru+7JuC7umqrpuyrKvm7Ks+7auC8us27oxuq7vq7It/KosC7+u+8Iy6z5jdF1fV21ZGFbZ9n3d95Vj1nVhWW1b+V1bZ7y+bgy7bvzKrQvLstq2scy6rSyvrxvDLux8W/iVmqratum6um7Ksq/Lui60dd1XRtf1fdW2fV+VZd+3hV9pG8OwjK6r+6os68Jry8ov67qw7MIvLKttK7+r68ow27qw3L6wLL/uC8uq277v6rrStXVluX2fsSu38QsAABhwAAAIMKEMFBqyIgCIEwBAEHIOKQahYgpCCKGkEEIqFWNSMuakZM5JKaWUFEpJrWJMSuaclMwxKaGUlkopqYRSWiqlxBRKaS2l1mJKqcVQSmulpNZKSa2llGJMrcUYMSYlc05K5pyUklJrJZXWMucoZQ5K6iCklEoqraTUYuacpA46Kx2E1EoqMZWUYgupxFZKaq2kFGMrMdXUWo4hpRhLSrGVlFptMdXWWqs1YkxK5pyUzDkqJaXWSiqtZc5J6iC01DkoqaTUYiopxco5SR2ElDLIqJSUWiupxBJSia20FGMpqcXUYq4pxRZDSS2WlFosqcTWYoy1tVRTJ6XFklKMJZUYW6y5ttZqDKXEVkqLsaSUW2sx1xZjjqGkFksrsZWUWmy15dhayzW1VGNKrdYWY40x5ZRrrT2n1mJNMdXaWqy51ZZbzLXnTkprpZQWS0oxttZijTHmHEppraQUWykpxtZara3FXEMpsZXSWiypxNhirLXFVmNqrcYWW62ltVprrb3GVlsurdXcYqw9tZRrrLXmWFNtBQAADDgAAASYUAYKDVkJAEQBAADGMMYYhEYpx5yT0ijlnHNSKucghJBS5hyEEFLKnINQSkuZcxBKSSmUklJqrYVSUmqttQIAAAocAAACbNCUWByg0JCVAEAqAIDBcTRNFFXVdX1fsSxRVFXXlW3jVyxNFFVVdm1b+DVRVFXXtW3bFn5NFFVVdmXZtoWiqrqybduybgvDqKqua9uybeuorqvbuq3bui9UXVmWbVu3dR3XtnXd9nVd+Bmzbeu2buu+8CMMR9/4IeTj+3RCCAAAT3AAACqwYXWEk6KxwEJDVgIAGQAAgDFKGYUYM0gxphhjTDHGmAAAgAEHAIAAE8pAoSErAoAoAADAOeecc84555xzzjnnnHPOOeecc44xxhhjjDHGGGOMMcYYY4wxxhhjjDHGGGOMMcYYY0wAwE6EA8BOhIVQaMhKACAcAABACCEpKaWUUkoRU85BSSmllFKqFIOMSkoppZRSpBR1lFJKKaWUIqWgpJJSSimllElJKaWUUkoppYw6SimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaWUUkoppZRSSimllFJKKaVUSimllFJKKaWUUkoppRQAYPLgAACVYOMMK0lnhaPBhYasBAByAwAAhRiDEEJpraRUUkolVc5BKCWUlEpKKZWUUqqYgxBKKqmlklJKKbXSQSihlFBKKSWUUkooJYQQSgmhlFRCK6mEUkoHoYQSQimhhFRKKSWUzkEoIYUOQkmllNRCSB10VFIpIZVSSiklpZQ6CKGUklJLLZVSWkqpdBJSKamV1FJqqbWSUgmhpFZKSSWl0lpJJbUSSkklpZRSSymFVFJJJYSSUioltZZaSqm11lJIqZWUUkqppdRSSiWlkEpKqZSSUmollZRSaiGVlEpJKaTUSimlpFRCSamlUlpKLbWUSkmptFRSSaWUlEpJKaVSSksppRJKSqmllFpJKYWSUkoplZJSSyW1VEoKJaWUUkmptJRSSymVklIBAEAHDgAAAUZUWoidZlx5BI4oZJiAAgAAQABAgAkgMEBQMApBgDACAQAAAADAAAAfAABHARAR0ZzBAUKCwgJDg8MDAAAAAAAAAAAAAACAT2dnUwAEAAAAAAAAAADqnjMlAgAAADzQPmcBAQA=');
if (mpeg) canPlayThrough('audio/mpeg;base64,/+MYxAAAAANIAUAAAASEEB/jwOFM/0MM/90b/+RhST//w4NFwOjf///PZu////9lns5GFDv//l9GlUIEEIAAAgIg8Ir/JGq3/+MYxDsLIj5QMYcoAP0dv9HIjUcH//yYSg+CIbkGP//8w0bLVjUP///3Z0x5QCAv/yLjwtGKTEFNRTMuOTeqqqqqqqqqqqqq/+MYxEkNmdJkUYc4AKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq');
/// lets find out!
var time = (new Date()).getTime();
var interval = window.setInterval(function() {
var now = (new Date()).getTime();
var maxExecution = now - time > 5000;
if (!pending || maxExecution) {
window.clearInterval(interval);
onsuccess(supports);
}
}, 1);
};
})(MIDI);

View File

@ -1,161 +0,0 @@
/*
----------------------------------------------------------
GeneralMIDI
----------------------------------------------------------
*/
(function(root) { 'use strict';
root.GM = (function(arr) {
var clean = function(name) {
return name.replace(/[^a-z0-9 ]/gi, '').replace(/[ ]/g, '_').toLowerCase();
};
var res = {
byName: { },
byId: { },
byCategory: { }
};
for (var key in arr) {
var list = arr[key];
for (var n = 0, length = list.length; n < length; n++) {
var instrument = list[n];
if (!instrument) continue;
var num = parseInt(instrument.substr(0, instrument.indexOf(' ')), 10);
instrument = instrument.replace(num + ' ', '');
res.byId[--num] =
res.byName[clean(instrument)] =
res.byCategory[clean(key)] = {
id: clean(instrument),
instrument: instrument,
number: num,
category: key
};
}
}
return res;
})({
'Piano': ['1 Acoustic Grand Piano', '2 Bright Acoustic Piano', '3 Electric Grand Piano', '4 Honky-tonk Piano', '5 Electric Piano 1', '6 Electric Piano 2', '7 Harpsichord', '8 Clavinet'],
'Chromatic Percussion': ['9 Celesta', '10 Glockenspiel', '11 Music Box', '12 Vibraphone', '13 Marimba', '14 Xylophone', '15 Tubular Bells', '16 Dulcimer'],
'Organ': ['17 Drawbar Organ', '18 Percussive Organ', '19 Rock Organ', '20 Church Organ', '21 Reed Organ', '22 Accordion', '23 Harmonica', '24 Tango Accordion'],
'Guitar': ['25 Acoustic Guitar (nylon)', '26 Acoustic Guitar (steel)', '27 Electric Guitar (jazz)', '28 Electric Guitar (clean)', '29 Electric Guitar (muted)', '30 Overdriven Guitar', '31 Distortion Guitar', '32 Guitar Harmonics'],
'Bass': ['33 Acoustic Bass', '34 Electric Bass (finger)', '35 Electric Bass (pick)', '36 Fretless Bass', '37 Slap Bass 1', '38 Slap Bass 2', '39 Synth Bass 1', '40 Synth Bass 2'],
'Strings': ['41 Violin', '42 Viola', '43 Cello', '44 Contrabass', '45 Tremolo Strings', '46 Pizzicato Strings', '47 Orchestral Harp', '48 Timpani'],
'Ensemble': ['49 String Ensemble 1', '50 String Ensemble 2', '51 Synth Strings 1', '52 Synth Strings 2', '53 Choir Aahs', '54 Voice Oohs', '55 Synth Choir', '56 Orchestra Hit'],
'Brass': ['57 Trumpet', '58 Trombone', '59 Tuba', '60 Muted Trumpet', '61 French Horn', '62 Brass Section', '63 Synth Brass 1', '64 Synth Brass 2'],
'Reed': ['65 Soprano Sax', '66 Alto Sax', '67 Tenor Sax', '68 Baritone Sax', '69 Oboe', '70 English Horn', '71 Bassoon', '72 Clarinet'],
'Pipe': ['73 Piccolo', '74 Flute', '75 Recorder', '76 Pan Flute', '77 Blown Bottle', '78 Shakuhachi', '79 Whistle', '80 Ocarina'],
'Synth Lead': ['81 Lead 1 (square)', '82 Lead 2 (sawtooth)', '83 Lead 3 (calliope)', '84 Lead 4 (chiff)', '85 Lead 5 (charang)', '86 Lead 6 (voice)', '87 Lead 7 (fifths)', '88 Lead 8 (bass + lead)'],
'Synth Pad': ['89 Pad 1 (new age)', '90 Pad 2 (warm)', '91 Pad 3 (polysynth)', '92 Pad 4 (choir)', '93 Pad 5 (bowed)', '94 Pad 6 (metallic)', '95 Pad 7 (halo)', '96 Pad 8 (sweep)'],
'Synth Effects': ['97 FX 1 (rain)', '98 FX 2 (soundtrack)', '99 FX 3 (crystal)', '100 FX 4 (atmosphere)', '101 FX 5 (brightness)', '102 FX 6 (goblins)', '103 FX 7 (echoes)', '104 FX 8 (sci-fi)'],
'Ethnic': ['105 Sitar', '106 Banjo', '107 Shamisen', '108 Koto', '109 Kalimba', '110 Bagpipe', '111 Fiddle', '112 Shanai'],
'Percussive': ['113 Tinkle Bell', '114 Agogo', '115 Steel Drums', '116 Woodblock', '117 Taiko Drum', '118 Melodic Tom', '119 Synth Drum'],
'Sound effects': ['120 Reverse Cymbal', '121 Guitar Fret Noise', '122 Breath Noise', '123 Seashore', '124 Bird Tweet', '125 Telephone Ring', '126 Helicopter', '127 Applause', '128 Gunshot']
});
/* get/setInstrument
--------------------------------------------------- */
root.getInstrument = function(channelId) {
var channel = root.channels[channelId];
return channel && channel.instrument;
};
root.setInstrument = function(channelId, program, delay) {
var channel = root.channels[channelId];
if (delay) {
return setTimeout(function() {
channel.instrument = program;
}, delay);
} else {
channel.instrument = program;
}
};
/* get/setMono
--------------------------------------------------- */
root.getMono = function(channelId) {
var channel = root.channels[channelId];
return channel && channel.mono;
};
root.setMono = function(channelId, truthy, delay) {
var channel = root.channels[channelId];
if (delay) {
return setTimeout(function() {
channel.mono = truthy;
}, delay);
} else {
channel.mono = truthy;
}
};
/* get/setOmni
--------------------------------------------------- */
root.getOmni = function(channelId) {
var channel = root.channels[channelId];
return channel && channel.omni;
};
root.setOmni = function(channelId, truthy) {
var channel = root.channels[channelId];
if (delay) {
return setTimeout(function() {
channel.omni = truthy;
}, delay);
} else {
channel.omni = truthy;
}
};
/* get/setSolo
--------------------------------------------------- */
root.getSolo = function(channelId) {
var channel = root.channels[channelId];
return channel && channel.solo;
};
root.setSolo = function(channelId, truthy) {
var channel = root.channels[channelId];
if (delay) {
return setTimeout(function() {
channel.solo = truthy;
}, delay);
} else {
channel.solo = truthy;
}
};
/* channels
--------------------------------------------------- */
root.channels = (function() { // 0 - 15 channels
var channels = {};
for (var i = 0; i < 16; i++) {
channels[i] = { // default values
instrument: i,
pitchBend: 0,
mute: false,
mono: false,
omni: false,
solo: false
};
}
return channels;
})();
/* note conversions
--------------------------------------------------- */
root.keyToNote = {}; // C8 == 108
root.noteToKey = {}; // 108 == C8
(function() {
var A0 = 0x15; // first note
var C8 = 0x6C; // last note
var number2key = ['C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B'];
for (var n = A0; n <= C8; n++) {
var octave = (n - 12) / 12 >> 0;
var name = number2key[n % 12] + octave;
root.keyToNote[name] = n;
root.noteToKey[n] = name;
}
})();
})(MIDI);

View File

@ -1,199 +0,0 @@
/*
----------------------------------------------------------
MIDI.Plugin : 0.3.4 : 2015-03-26
----------------------------------------------------------
https://github.com/mudcube/MIDI.js
----------------------------------------------------------
Inspired by javax.sound.midi (albeit a super simple version):
http://docs.oracle.com/javase/6/docs/api/javax/sound/midi/package-summary.html
----------------------------------------------------------
Technologies
----------------------------------------------------------
Web MIDI API - no native support yet (jazzplugin)
Web Audio API - firefox 25+, chrome 10+, safari 6+, opera 15+
HTML5 Audio Tag - ie 9+, firefox 3.5+, chrome 4+, safari 4+, opera 9.5+, ios 4+, android 2.3+
----------------------------------------------------------
*/
if (typeof MIDI === 'undefined') MIDI = {};
MIDI.Soundfont = MIDI.Soundfont || {};
MIDI.Player = MIDI.Player || {};
(function(root) { 'use strict';
root.DEBUG = true;
root.USE_XHR = true;
root.soundfontUrl = './soundfont/';
/*
MIDI.loadPlugin({
onsuccess: function() { },
onprogress: function(state, percent) { },
targetFormat: 'mp3', // optionally can force to use MP3 (for instance on mobile networks)
instrument: 'acoustic_grand_piano', // or 1 (default)
instruments: [ 'acoustic_grand_piano', 'acoustic_guitar_nylon' ] // or multiple instruments
});
*/
root.loadPlugin = function(opts) {
if (typeof opts === 'function') {
opts = {onsuccess: opts};
}
root.soundfontUrl = opts.soundfontUrl || root.soundfontUrl;
/// Detect the best type of audio to use
root.audioDetect(function(supports) {
var hash = window.location.hash;
var api = '';
/// use the most appropriate plugin if not specified
if (supports[opts.api]) {
api = opts.api;
} else if (supports[hash.substr(1)]) {
api = hash.substr(1);
} else if (supports.webmidi) {
api = 'webmidi';
} else if (window.AudioContext) { // Chrome
api = 'webaudio';
} else if (window.Audio) { // Firefox
api = 'audiotag';
}
if (connect[api]) {
/// use audio/ogg when supported
if (opts.targetFormat) {
var audioFormat = opts.targetFormat;
} else { // use best quality
var audioFormat = supports['audio/ogg'] ? 'ogg' : 'mp3';
}
/// load the specified plugin
root.__api = api;
root.__audioFormat = audioFormat;
root.supports = supports;
root.loadResource(opts);
}
});
};
/*
root.loadResource({
onsuccess: function() { },
onprogress: function(state, percent) { },
instrument: 'banjo'
})
*/
root.loadResource = function(opts) {
var instruments = opts.instruments || opts.instrument || 'acoustic_grand_piano';
///
if (typeof instruments !== 'object') {
if (instruments || instruments === 0) {
instruments = [instruments];
} else {
instruments = [];
}
}
/// convert numeric ids into strings
for (var i = 0; i < instruments.length; i ++) {
var instrument = instruments[i];
if (instrument === +instrument) { // is numeric
if (root.GM.byId[instrument]) {
instruments[i] = root.GM.byId[instrument].id;
}
}
}
///
opts.format = root.__audioFormat;
opts.instruments = instruments;
///
connect[root.__api](opts);
};
var connect = {
webmidi: function(opts) {
// cant wait for this to be standardized!
root.WebMIDI.connect(opts);
},
audiotag: function(opts) {
// works ok, kinda like a drunken tuna fish, across the board
// http://caniuse.com/audio
requestQueue(opts, 'AudioTag');
},
webaudio: function(opts) {
// works awesome! safari, chrome and firefox support
// http://caniuse.com/web-audio
requestQueue(opts, 'WebAudio');
}
};
var requestQueue = function(opts, context) {
var audioFormat = opts.format;
var instruments = opts.instruments;
var onprogress = opts.onprogress;
var onerror = opts.onerror;
///
var length = instruments.length;
var pending = length;
var waitForEnd = function() {
if (!--pending) {
onprogress && onprogress('load', 1.0);
root[context].connect(opts);
}
};
///
for (var i = 0; i < length; i ++) {
var instrumentId = instruments[i];
if (MIDI.Soundfont[instrumentId]) { // already loaded
waitForEnd();
} else { // needs to be requested
sendRequest(instruments[i], audioFormat, function(evt, progress) {
var fileProgress = progress / length;
var queueProgress = (length - pending) / length;
onprogress && onprogress('load', fileProgress + queueProgress, instrumentId);
}, function() {
waitForEnd();
}, onerror);
}
};
};
var sendRequest = function(instrumentId, audioFormat, onprogress, onsuccess, onerror) {
var soundfontPath = root.soundfontUrl + instrumentId + '-' + audioFormat + '.js';
if (root.USE_XHR) {
root.util.request({
url: soundfontPath,
format: 'text',
onerror: onerror,
onprogress: onprogress,
onsuccess: function(event, responseText) {
var script = document.createElement('script');
script.language = 'javascript';
script.type = 'text/javascript';
script.text = responseText;
document.body.appendChild(script);
///
onsuccess();
}
});
} else {
dom.loadScript.add({
url: soundfontPath,
verify: 'MIDI.Soundfont["' + instrumentId + '"]',
onerror: onerror,
onsuccess: function() {
onsuccess();
}
});
}
};
root.setDefaultPlugin = function(midi) {
for (var key in midi) {
root[key] = midi[key];
}
};
})(MIDI);

View File

@ -1,380 +0,0 @@
/*
----------------------------------------------------------
MIDI.Player : 0.3.1 : 2015-03-26
----------------------------------------------------------
https://github.com/mudcube/MIDI.js
----------------------------------------------------------
*/
if (typeof MIDI === 'undefined') MIDI = {};
if (typeof MIDI.Player === 'undefined') MIDI.Player = {};
(function() { 'use strict';
var midi = MIDI.Player;
midi.currentTime = 0;
midi.endTime = 0;
midi.restart = 0;
midi.playing = false;
midi.timeWarp = 1;
midi.startDelay = 0;
midi.BPM = 120;
midi.start =
midi.resume = function(onsuccess) {
if (midi.currentTime < -1) {
midi.currentTime = -1;
}
startAudio(midi.currentTime, null, onsuccess);
};
midi.pause = function() {
var tmp = midi.restart;
stopAudio();
midi.restart = tmp;
};
midi.stop = function() {
stopAudio();
midi.restart = 0;
midi.currentTime = 0;
};
midi.addListener = function(onsuccess) {
onMidiEvent = onsuccess;
};
midi.removeListener = function() {
onMidiEvent = undefined;
};
midi.clearAnimation = function() {
if (midi.animationFrameId) {
cancelAnimationFrame(midi.animationFrameId);
}
};
midi.setAnimation = function(callback) {
var currentTime = 0;
var tOurTime = 0;
var tTheirTime = 0;
//
midi.clearAnimation();
///
var frame = function() {
midi.animationFrameId = requestAnimationFrame(frame);
///
if (midi.endTime === 0) {
return;
}
if (midi.playing) {
currentTime = (tTheirTime === midi.currentTime) ? tOurTime - Date.now() : 0;
if (midi.currentTime === 0) {
currentTime = 0;
} else {
currentTime = midi.currentTime - currentTime;
}
if (tTheirTime !== midi.currentTime) {
tOurTime = Date.now();
tTheirTime = midi.currentTime;
}
} else { // paused
currentTime = midi.currentTime;
}
///
var endTime = midi.endTime;
var percent = currentTime / endTime;
var total = currentTime / 1000;
var minutes = total / 60;
var seconds = total - (minutes * 60);
var t1 = minutes * 60 + seconds;
var t2 = (endTime / 1000);
///
if (t2 - t1 < -1.0) {
return;
} else {
callback({
now: t1,
end: t2,
events: noteRegistrar
});
}
};
///
requestAnimationFrame(frame);
};
// helpers
midi.loadMidiFile = function(onsuccess, onprogress, onerror) {
try {
midi.replayer = new Replayer(MidiFile(midi.currentData), midi.timeWarp, null, midi.BPM);
midi.data = midi.replayer.getData();
midi.endTime = getLength();
///
MIDI.loadPlugin({
// instruments: midi.getFileInstruments(),
onsuccess: onsuccess,
onprogress: onprogress,
onerror: onerror
});
} catch(event) {
onerror && onerror(event);
}
};
midi.loadFile = function(file, onsuccess, onprogress, onerror) {
midi.stop();
if (file.indexOf('base64,') !== -1) {
var data = window.atob(file.split(',')[1]);
midi.currentData = data;
midi.loadMidiFile(onsuccess, onprogress, onerror);
} else {
var fetch = new XMLHttpRequest();
fetch.open('GET', file);
fetch.overrideMimeType('text/plain; charset=x-user-defined');
fetch.onreadystatechange = function() {
if (this.readyState === 4) {
if (this.status === 200) {
var t = this.responseText || '';
var ff = [];
var mx = t.length;
var scc = String.fromCharCode;
for (var z = 0; z < mx; z++) {
ff[z] = scc(t.charCodeAt(z) & 255);
}
///
var data = ff.join('');
midi.currentData = data;
midi.loadMidiFile(onsuccess, onprogress, onerror);
} else {
onerror && onerror('Unable to load MIDI file');
}
}
};
fetch.send();
}
};
midi.getFileInstruments = function() {
var instruments = {};
var programs = {};
for (var n = 0; n < midi.data.length; n ++) {
var event = midi.data[n][0].event;
if (event.type !== 'channel') {
continue;
}
var channel = event.channel;
switch(event.subtype) {
case 'controller':
// console.log(event.channel, MIDI.defineControl[event.controllerType], event.value);
break;
case 'programChange':
programs[channel] = event.programNumber;
break;
case 'noteOn':
var program = programs[channel];
var gm = MIDI.GM.byId[isFinite(program) ? program : channel];
instruments[gm.id] = true;
break;
}
}
var ret = [];
for (var key in instruments) {
ret.push(key);
}
return ret;
};
// Playing the audio
var eventQueue = []; // hold events to be triggered
var queuedTime; //
var startTime = 0; // to measure time elapse
var noteRegistrar = {}; // get event for requested note
var onMidiEvent = undefined; // listener
var scheduleTracking = function(channel, note, currentTime, offset, message, velocity, time) {
return setTimeout(function() {
var data = {
channel: channel,
note: note,
now: currentTime,
end: midi.endTime,
message: message,
velocity: velocity
};
//
if (message === 128) {
delete noteRegistrar[note];
} else {
noteRegistrar[note] = data;
}
if (onMidiEvent) {
onMidiEvent(data);
}
midi.currentTime = currentTime;
///
eventQueue.shift();
///
if (eventQueue.length < 1000) {
startAudio(queuedTime, true);
} else if (midi.currentTime === queuedTime && queuedTime < midi.endTime) { // grab next sequence
startAudio(queuedTime, true);
}
}, currentTime - offset);
};
var getContext = function() {
if (MIDI.api === 'webaudio') {
return MIDI.WebAudio.getContext();
} else {
midi.ctx = {currentTime: 0};
}
return midi.ctx;
};
var getLength = function() {
var data = midi.data;
var length = data.length;
var totalTime = 0.5;
for (var n = 0; n < length; n++) {
totalTime += data[n][1];
}
return totalTime;
};
var __now;
var getNow = function() {
if (window.performance && window.performance.now) {
return window.performance.now();
} else {
return Date.now();
}
};
var startAudio = function(currentTime, fromCache, onsuccess) {
if (!midi.replayer) {
return;
}
if (!fromCache) {
if (typeof currentTime === 'undefined') {
currentTime = midi.restart;
}
///
midi.playing && stopAudio();
midi.playing = true;
midi.data = midi.replayer.getData();
midi.endTime = getLength();
}
///
var note;
var offset = 0;
var messages = 0;
var data = midi.data;
var ctx = getContext();
var length = data.length;
//
queuedTime = 0.5;
///
var interval = eventQueue[0] && eventQueue[0].interval || 0;
var foffset = currentTime - midi.currentTime;
///
if (MIDI.api !== 'webaudio') { // set currentTime on ctx
var now = getNow();
__now = __now || now;
ctx.currentTime = (now - __now) / 1000;
}
///
startTime = ctx.currentTime;
///
for (var n = 0; n < length && messages < 100; n++) {
var obj = data[n];
if ((queuedTime += obj[1]) <= currentTime) {
offset = queuedTime;
continue;
}
///
currentTime = queuedTime - offset;
///
var event = obj[0].event;
if (event.type !== 'channel') {
continue;
}
///
var channelId = event.channel;
var channel = MIDI.channels[channelId];
var delay = ctx.currentTime + ((currentTime + foffset + midi.startDelay) / 1000);
var queueTime = queuedTime - offset + midi.startDelay;
switch (event.subtype) {
case 'controller':
MIDI.setController(channelId, event.controllerType, event.value, delay);
break;
case 'programChange':
MIDI.programChange(channelId, event.programNumber, delay);
break;
case 'pitchBend':
MIDI.pitchBend(channelId, event.value, delay);
break;
case 'noteOn':
if (channel.mute) break;
note = event.noteNumber - (midi.MIDIOffset || 0);
eventQueue.push({
event: event,
time: queueTime,
source: MIDI.noteOn(channelId, event.noteNumber, event.velocity, delay),
interval: scheduleTracking(channelId, note, queuedTime + midi.startDelay, offset - foffset, 144, event.velocity)
});
messages++;
break;
case 'noteOff':
if (channel.mute) break;
note = event.noteNumber - (midi.MIDIOffset || 0);
eventQueue.push({
event: event,
time: queueTime,
source: MIDI.noteOff(channelId, event.noteNumber, delay),
interval: scheduleTracking(channelId, note, queuedTime, offset - foffset, 128, 0)
});
break;
default:
break;
}
}
///
onsuccess && onsuccess(eventQueue);
};
var stopAudio = function() {
var ctx = getContext();
midi.playing = false;
midi.restart += (ctx.currentTime - startTime) * 1000;
// stop the audio, and intervals
while (eventQueue.length) {
var o = eventQueue.pop();
window.clearInterval(o.interval);
if (!o.source) continue; // is not webaudio
if (typeof(o.source) === 'number') {
window.clearTimeout(o.source);
} else { // webaudio
o.source.disconnect(0);
}
}
// run callback to cancel any notes still playing
for (var key in noteRegistrar) {
var o = noteRegistrar[key]
if (noteRegistrar[key].message === 144 && onMidiEvent) {
onMidiEvent({
channel: o.channel,
note: o.note,
now: o.now,
end: o.end,
message: 128,
velocity: o.velocity
});
}
}
// reset noteRegistrar
noteRegistrar = {};
};
})();

View File

@ -1,150 +0,0 @@
/*
----------------------------------------------------------------------
AudioTag <audio> - OGG or MPEG Soundbank
----------------------------------------------------------------------
http://dev.w3.org/html5/spec/Overview.html#the-audio-element
----------------------------------------------------------------------
*/
(function(root) { 'use strict';
window.Audio && (function() {
var midi = root.AudioTag = { api: 'audiotag' };
var noteToKey = {};
var volume = 127; // floating point
var buffer_nid = -1; // current channel
var audioBuffers = []; // the audio channels
var notesOn = []; // instrumentId + noteId that is currently playing in each 'channel', for routing noteOff/chordOff calls
var notes = {}; // the piano keys
for (var nid = 0; nid < 12; nid ++) {
audioBuffers[nid] = new Audio();
}
var playChannel = function(channel, note) {
if (!root.channels[channel]) return;
var instrument = root.channels[channel].instrument;
var instrumentId = root.GM.byId[instrument].id;
var note = notes[note];
if (note) {
var instrumentNoteId = instrumentId + '' + note.id;
var nid = (buffer_nid + 1) % audioBuffers.length;
var audio = audioBuffers[nid];
notesOn[ nid ] = instrumentNoteId;
if (!root.Soundfont[instrumentId]) {
if (root.DEBUG) {
console.log('404', instrumentId);
}
return;
}
audio.src = root.Soundfont[instrumentId][note.id];
audio.volume = volume / 127;
audio.play();
buffer_nid = nid;
}
};
var stopChannel = function(channel, note) {
if (!root.channels[channel]) return;
var instrument = root.channels[channel].instrument;
var instrumentId = root.GM.byId[instrument].id;
var note = notes[note];
if (note) {
var instrumentNoteId = instrumentId + '' + note.id;
for (var i = 0, len = audioBuffers.length; i < len; i++) {
var nid = (i + buffer_nid + 1) % len;
var cId = notesOn[nid];
if (cId && cId == instrumentNoteId) {
audioBuffers[nid].pause();
notesOn[nid] = null;
return;
}
}
}
};
midi.audioBuffers = audioBuffers;
midi.send = function(data, delay) { };
midi.setController = function(channel, type, value, delay) { };
midi.setVolume = function(channel, n) {
volume = n; //- should be channel specific volume
};
midi.programChange = function(channel, program) {
root.channels[channel].instrument = program;
};
midi.pitchBend = function(channel, program, delay) { };
midi.noteOn = function(channel, note, velocity, delay) {
var id = noteToKey[note];
if (!notes[id]) return;
if (delay) {
return setTimeout(function() {
playChannel(channel, id);
}, delay * 1000);
} else {
playChannel(channel, id);
}
};
midi.noteOff = function(channel, note, delay) {
// var id = noteToKey[note];
// if (!notes[id]) return;
// if (delay) {
// return setTimeout(function() {
// stopChannel(channel, id);
// }, delay * 1000)
// } else {
// stopChannel(channel, id);
// }
};
midi.chordOn = function(channel, chord, velocity, delay) {
for (var idx = 0; idx < chord.length; idx ++) {
var n = chord[idx];
var id = noteToKey[n];
if (!notes[id]) continue;
if (delay) {
return setTimeout(function() {
playChannel(channel, id);
}, delay * 1000);
} else {
playChannel(channel, id);
}
}
};
midi.chordOff = function(channel, chord, delay) {
for (var idx = 0; idx < chord.length; idx ++) {
var n = chord[idx];
var id = noteToKey[n];
if (!notes[id]) continue;
if (delay) {
return setTimeout(function() {
stopChannel(channel, id);
}, delay * 1000);
} else {
stopChannel(channel, id);
}
}
};
midi.stopAllNotes = function() {
for (var nid = 0, length = audioBuffers.length; nid < length; nid++) {
audioBuffers[nid].pause();
}
};
midi.connect = function(opts) {
root.setDefaultPlugin(midi);
///
for (var key in root.keyToNote) {
noteToKey[root.keyToNote[key]] = key;
notes[key] = {id: key};
}
///
opts.onsuccess && opts.onsuccess();
};
})();
})(MIDI);

View File

@ -1,326 +0,0 @@
/*
----------------------------------------------------------
Web Audio API - OGG or MPEG Soundbank
----------------------------------------------------------
http://webaudio.github.io/web-audio-api/
----------------------------------------------------------
*/
(function(root) { 'use strict';
window.AudioContext && (function() {
var audioContext = null; // new AudioContext();
var useStreamingBuffer = false; // !!audioContext.createMediaElementSource;
var midi = root.WebAudio = {api: 'webaudio'};
var ctx; // audio context
var sources = {};
var effects = {};
var masterVolume = 127;
var audioBuffers = {};
///
midi.audioBuffers = audioBuffers;
midi.send = function(data, delay) { };
midi.setController = function(channelId, type, value, delay) { };
midi.setVolume = function(channelId, volume, delay) {
if (delay) {
setTimeout(function() {
masterVolume = volume;
}, delay * 1000);
} else {
masterVolume = volume;
}
};
midi.programChange = function(channelId, program, delay) {
// if (delay) {
// return setTimeout(function() {
// var channel = root.channels[channelId];
// channel.instrument = program;
// }, delay);
// } else {
var channel = root.channels[channelId];
channel.instrument = program;
// }
};
midi.pitchBend = function(channelId, program, delay) {
// if (delay) {
// setTimeout(function() {
// var channel = root.channels[channelId];
// channel.pitchBend = program;
// }, delay);
// } else {
var channel = root.channels[channelId];
channel.pitchBend = program;
// }
};
midi.noteOn = function(channelId, noteId, velocity, delay) {
delay = delay || 0;
/// check whether the note exists
var channel = root.channels[channelId];
var instrument = channel.instrument;
var bufferId = instrument + '' + noteId;
var buffer = audioBuffers[bufferId];
if (!buffer) {
// console.log(MIDI.GM.byId[instrument].id, instrument, channelId);
return;
}
/// convert relative delay to absolute delay
if (delay < ctx.currentTime) {
delay += ctx.currentTime;
}
/// create audio buffer
if (useStreamingBuffer) {
var source = ctx.createMediaElementSource(buffer);
} else { // XMLHTTP buffer
var source = ctx.createBufferSource();
source.buffer = buffer;
}
/// add effects to buffer
if (effects) {
var chain = source;
for (var key in effects) {
chain.connect(effects[key].input);
chain = effects[key];
}
}
/// add gain + pitchShift
var gain = (velocity / 127) * (masterVolume / 127) * 2 - 1;
source.connect(ctx.destination);
source.playbackRate.value = 1; // pitch shift
source.gainNode = ctx.createGain(); // gain
source.gainNode.connect(ctx.destination);
source.gainNode.gain.value = Math.min(1.0, Math.max(-1.0, gain));
source.connect(source.gainNode);
///
if (useStreamingBuffer) {
if (delay) {
return setTimeout(function() {
buffer.currentTime = 0;
buffer.play()
}, delay * 1000);
} else {
buffer.currentTime = 0;
buffer.play()
}
} else {
source.start(delay || 0);
}
///
sources[channelId + '' + noteId] = source;
///
return source;
};
midi.noteOff = function(channelId, noteId, delay) {
delay = delay || 0;
/// check whether the note exists
var channel = root.channels[channelId];
var instrument = channel.instrument;
var bufferId = instrument + '' + noteId;
var buffer = audioBuffers[bufferId];
if (buffer) {
if (delay < ctx.currentTime) {
delay += ctx.currentTime;
}
///
var source = sources[channelId + '' + noteId];
if (source) {
if (source.gainNode) {
// @Miranet: 'the values of 0.2 and 0.3 could of course be used as
// a 'release' parameter for ADSR like time settings.'
// add { 'metadata': { release: 0.3 } } to soundfont files
var gain = source.gainNode.gain;
gain.linearRampToValueAtTime(gain.value, delay);
gain.linearRampToValueAtTime(-1.0, delay + 0.3);
}
///
if (useStreamingBuffer) {
if (delay) {
setTimeout(function() {
buffer.pause();
}, delay * 1000);
} else {
buffer.pause();
}
} else {
if (source.noteOff) {
source.noteOff(delay + 0.5);
} else {
source.stop(delay + 0.5);
}
}
///
delete sources[channelId + '' + noteId];
///
return source;
}
}
};
midi.chordOn = function(channel, chord, velocity, delay) {
var res = {};
for (var n = 0, note, len = chord.length; n < len; n++) {
res[note = chord[n]] = midi.noteOn(channel, note, velocity, delay);
}
return res;
};
midi.chordOff = function(channel, chord, delay) {
var res = {};
for (var n = 0, note, len = chord.length; n < len; n++) {
res[note = chord[n]] = midi.noteOff(channel, note, delay);
}
return res;
};
midi.stopAllNotes = function() {
for (var sid in sources) {
var delay = 0;
if (delay < ctx.currentTime) {
delay += ctx.currentTime;
}
var source = sources[sid];
source.gain.linearRampToValueAtTime(1, delay);
source.gain.linearRampToValueAtTime(0, delay + 0.3);
if (source.noteOff) { // old api
source.noteOff(delay + 0.3);
} else { // new api
source.stop(delay + 0.3);
}
delete sources[sid];
}
};
midi.setEffects = function(list) {
if (ctx.tunajs) {
for (var n = 0; n < list.length; n ++) {
var data = list[n];
var effect = new ctx.tunajs[data.type](data);
effect.connect(ctx.destination);
effects[data.type] = effect;
}
} else {
return console.log('Effects module not installed.');
}
};
midi.connect = function(opts) {
root.setDefaultPlugin(midi);
midi.setContext(ctx || createAudioContext(), opts.onsuccess);
};
midi.getContext = function() {
return ctx;
};
midi.setContext = function(newCtx, onload, onprogress, onerror) {
ctx = newCtx;
/// tuna.js effects module - https://github.com/Dinahmoe/tuna
if (typeof Tuna !== 'undefined' && !ctx.tunajs) {
ctx.tunajs = new Tuna(ctx);
}
/// loading audio files
var urls = [];
var notes = root.keyToNote;
for (var key in notes) urls.push(key);
///
var waitForEnd = function(instrument) {
for (var key in bufferPending) { // has pending items
if (bufferPending[key]) return;
}
///
if (onload) { // run onload once
onload();
onload = null;
}
};
///
var requestAudio = function(soundfont, instrumentId, index, key) {
var url = soundfont[key];
if (url) {
bufferPending[instrumentId] ++;
loadAudio(url, function(buffer) {
buffer.id = key;
var noteId = root.keyToNote[key];
audioBuffers[instrumentId + '' + noteId] = buffer;
///
if (-- bufferPending[instrumentId] === 0) {
var percent = index / 87;
// console.log(MIDI.GM.byId[instrumentId], 'processing: ', percent);
soundfont.isLoaded = true;
waitForEnd(instrument);
}
}, function(err) {
// console.log(err);
});
}
};
///
var bufferPending = {};
for (var instrument in root.Soundfont) {
var soundfont = root.Soundfont[instrument];
if (soundfont.isLoaded) {
continue;
}
///
var synth = root.GM.byName[instrument];
var instrumentId = synth.number;
///
bufferPending[instrumentId] = 0;
///
for (var index = 0; index < urls.length; index++) {
var key = urls[index];
requestAudio(soundfont, instrumentId, index, key);
}
}
///
setTimeout(waitForEnd, 1);
};
/* Load audio file: streaming | base64 | arraybuffer
---------------------------------------------------------------------- */
function loadAudio(url, onload, onerror) {
if (useStreamingBuffer) {
var audio = new Audio();
audio.src = url;
audio.controls = false;
audio.autoplay = false;
audio.preload = false;
audio.addEventListener('canplay', function() {
onload && onload(audio);
});
audio.addEventListener('error', function(err) {
onerror && onerror(err);
});
document.body.appendChild(audio);
} else if (url.indexOf('data:audio') === 0) { // Base64 string
var base64 = url.split(',')[1];
var buffer = Base64Binary.decodeArrayBuffer(base64);
ctx.decodeAudioData(buffer, onload, onerror);
} else { // XMLHTTP buffer
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onload = function() {
ctx.decodeAudioData(request.response, onload, onerror);
};
request.send();
}
};
function createAudioContext() {
return new (window.AudioContext || window.webkitAudioContext)();
};
})();
})(MIDI);

View File

@ -1,93 +0,0 @@
/*
----------------------------------------------------------------------
Web MIDI API - Native Soundbanks
----------------------------------------------------------------------
http://webaudio.github.io/web-midi-api/
----------------------------------------------------------------------
*/
(function(root) { 'use strict';
var plugin = null;
var output = null;
var channels = [];
var midi = root.WebMIDI = {api: 'webmidi'};
midi.send = function(data, delay) { // set channel volume
output.send(data, delay * 1000);
};
midi.setController = function(channel, type, value, delay) {
output.send([channel, type, value], delay * 1000);
};
midi.setVolume = function(channel, volume, delay) { // set channel volume
output.send([0xB0 + channel, 0x07, volume], delay * 1000);
};
midi.programChange = function(channel, program, delay) { // change patch (instrument)
output.send([0xC0 + channel, program], delay * 1000);
};
midi.pitchBend = function(channel, program, delay) { // pitch bend
output.send([0xE0 + channel, program], delay * 1000);
};
midi.noteOn = function(channel, note, velocity, delay) {
output.send([0x90 + channel, note, velocity], delay * 1000);
};
midi.noteOff = function(channel, note, delay) {
output.send([0x80 + channel, note, 0], delay * 1000);
};
midi.chordOn = function(channel, chord, velocity, delay) {
for (var n = 0; n < chord.length; n ++) {
var note = chord[n];
output.send([0x90 + channel, note, velocity], delay * 1000);
}
};
midi.chordOff = function(channel, chord, delay) {
for (var n = 0; n < chord.length; n ++) {
var note = chord[n];
output.send([0x80 + channel, note, 0], delay * 1000);
}
};
midi.stopAllNotes = function() {
output.cancel();
for (var channel = 0; channel < 16; channel ++) {
output.send([0xB0 + channel, 0x7B, 0]);
}
};
midi.connect = function(opts) {
root.setDefaultPlugin(midi);
var errFunction = function(err) { // well at least we tried!
if (window.AudioContext) { // Chrome
opts.api = 'webaudio';
} else if (window.Audio) { // Firefox
opts.api = 'audiotag';
} else { // no support
return;
}
root.loadPlugin(opts);
};
///
navigator.requestMIDIAccess().then(function(access) {
plugin = access;
var pluginOutputs = plugin.outputs;
if (typeof pluginOutputs == 'function') { // Chrome pre-43
output = pluginOutputs()[0];
} else { // Chrome post-43
output = pluginOutputs[0];
}
if (output === undefined) { // nothing there...
errFunction();
} else {
opts.onsuccess && opts.onsuccess();
}
}, errFunction);
};
})(MIDI);

View File

@ -1,320 +0,0 @@
/*
----------------------------------------------------------
MIDI.Synesthesia : 0.3.1 : 2012-01-06
----------------------------------------------------------
Peacock: Instruments to perform color-music: Two centuries of technological experimentation, Leonardo, 21 (1988), 397-406.
Gerstner: Karl Gerstner, The Forms of Color 1986
Klein: Colour-Music: The art of light, London: Crosby Lockwood and Son, 1927.
Jameson: Visual music in a visual programming language, IEEE Symposium on Visual Languages, 1999, 111-118.
Helmholtz: Treatise on Physiological Optics, New York: Dover Books, 1962
Jones: The art of light & color, New York: Van Nostrand Reinhold, 1972
----------------------------------------------------------
Reference: http://rhythmiclight.com/archives/ideas/colorscales.html
----------------------------------------------------------
*/
if (typeof MIDI === 'undefined') var MIDI = {};
MIDI.Synesthesia = MIDI.Synesthesia || {};
(function(root) {
root.data = {
'Isaac Newton (1704)': {
format: 'HSL',
ref: 'Gerstner, p.167',
english: ['red',null,'orange',null,'yellow','green',null,'blue',null,'indigo',null,'violet'],
0: [ 0, 96, 51 ], // C
1: [ 0, 0, 0 ], // C#
2: [ 29, 94, 52 ], // D
3: [ 0, 0, 0 ], // D#
4: [ 60, 90, 60 ], // E
5: [ 135, 76, 32 ], // F
6: [ 0, 0, 0 ], // F#
7: [ 248, 82, 28 ], // G
8: [ 0, 0, 0 ], // G#
9: [ 302, 88, 26 ], // A
10: [ 0, 0, 0 ], // A#
11: [ 325, 84, 46 ] // B
},
'Louis Bertrand Castel (1734)': {
format: 'HSL',
ref: 'Peacock, p.400',
english: ['blue','blue-green','green','olive green','yellow','yellow-orange','orange','red','crimson','violet','agate','indigo'],
0: [ 248, 82, 28 ],
1: [ 172, 68, 34 ],
2: [ 135, 76, 32 ],
3: [ 79, 59, 36 ],
4: [ 60, 90, 60 ],
5: [ 49, 90, 60 ],
6: [ 29, 94, 52 ],
7: [ 360, 96, 51 ],
8: [ 1, 89, 33 ],
9: [ 325, 84, 46 ],
10: [ 273, 80, 27 ],
11: [ 302, 88, 26 ]
},
'George Field (1816)': {
format: 'HSL',
ref: 'Klein, p.69',
english: ['blue',null,'purple',null,'red','orange',null,'yellow',null,'yellow green',null,'green'],
0: [ 248, 82, 28 ],
1: [ 0, 0, 0 ],
2: [ 302, 88, 26 ],
3: [ 0, 0, 0 ],
4: [ 360, 96, 51 ],
5: [ 29, 94, 52 ],
6: [ 0, 0, 0 ],
7: [ 60, 90, 60 ],
8: [ 0, 0, 0 ],
9: [ 79, 59, 36 ],
10: [ 0, 0, 0 ],
11: [ 135, 76, 32 ]
},
'D. D. Jameson (1844)': {
format: 'HSL',
ref: 'Jameson, p.12',
english: ['red','red-orange','orange','orange-yellow','yellow','green','green-blue','blue','blue-purple','purple','purple-violet','violet'],
0: [ 360, 96, 51 ],
1: [ 14, 91, 51 ],
2: [ 29, 94, 52 ],
3: [ 49, 90, 60 ],
4: [ 60, 90, 60 ],
5: [ 135, 76, 32 ],
6: [ 172, 68, 34 ],
7: [ 248, 82, 28 ],
8: [ 273, 80, 27 ],
9: [ 302, 88, 26 ],
10: [ 313, 78, 37 ],
11: [ 325, 84, 46 ]
},
'Theodor Seemann (1881)': {
format: 'HSL',
ref: 'Klein, p.86',
english: ['carmine','scarlet','orange','yellow-orange','yellow','green','green blue','blue','indigo','violet','brown','black'],
0: [ 0, 58, 26 ],
1: [ 360, 96, 51 ],
2: [ 29, 94, 52 ],
3: [ 49, 90, 60 ],
4: [ 60, 90, 60 ],
5: [ 135, 76, 32 ],
6: [ 172, 68, 34 ],
7: [ 248, 82, 28 ],
8: [ 302, 88, 26 ],
9: [ 325, 84, 46 ],
10: [ 0, 58, 26 ],
11: [ 0, 0, 3 ]
},
'A. Wallace Rimington (1893)': {
format: 'HSL',
ref: 'Peacock, p.402',
english: ['deep red','crimson','orange-crimson','orange','yellow','yellow-green','green','blueish green','blue-green','indigo','deep blue','violet'],
0: [ 360, 96, 51 ],
1: [ 1, 89, 33 ],
2: [ 14, 91, 51 ],
3: [ 29, 94, 52 ],
4: [ 60, 90, 60 ],
5: [ 79, 59, 36 ],
6: [ 135, 76, 32 ],
7: [ 163, 62, 40 ],
8: [ 172, 68, 34 ],
9: [ 302, 88, 26 ],
10: [ 248, 82, 28 ],
11: [ 325, 84, 46 ]
},
'Bainbridge Bishop (1893)': {
format: 'HSL',
ref: 'Bishop, p.11',
english: ['red','orange-red or scarlet','orange','gold or yellow-orange','yellow or green-gold','yellow-green','green','greenish-blue or aquamarine','blue','indigo or violet-blue','violet','violet-red','red'],
0: [ 360, 96, 51 ],
1: [ 1, 89, 33 ],
2: [ 29, 94, 52 ],
3: [ 50, 93, 52 ],
4: [ 60, 90, 60 ],
5: [ 73, 73, 55 ],
6: [ 135, 76, 32 ],
7: [ 163, 62, 40 ],
8: [ 302, 88, 26 ],
9: [ 325, 84, 46 ],
10: [ 343, 79, 47 ],
11: [ 360, 96, 51 ]
},
'H. von Helmholtz (1910)': {
format: 'HSL',
ref: 'Helmholtz, p.22',
english: ['yellow','green','greenish blue','cayan-blue','indigo blue','violet','end of red','red','red','red','red orange','orange'],
0: [ 60, 90, 60 ],
1: [ 135, 76, 32 ],
2: [ 172, 68, 34 ],
3: [ 211, 70, 37 ],
4: [ 302, 88, 26 ],
5: [ 325, 84, 46 ],
6: [ 330, 84, 34 ],
7: [ 360, 96, 51 ],
8: [ 10, 91, 43 ],
9: [ 10, 91, 43 ],
10: [ 8, 93, 51 ],
11: [ 28, 89, 50 ]
},
'Alexander Scriabin (1911)': {
format: 'HSL',
ref: 'Jones, p.104',
english: ['red','violet','yellow','steely with the glint of metal','pearly blue the shimmer of moonshine','dark red','bright blue','rosy orange','purple','green','steely with a glint of metal','pearly blue the shimmer of moonshine'],
0: [ 360, 96, 51 ],
1: [ 325, 84, 46 ],
2: [ 60, 90, 60 ],
3: [ 245, 21, 43 ],
4: [ 211, 70, 37 ],
5: [ 1, 89, 33 ],
6: [ 248, 82, 28 ],
7: [ 29, 94, 52 ],
8: [ 302, 88, 26 ],
9: [ 135, 76, 32 ],
10: [ 245, 21, 43 ],
11: [ 211, 70, 37 ]
},
'Adrian Bernard Klein (1930)': {
format: 'HSL',
ref: 'Klein, p.209',
english: ['dark red','red','red orange','orange','yellow','yellow green','green','blue-green','blue','blue violet','violet','dark violet'],
0: [ 0, 91, 40 ],
1: [ 360, 96, 51 ],
2: [ 14, 91, 51 ],
3: [ 29, 94, 52 ],
4: [ 60, 90, 60 ],
5: [ 73, 73, 55 ],
6: [ 135, 76, 32 ],
7: [ 172, 68, 34 ],
8: [ 248, 82, 28 ],
9: [ 292, 70, 31 ],
10: [ 325, 84, 46 ],
11: [ 330, 84, 34 ]
},
'August Aeppli (1940)': {
format: 'HSL',
ref: 'Gerstner, p.169',
english: ['red',null,'orange',null,'yellow',null,'green','blue-green',null,'ultramarine blue','violet','purple'],
0: [ 0, 96, 51 ],
1: [ 0, 0, 0 ],
2: [ 29, 94, 52 ],
3: [ 0, 0, 0 ],
4: [ 60, 90, 60 ],
5: [ 0, 0, 0 ],
6: [ 135, 76, 32 ],
7: [ 172, 68, 34 ],
8: [ 0, 0, 0 ],
9: [ 211, 70, 37 ],
10: [ 273, 80, 27 ],
11: [ 302, 88, 26 ]
},
'I. J. Belmont (1944)': {
ref: 'Belmont, p.226',
english: ['red','red-orange','orange','yellow-orange','yellow','yellow-green','green','blue-green','blue','blue-violet','violet','red-violet'],
0: [ 360, 96, 51 ],
1: [ 14, 91, 51 ],
2: [ 29, 94, 52 ],
3: [ 50, 93, 52 ],
4: [ 60, 90, 60 ],
5: [ 73, 73, 55 ],
6: [ 135, 76, 32 ],
7: [ 172, 68, 34 ],
8: [ 248, 82, 28 ],
9: [ 313, 78, 37 ],
10: [ 325, 84, 46 ],
11: [ 338, 85, 37 ]
},
'Steve Zieverink (2004)': {
format: 'HSL',
ref: 'Cincinnati Contemporary Art Center',
english: ['yellow-green','green','blue-green','blue','indigo','violet','ultra violet','infra red','red','orange','yellow-white','yellow'],
0: [ 73, 73, 55 ],
1: [ 135, 76, 32 ],
2: [ 172, 68, 34 ],
3: [ 248, 82, 28 ],
4: [ 302, 88, 26 ],
5: [ 325, 84, 46 ],
6: [ 326, 79, 24 ],
7: [ 1, 89, 33 ],
8: [ 360, 96, 51 ],
9: [ 29, 94, 52 ],
10: [ 62, 78, 74 ],
11: [ 60, 90, 60 ]
},
'Circle of Fifths (Johnston 2003)': {
format: 'RGB',
ref: 'Joseph Johnston',
english: ['yellow', 'blue', 'orange', 'teal', 'red', 'green', 'purple', 'light orange', 'light blue', 'dark orange', 'dark green', 'violet' ],
0: [ 255, 255, 0 ],
1: [ 50, 0, 255 ],
2: [ 255, 150, 0 ],
3: [ 0, 210, 180 ],
4: [ 255, 0, 0 ],
5: [ 130, 255, 0 ],
6: [ 150, 0, 200 ],
7: [ 255, 195, 0 ],
8: [ 30, 130, 255 ],
9: [ 255, 100, 0 ],
10: [ 0, 200, 0 ],
11: [ 225, 0, 225 ]
},
'Circle of Fifths (Wheatman 2002)': {
format: 'HEX',
ref: 'Stuart Wheatman', // http://www.valleysfamilychurch.org/
english: [],
data: ['#122400', '#2E002E', '#002914', '#470000', '#002142', '#2E2E00', '#290052', '#003D00', '#520029', '#003D3D', '#522900', '#000080', '#244700', '#570057', '#004D26', '#7A0000', '#003B75', '#4C4D00', '#47008F', '#006100', '#850042', '#005C5C', '#804000', '#0000C7', '#366B00', '#80007F', '#00753B', '#B80000', '#0057AD', '#6B6B00', '#6600CC', '#008A00', '#B8005C', '#007F80', '#B35900', '#2424FF', '#478F00', '#AD00AD', '#00994D', '#F00000', '#0073E6', '#8F8F00', '#8A14FF', '#00AD00', '#EB0075', '#00A3A3', '#E07000', '#6B6BFF', '#5CB800', '#DB00DB', '#00C261', '#FF5757', '#3399FF', '#ADAD00', '#B56BFF', '#00D600', '#FF57AB', '#00C7C7', '#FF9124', '#9999FF', '#6EDB00', '#FF29FF', '#00E070', '#FF9999', '#7ABDFF', '#D1D100', '#D1A3FF', '#00FA00', '#FFA3D1', '#00E5E6', '#FFC285', '#C2C2FF', '#80FF00', '#FFA8FF', '#00E070', '#FFCCCC', '#C2E0FF', '#F0F000', '#EBD6FF', '#ADFFAD', '#FFD6EB', '#8AFFFF', '#FFEBD6', '#EBEBFF', '#E0FFC2', '#FFEBFF', '#E5FFF2', '#FFF5F5'] }
};
root.map = function(type) {
var data = {};
var blend = function(a, b) {
return [ // blend two colors and round results
(a[0] * 0.5 + b[0] * 0.5 + 0.5) >> 0,
(a[1] * 0.5 + b[1] * 0.5 + 0.5) >> 0,
(a[2] * 0.5 + b[2] * 0.5 + 0.5) >> 0
];
};
///
var syn = root.data;
var colors = syn[type] || syn['D. D. Jameson (1844)'];
for (var note = 0, pclr, H, S, L; note <= 88; note ++) { // creates mapping for 88 notes
if (colors.data) {
data[note] = {
hsl: colors.data[note],
hex: colors.data[note]
};
} else {
var clr = colors[(note + 9) % 12];
///
switch(colors.format) {
case 'RGB':
clr = Color.Space(clr, 'RGB>HSL');
H = clr.H >> 0;
S = clr.S >> 0;
L = clr.L >> 0;
break;
case 'HSL':
H = clr[0];
S = clr[1];
L = clr[2];
break;
}
///
if (H === S && S === L) { // note color is unset
clr = blend(pclr, colors[(note + 10) % 12]);
}
///
// var amount = L / 10;
// var octave = note / 12 >> 0;
// var octaveLum = L + amount * octave - 3.0 * amount; // map luminance to octave
///
data[note] = {
hsl: 'hsla(' + H + ',' + S + '%,' + L + '%, 1)',
hex: Color.Space({H: H, S: S, L: L}, 'HSL>RGB>HEX>W3')
};
///
pclr = clr;
}
}
return data;
};
})(MIDI.Synesthesia);

View File

@ -1,225 +0,0 @@
/*
-----------------------------------------------------------
dom.loadScript.js : 0.1.4 : 2014/02/12 : http://mudcu.be
-----------------------------------------------------------
Copyright 2011-2014 Mudcube. All rights reserved.
-----------------------------------------------------------
/// No verification
dom.loadScript.add("../js/jszip/jszip.js");
/// Strict loading order and verification.
dom.loadScript.add({
strictOrder: true,
urls: [
{
url: "../js/jszip/jszip.js",
verify: "JSZip",
onsuccess: function() {
console.log(1)
}
},
{
url: "../inc/downloadify/js/swfobject.js",
verify: "swfobject",
onsuccess: function() {
console.log(2)
}
}
],
onsuccess: function() {
console.log(3)
}
});
/// Just verification.
dom.loadScript.add({
url: "../js/jszip/jszip.js",
verify: "JSZip",
onsuccess: function() {
console.log(1)
}
});
*/
if (typeof(dom) === "undefined") var dom = {};
(function() { "use strict";
dom.loadScript = function() {
this.loaded = {};
this.loading = {};
return this;
};
dom.loadScript.prototype.add = function(config) {
var that = this;
if (typeof(config) === "string") {
config = { url: config };
}
var urls = config.urls;
if (typeof(urls) === "undefined") {
urls = [{
url: config.url,
verify: config.verify
}];
}
/// adding the elements to the head
var doc = document.getElementsByTagName("head")[0];
///
var testElement = function(element, test) {
if (that.loaded[element.url]) return;
if (test && globalExists(test) === false) return;
that.loaded[element.url] = true;
//
if (that.loading[element.url]) that.loading[element.url]();
delete that.loading[element.url];
//
if (element.onsuccess) element.onsuccess();
if (typeof(getNext) !== "undefined") getNext();
};
///
var hasError = false;
var batchTest = [];
var addElement = function(element) {
if (typeof(element) === "string") {
element = {
url: element,
verify: config.verify
};
}
if (/([\w\d.\[\]\'\"])$/.test(element.verify)) { // check whether its a variable reference
var verify = element.test = element.verify;
if (typeof(verify) === "object") {
for (var n = 0; n < verify.length; n ++) {
batchTest.push(verify[n]);
}
} else {
batchTest.push(verify);
}
}
if (that.loaded[element.url]) return;
var script = document.createElement("script");
script.onreadystatechange = function() {
if (this.readyState !== "loaded" && this.readyState !== "complete") return;
testElement(element);
};
script.onload = function() {
testElement(element);
};
script.onerror = function() {
hasError = true;
delete that.loading[element.url];
if (typeof(element.test) === "object") {
for (var key in element.test) {
removeTest(element.test[key]);
}
} else {
removeTest(element.test);
}
};
script.setAttribute("type", "text/javascript");
script.setAttribute("src", element.url);
doc.appendChild(script);
that.loading[element.url] = function() {};
};
/// checking to see whether everything loaded properly
var removeTest = function(test) {
var ret = [];
for (var n = 0; n < batchTest.length; n ++) {
if (batchTest[n] === test) continue;
ret.push(batchTest[n]);
}
batchTest = ret;
};
var onLoad = function(element) {
if (element) {
testElement(element, element.test);
} else {
for (var n = 0; n < urls.length; n ++) {
testElement(urls[n], urls[n].test);
}
}
var istrue = true;
for (var n = 0; n < batchTest.length; n ++) {
if (globalExists(batchTest[n]) === false) {
istrue = false;
}
}
if (!config.strictOrder && istrue) { // finished loading all the requested scripts
if (hasError) {
if (config.error) {
config.error();
}
} else if (config.onsuccess) {
config.onsuccess();
}
} else { // keep calling back the function
setTimeout(function() { //- should get slower over time?
onLoad(element);
}, 10);
}
};
/// loading methods; strict ordering or loose ordering
if (config.strictOrder) {
var ID = -1;
var getNext = function() {
ID ++;
if (!urls[ID]) { // all elements are loaded
if (hasError) {
if (config.error) {
config.error();
}
} else if (config.onsuccess) {
config.onsuccess();
}
} else { // loading new script
var element = urls[ID];
var url = element.url;
if (that.loading[url]) { // already loading from another call (attach to event)
that.loading[url] = function() {
if (element.onsuccess) element.onsuccess();
getNext();
}
} else if (!that.loaded[url]) { // create script element
addElement(element);
onLoad(element);
} else { // it's already been successfully loaded
getNext();
}
}
};
getNext();
} else { // loose ordering
for (var ID = 0; ID < urls.length; ID ++) {
addElement(urls[ID]);
onLoad(urls[ID]);
}
}
};
dom.loadScript = new dom.loadScript();
var globalExists = function(path, root) {
try {
path = path.split('"').join('').split("'").join('').split(']').join('').split('[').join('.');
var parts = path.split(".");
var length = parts.length;
var object = root || window;
for (var n = 0; n < length; n ++) {
var key = parts[n];
if (object[key] == null) {
return false;
} else { //
object = object[key];
}
}
return true;
} catch(e) {
return false;
}
};
})();
/// For NodeJS
if (typeof (module) !== "undefined" && module.exports) {
module.exports = dom.loadScript;
}

View File

@ -1,146 +0,0 @@
/*
----------------------------------------------------------
util/Request : 0.1.1 : 2015-03-26
----------------------------------------------------------
util.request({
url: './dir/something.extension',
data: 'test!',
format: 'text', // text | xml | json | binary
responseType: 'text', // arraybuffer | blob | document | json | text
headers: {},
withCredentials: true, // true | false
///
onerror: function(evt, percent) {
console.log(evt);
},
onsuccess: function(evt, responseText) {
console.log(responseText);
},
onprogress: function(evt, percent) {
percent = Math.round(percent * 100);
loader.create('thread', 'loading... ', percent);
}
});
*/
if (typeof MIDI === 'undefined') MIDI = {};
(function(root) {
var util = root.util || (root.util = {});
util.request = function(opts, onsuccess, onerror, onprogress) { 'use strict';
if (typeof opts === 'string') opts = {url: opts};
///
var data = opts.data;
var url = opts.url;
var method = opts.method || (opts.data ? 'POST' : 'GET');
var format = opts.format;
var headers = opts.headers;
var responseType = opts.responseType;
var withCredentials = opts.withCredentials || false;
///
var onsuccess = onsuccess || opts.onsuccess;
var onerror = onerror || opts.onerror;
var onprogress = onprogress || opts.onprogress;
///
if (typeof NodeFS !== 'undefined' && root.loc.isLocalUrl(url)) {
NodeFS.readFile(url, 'utf8', function(err, res) {
if (err) {
onerror && onerror(err);
} else {
onsuccess && onsuccess({responseText: res});
}
});
return;
}
///
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
///
if (headers) {
for (var type in headers) {
xhr.setRequestHeader(type, headers[type]);
}
} else if (data) { // set the default headers for POST
xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
}
if (format === 'binary') { //- default to responseType="blob" when supported
if (xhr.overrideMimeType) {
xhr.overrideMimeType('text/plain; charset=x-user-defined');
}
}
if (responseType) {
xhr.responseType = responseType;
}
if (withCredentials) {
xhr.withCredentials = 'true';
}
if (onerror && 'onerror' in xhr) {
xhr.onerror = onerror;
}
if (onprogress && xhr.upload && 'onprogress' in xhr.upload) {
if (data) {
xhr.upload.onprogress = function(evt) {
onprogress.call(xhr, evt, event.loaded / event.total);
};
} else {
xhr.addEventListener('progress', function(evt) {
var totalBytes = 0;
if (evt.lengthComputable) {
totalBytes = evt.total;
} else if (xhr.totalBytes) {
totalBytes = xhr.totalBytes;
} else {
var rawBytes = parseInt(xhr.getResponseHeader('Content-Length-Raw'));
if (isFinite(rawBytes)) {
xhr.totalBytes = totalBytes = rawBytes;
} else {
return;
}
}
onprogress.call(xhr, evt, evt.loaded / totalBytes);
});
}
}
///
xhr.onreadystatechange = function(evt) {
if (xhr.readyState === 4) { // The request is complete
if (xhr.status === 200 || // Response OK
xhr.status === 304 || // Not Modified
xhr.status === 308 || // Permanent Redirect
xhr.status === 0 && root.client.cordova // Cordova quirk
) {
if (onsuccess) {
var res;
if (format === 'xml') {
res = evt.target.responseXML;
} else if (format === 'text') {
res = evt.target.responseText;
} else if (format === 'json') {
try {
res = JSON.parse(evt.target.response);
} catch(err) {
onerror && onerror.call(xhr, evt);
}
}
///
onsuccess.call(xhr, evt, res);
}
} else {
onerror && onerror.call(xhr, evt);
}
}
};
xhr.send(data);
return xhr;
};
/// NodeJS
if (typeof module !== 'undefined' && module.exports) {
var NodeFS = require('fs');
XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest;
module.exports = root.util.request;
}
})(MIDI);

View File

@ -1,70 +0,0 @@
var midiLoaded = false;
function loadMIDI() {
MIDI.loadPlugin({
soundfontUrl: "/assets/trading-in-the-rain/soundfont/",
instrument: "acoustic_grand_piano",
onprogress: (state, progress) => {
console.log("MIDI loading...", progress*100, "%");
},
onsuccess: () => {
console.log("MIDI is ready to be used");
MIDI.setVolume(0, 127);
midiLoaded = true;
},
});
}
function MusicBox(priceDist, volumeDist) {
this.priceDist = priceDist;
this.volumeDist = volumeDist;
// clamp the keyboard so we're not using the very low notes, they don't sound
// good.
const noteRange = {
//low: 21,
//low: 36, // C2
low: 60, // C4, middle C
high: 108
};
function makeScale(tpl) {
tplObj = {};
for (i in tpl) {
tplObj[tpl[i]] = true;
}
let scale = [];
for (let note=noteRange.low; note<=noteRange.high; note++) {
let key = MIDI.noteToKey[note].replace(/\d+$/, "");
if (tplObj[key]) {
scale.push(note);
}
}
return scale;
}
//this.scale = makeScale(["C", "D", "E", "F", "G", "A", "B"]); // cMajor
//this.scale = makeScale(["D", "E", "Gb", "G", "A", "Db"]); //dMajor
//this.scale = makeScale(["C", "D", "E", "G", "A"]); // cMajor pentatonic
this.scale = makeScale(["F", "G", "A", "C", "D"]); // fMajor pentatonic
this.playNote = (note, holdFor) => {
if (!midiLoaded) return;
let velocity = 127;
MIDI.noteOn(0, note, velocity, 0);
MIDI.noteOff(0, note, holdFor);
};
this.playTrades = (trades) => {
if (this.priceDist.length == 0) return;
for (let i in trades) {
let noteIdx = this.priceDist.distribute(trades[i].price, 0, this.scale.length-1);
noteIdx = Math.round(noteIdx);
let holdFor = 0.25 + this.volumeDist.distribute(trades[i].volume, 0, 1.75);
let note = this.scale[noteIdx];
this.playNote(note, holdFor);
}
};
}

View File

@ -1,74 +0,0 @@
function RainCanvas(canvasDOM) {
this.canvas = canvasDOM;
this.ctx = this.canvas.getContext("2d");
this.drops = [];
this.tick = 0;
// drop: {x, y, intensity, color} (all in range [0, 1], except color which is
// an array [r,g,b])
this.newDrop = (newDrop) => {
if (!document.hasFocus()) return;
// scale intensity up a bit right off the bat. If the intensity was near 0
// the drop wouldn't actually show up at all.
newDrop.intensity = distribute(newDrop.intensity, 0, 1, 0.1, 1);
newDrop.tick = this.tick;
this.drops.push(newDrop);
};
// alpha isn't really alpha, it's used to determine line width, but it plays
// the same role.
this.drawDrop = (drop, alpha) => {
let cW = this.canvas.width, cH = this.canvas.height;
let minDim = Math.min(cW, cH);
let tickDiff = this.tick - drop.tick;
let radius = tickDiff * (minDim / 250);
let x = distribute(drop.x, 0, 1, cW*0.1, cW*0.9);
let y = distribute(drop.y, 0, 1, cH*0.1, cH*0.9);
this.ctx.beginPath();
this.ctx.arc(x, y, radius, 0, Math.PI * 2, false);
this.ctx.closePath();
// multiple lineWidth by alpha so that the line width drops over time in
// correspondence with the opacity.
this.ctx.lineWidth = distribute(drop.intensity, 0, 1, 2, 9) * alpha;
let r = drop.color[0], g = drop.color[1], b = drop.color[2];
this.ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, 1)`;
this.ctx.stroke();
};
let requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame;
this.doTick = () => {
this.canvas.width = window.innerWidth;
this.canvas.height = window.innerHeight;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
let newDrops = [];
for (let i in this.drops) {
let drop = this.drops[i];
let alpha = distribute(
this.tick - drop.tick,
0, 200 * drop.intensity,
1, 0,
);
if (alpha <= 0) continue;
this.drawDrop(drop, alpha);
newDrops.push(drop);
}
this.drops = newDrops;
this.tick++;
requestAnimationFrame(this.doTick);
};
requestAnimationFrame(this.doTick);
}

View File

@ -1,51 +0,0 @@
function SeriesComposer(resource, rainCanvas, color) {
this.rainCanvas = rainCanvas;
this.color = color;
this.priceDist = new Distributor(200);
this.volumeDist = new Distributor(200);
this.musicBox = new MusicBox(this.priceDist, this.volumeDist);
this.enabled = false;
this.setEnabled = (enabled) => this.enabled = enabled;
this.getEnabled = () => { return this.enabled; }
this.totalTrades = 0;
this.getTotalTrades = () => { return this.totalTrades; }
this.cw = new CW(resource);
this.cw.ontrades = (trades) => {
if (this.totalTrades > 0 && this.enabled) {
let priceVols = {}; // sum volumes by price, for deduplication
for (let i in trades) {
let price = trades[i].price, volume = trades[i].volume;
if (!priceVols[price]) priceVols[price] = 0;
priceVols[price] += volume;
}
trades = []; // overwrite trades with deduplicated ones.
for (let price in priceVols) {
let volume = priceVols[price];
let intensity = this.volumeDist.distribute(volume, 0, 1);
this.rainCanvas.newDrop({
x: this.priceDist.distribute(price, 0, 1),
y: Math.random(),
intensity: intensity,
color: this.color,
});
trades.push({price: price, volume: volume});
}
this.musicBox.playTrades(trades);
}
for (let i in trades) {
this.priceDist.add(trades[i].price);
this.volumeDist.add(trades[i].volume);
}
this.totalTrades += trades.length;
if (this.ontrades) this.ontrades(trades);
};
}

Binary file not shown.

View File

@ -1,144 +0,0 @@
const tradesToWaitFor = 5;
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return [
parseInt(result[1], 16),
parseInt(result[2], 16),
parseInt(result[3], 16),
];
}
const colorPalette = [
"#28D2EE",
"#ED778E",
"#6557DC",
"#EEE386",
"#B55AA0",
];
// Use https://api.cryptowat.ch/markets/<exchange>
// or https://api.cryptowat.ch/pairs (for "all")
const markets = {
"kraken":[
{
name: "BTCUSD",
resource: "markets:87:trades",
},
{
name: "BTCEUR",
resource: "markets:86:trades",
},
{
name: "BTCEUR",
resource: "markets:96:trades",
},
{
name: "ETHEUR",
resource: "markets:97:trades",
},
{
name: "BCHUSD",
resource: "markets:146:trades",
},
],
"bitfinex":[
{
name: "BTCUSD",
resource: "markets:1:trades",
},
{
name: "ETHUSD",
resource: "markets:4:trades",
},
{
name: "BSVUSD",
resource: "markets:5558:trades",
},
{
name: "BTCEUR",
resource: "markets:415:trades",
},
{
name: "XRPUSD",
resource: "markets:25:trades",
},
],
"all": [
{
name: "BTCUSD",
resource: "instruments:9:trades",
},
{
name: "ETHUSD",
resource: "instruments:125:trades",
},
{
name: "LTCUSD",
resource: "instruments:138:trades",
},
{
name: "EOSUSD",
resource: "instruments:4:trades",
},
{
name: "XRPUSD",
resource: "instruments:160:trades",
},
],
};
const exchange = "all";
function fillMarketP() {
let marketsEl = document.getElementById("markets");
for (let i in markets[exchange]) {
let name = markets[exchange][i].name;
let color = colorPalette[i];
if (i > 0) marketsEl.innerHTML += "</br>";
marketsEl.innerHTML += `<strong style="color: ${color}; font-size: 2rem;">${name}</strong>`;
}
}
function run() {
document.getElementById("button").style.display = "none";
let progress = document.getElementById("progress");
progress.innerHTML = "Connecting to Cryptowat.ch...";
let canvas = document.getElementById("rainCanvas");
let rainCanvas = new RainCanvas(canvas);
let modalHidden = false;
for (let i in markets[exchange]) {
let seriesComposer = new SeriesComposer(
markets[exchange][i].resource,
rainCanvas,
hexToRgb(colorPalette[i]),
);
seriesComposer.cw.onconnect = () => {
progress.innerHTML = "Preloading a few trades before continuing.";
};
// wait for each series to rech tradesToWaitFor before letting it begin.
// Hide the modal when the first series is enabled.
seriesComposer.ontrades = (trades) => {
if (!modalHidden && seriesComposer.getTotalTrades() < tradesToWaitFor) {
progress.innerHTML += "."; // indicate that _something_ is happening
return;
}
if (!modalHidden) {
let modal = document.getElementById("tradingInRainModal");
modal.style.display = "none";
modalHidden = true;
}
seriesComposer.setEnabled(true);
seriesComposer.ontrades = undefined;
};
}
}
function autorun() {
loadMIDI();
}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More