diff --git a/.ghjk/deno.lock b/.ghjk/deno.lock index 90ef5d97b5..56d7707b93 100644 --- a/.ghjk/deno.lock +++ b/.ghjk/deno.lock @@ -34,6 +34,202 @@ "https://deno.land/std@0.196.0/console/_rle.ts": "56668d5c44f964f1b4ff93f21c9896df42d6ee4394e814db52d6d13f5bb247c7", "https://deno.land/std@0.196.0/console/unicode_width.ts": "10661c0f2eeab802d16b8b85ed8825bbc573991bbfb6affed32dc1ff994f54f9", "https://deno.land/std@0.196.0/fmt/colors.ts": "a7eecffdf3d1d54db890723b303847b6e0a1ab4b528ba6958b8f2e754cf1b3bc", + "https://deno.land/std@0.201.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.201.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.201.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://deno.land/std@0.201.0/fmt/colors.ts": "87544aa2bc91087bb37f9c077970c85bfb041b48e4c37356129d7b450a415b6f", + "https://deno.land/std@0.201.0/fs/_util.ts": "fbf57dcdc9f7bc8128d60301eece608246971a7836a3bb1e78da75314f08b978", + "https://deno.land/std@0.201.0/fs/copy.ts": "23cc1c465babe5ca4d69778821e2f8addc44593e30a5ca0b902b3784eed75bb6", + "https://deno.land/std@0.201.0/fs/empty_dir.ts": "2e52cd4674d18e2e007175c80449fc3d263786a1361e858d9dfa9360a6581b47", + "https://deno.land/std@0.201.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", + "https://deno.land/std@0.201.0/fs/ensure_file.ts": "39ac83cc283a20ec2735e956adf5de3e8a3334e0b6820547b5772f71c49ae083", + "https://deno.land/std@0.201.0/fs/ensure_link.ts": "c15e69c48556d78aae31b83e0c0ece04b7b8bc0951412f5b759aceb6fde7f0ac", + "https://deno.land/std@0.201.0/fs/ensure_symlink.ts": "b389c8568f0656d145ac7ece472afe710815cccbb2ebfd19da7978379ae143fe", + "https://deno.land/std@0.201.0/fs/eol.ts": "f1f2eb348a750c34500741987b21d65607f352cf7205f48f4319d417fff42842", + "https://deno.land/std@0.201.0/fs/exists.ts": "cb59a853d84871d87acab0e7936a4dac11282957f8e195102c5a7acb42546bb8", + "https://deno.land/std@0.201.0/fs/expand_glob.ts": "52b8b6f5b1fa585c348250da1c80ce5d820746cb4a75d874b3599646f677d3a7", + "https://deno.land/std@0.201.0/fs/mod.ts": "bc3d0acd488cc7b42627044caf47d72019846d459279544e1934418955ba4898", + "https://deno.land/std@0.201.0/fs/move.ts": "b4f8f46730b40c32ea3c0bc8eb0fd0e8139249a698883c7b3756424cf19785c9", + "https://deno.land/std@0.201.0/fs/walk.ts": "a16146724a6aaf9efdb92023a74e9805195c3469900744ce5de4113b07b29779", + "https://deno.land/std@0.201.0/io/buf_reader.ts": "0bd8ad26255945b5f418940db23db03bee0c160dbb5ae4627e2c0be3b361df6a", + "https://deno.land/std@0.201.0/io/buffer.ts": "4d6883daeb2e698579c4064170515683d69f40f3de019bfe46c5cf31e74ae793", + "https://deno.land/std@0.201.0/path/_basename.ts": "057d420c9049821f983f784fd87fa73ac471901fb628920b67972b0f44319343", + "https://deno.land/std@0.201.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.201.0/path/_dirname.ts": "355e297236b2218600aee7a5301b937204c62e12da9db4b0b044993d9e658395", + "https://deno.land/std@0.201.0/path/_extname.ts": "eaaa5aae1acf1f03254d681bd6a8ce42a9cb5b7ff2213a9d4740e8ab31283664", + "https://deno.land/std@0.201.0/path/_format.ts": "4a99270d6810f082e614309164fad75d6f1a483b68eed97c830a506cc589f8b4", + "https://deno.land/std@0.201.0/path/_from_file_url.ts": "6eadfae2e6f63ad9ee46b26db4a1b16583055c0392acedfb50ed2fc694b6f581", + "https://deno.land/std@0.201.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.201.0/path/_is_absolute.ts": "05dac10b5e93c63198b92e3687baa2be178df5321c527dc555266c0f4f51558c", + "https://deno.land/std@0.201.0/path/_join.ts": "815f5e85b042285175b1492dd5781240ce126c23bd97bad6b8211fe7129c538e", + "https://deno.land/std@0.201.0/path/_normalize.ts": "a19ec8706b2707f9dd974662a5cd89fad438e62ab1857e08b314a8eb49a34d81", + "https://deno.land/std@0.201.0/path/_os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3", + "https://deno.land/std@0.201.0/path/_parse.ts": "0f9b0ff43682dd9964eb1c4398610c4e165d8db9d3ac9d594220217adf480cfa", + "https://deno.land/std@0.201.0/path/_relative.ts": "27bdeffb5311a47d85be26d37ad1969979359f7636c5cd9fcf05dcd0d5099dc5", + "https://deno.land/std@0.201.0/path/_resolve.ts": "7a3616f1093735ed327e758313b79c3c04ea921808ca5f19ddf240cb68d0adf6", + "https://deno.land/std@0.201.0/path/_to_file_url.ts": "a141e4a525303e1a3a0c0571fd024552b5f3553a2af7d75d1ff3a503dcbb66d8", + "https://deno.land/std@0.201.0/path/_to_namespaced_path.ts": "0d5f4caa2ed98ef7a8786286df6af804b50e38859ae897b5b5b4c8c5930a75c8", + "https://deno.land/std@0.201.0/path/_util.ts": "4e191b1bac6b3bf0c31aab42e5ca2e01a86ab5a0d2e08b75acf8585047a86221", + "https://deno.land/std@0.201.0/path/basename.ts": "bdfa5a624c6a45564dc6758ef2077f2822978a6dbe77b0a3514f7d1f81362930", + "https://deno.land/std@0.201.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000", + "https://deno.land/std@0.201.0/path/dirname.ts": "b6533f4ee4174a526dec50c279534df5345836dfdc15318400b08c62a62a39dd", + "https://deno.land/std@0.201.0/path/extname.ts": "62c4b376300795342fe1e4746c0de518b4dc9c4b0b4617bfee62a2973a9555cf", + "https://deno.land/std@0.201.0/path/format.ts": "110270b238514dd68455a4c54956215a1aff7e37e22e4427b7771cefe1920aa5", + "https://deno.land/std@0.201.0/path/from_file_url.ts": "9f5cb58d58be14c775ec2e57fc70029ac8b17ed3bd7fe93e475b07280adde0ac", + "https://deno.land/std@0.201.0/path/glob.ts": "593e2c3573883225c25c5a21aaa8e9382a696b8e175ea20a3b6a1471ad17aaed", + "https://deno.land/std@0.201.0/path/is_absolute.ts": "0b92eb35a0a8780e9f16f16bb23655b67dace6a8e0d92d42039e518ee38103c1", + "https://deno.land/std@0.201.0/path/join.ts": "31c5419f23d91655b08ec7aec403f4e4cd1a63d39e28f6e42642ea207c2734f8", + "https://deno.land/std@0.201.0/path/mod.ts": "6e1efb0b13121463aedb53ea51dabf5639a3172ab58c89900bbb72b486872532", + "https://deno.land/std@0.201.0/path/normalize.ts": "6ea523e0040979dd7ae2f1be5bf2083941881a252554c0f32566a18b03021955", + "https://deno.land/std@0.201.0/path/parse.ts": "be8de342bb9e1924d78dc4d93c45215c152db7bf738ec32475560424b119b394", + "https://deno.land/std@0.201.0/path/posix.ts": "0a1c1952d132323a88736d03e92bd236f3ed5f9f079e5823fae07c8d978ee61b", + "https://deno.land/std@0.201.0/path/relative.ts": "8bedac226afd360afc45d451a6c29fabceaf32978526bcb38e0c852661f66c61", + "https://deno.land/std@0.201.0/path/resolve.ts": "133161e4949fc97f9ca67988d51376b0f5eef8968a6372325ab84d39d30b80dc", + "https://deno.land/std@0.201.0/path/separator.ts": "40a3e9a4ad10bef23bc2cd6c610291b6c502a06237c2c4cd034a15ca78dedc1f", + "https://deno.land/std@0.201.0/path/to_file_url.ts": "00e6322373dd51ad109956b775e4e72e5f9fa68ce2c6b04e4af2a6eed3825d31", + "https://deno.land/std@0.201.0/path/to_namespaced_path.ts": "1b1db3055c343ab389901adfbda34e82b7386bcd1c744d54f9c1496ee0fd0c3d", + "https://deno.land/std@0.201.0/path/win32.ts": "8b3f80ef7a462511d5e8020ff490edcaa0a0d118f1b1e9da50e2916bdd73f9dd", + "https://deno.land/std@0.201.0/streams/read_all.ts": "ee319772fb0fd28302f97343cc48dfcf948f154fd0d755d8efe65814b70533be", + "https://deno.land/std@0.201.0/streams/reader_from_stream_reader.ts": "fa4971e5615a010e49492c5d1688ca1a4d17472a41e98b498ab89a64ebd7ac73", + "https://deno.land/std@0.201.0/streams/write_all.ts": "aec90152978581ea62d56bb53a5cbf487e6a89c902f87c5969681ffbdf32b998", + "https://deno.land/std@0.205.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.205.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.205.0/bytes/copy.ts": "939d89e302a9761dcf1d9c937c7711174ed74c59eef40a1e4569a05c9de88219", + "https://deno.land/std@0.205.0/fmt/colors.ts": "c51c4642678eb690dcf5ffee5918b675bf01a33fba82acf303701ae1a4f8c8d9", + "https://deno.land/std@0.205.0/fs/_util.ts": "fbf57dcdc9f7bc8128d60301eece608246971a7836a3bb1e78da75314f08b978", + "https://deno.land/std@0.205.0/fs/copy.ts": "ca19e4837965914471df38fbd61e16f9e8adfe89f9cffb0c83615c83ea3fc2bf", + "https://deno.land/std@0.205.0/fs/empty_dir.ts": "0b4a2508232446eed232ad1243dd4b0f07ac503a281633ae1324d1528df70964", + "https://deno.land/std@0.205.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40", + "https://deno.land/std@0.205.0/fs/ensure_file.ts": "39ac83cc283a20ec2735e956adf5de3e8a3334e0b6820547b5772f71c49ae083", + "https://deno.land/std@0.205.0/fs/ensure_link.ts": "c15e69c48556d78aae31b83e0c0ece04b7b8bc0951412f5b759aceb6fde7f0ac", + "https://deno.land/std@0.205.0/fs/ensure_symlink.ts": "b389c8568f0656d145ac7ece472afe710815cccbb2ebfd19da7978379ae143fe", + "https://deno.land/std@0.205.0/fs/eol.ts": "f1f2eb348a750c34500741987b21d65607f352cf7205f48f4319d417fff42842", + "https://deno.land/std@0.205.0/fs/exists.ts": "cb59a853d84871d87acab0e7936a4dac11282957f8e195102c5a7acb42546bb8", + "https://deno.land/std@0.205.0/fs/expand_glob.ts": "4f98c508fc9e40d6311d2f7fd88aaad05235cc506388c22dda315e095305811d", + "https://deno.land/std@0.205.0/fs/mod.ts": "bc3d0acd488cc7b42627044caf47d72019846d459279544e1934418955ba4898", + "https://deno.land/std@0.205.0/fs/move.ts": "b4f8f46730b40c32ea3c0bc8eb0fd0e8139249a698883c7b3756424cf19785c9", + "https://deno.land/std@0.205.0/fs/walk.ts": "c1e6b43f72a46e89b630140308bd51a4795d416a416b4cfb7cd4bd1e25946723", + "https://deno.land/std@0.205.0/io/buf_writer.ts": "c49d1a3114ad936690847abd0dd2e321e96188546d6e8ae9d22b292b8b59f9f8", + "https://deno.land/std@0.205.0/log/handlers.ts": "3a0883f65567f59a9a88e44c972b24b924621bc28ead91af11d7a6da93c4a64c", + "https://deno.land/std@0.205.0/log/levels.ts": "6309147664e9e008cd6671610f2505c4c95f181f6bae4816a84b33e0aec66859", + "https://deno.land/std@0.205.0/log/logger.ts": "180c50a07c43a556dc5794e913c82946399e89d683201d01c8f0091e1e4ae3fc", + "https://deno.land/std@0.205.0/log/mod.ts": "a274d2129c8d08d4c96e0fb165a595e6c730b5130b437a9ce04364156bfe955a", + "https://deno.land/std@0.205.0/path/_common/assert_path.ts": "061e4d093d4ba5aebceb2c4da3318bfe3289e868570e9d3a8e327d91c2958946", + "https://deno.land/std@0.205.0/path/_common/basename.ts": "0d978ff818f339cd3b1d09dc914881f4d15617432ae519c1b8fdc09ff8d3789a", + "https://deno.land/std@0.205.0/path/_common/common.ts": "9e4233b2eeb50f8b2ae10ecc2108f58583aea6fd3e8907827020282dc2b76143", + "https://deno.land/std@0.205.0/path/_common/constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0", + "https://deno.land/std@0.205.0/path/_common/dirname.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397", + "https://deno.land/std@0.205.0/path/_common/format.ts": "11aa62e316dfbf22c126917f5e03ea5fe2ee707386555a8f513d27ad5756cf96", + "https://deno.land/std@0.205.0/path/_common/from_file_url.ts": "ef1bf3197d2efbf0297a2bdbf3a61d804b18f2bcce45548ae112313ec5be3c22", + "https://deno.land/std@0.205.0/path/_common/glob_to_reg_exp.ts": "5c3c2b79fc2294ec803d102bd9855c451c150021f452046312819fbb6d4dc156", + "https://deno.land/std@0.205.0/path/_common/is_glob.ts": "567dce5c6656bdedfc6b3ee6c0833e1e4db2b8dff6e62148e94a917f289c06ad", + "https://deno.land/std@0.205.0/path/_common/normalize.ts": "2ba7fb4cc9fafb0f38028f434179579ce61d4d9e51296fad22b701c3d3cd7397", + "https://deno.land/std@0.205.0/path/_common/normalize_string.ts": "88c472f28ae49525f9fe82de8c8816d93442d46a30d6bb5063b07ff8a89ff589", + "https://deno.land/std@0.205.0/path/_common/relative.ts": "1af19d787a2a84b8c534cc487424fe101f614982ae4851382c978ab2216186b4", + "https://deno.land/std@0.205.0/path/_common/strip_trailing_separators.ts": "7ffc7c287e97bdeeee31b155828686967f222cd73f9e5780bfe7dfb1b58c6c65", + "https://deno.land/std@0.205.0/path/_common/to_file_url.ts": "a8cdd1633bc9175b7eebd3613266d7c0b6ae0fb0cff24120b6092ac31662f9ae", + "https://deno.land/std@0.205.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b", + "https://deno.land/std@0.205.0/path/_os.ts": "30b0c2875f360c9296dbe6b7f2d528f0f9c741cecad2e97f803f5219e91b40a2", + "https://deno.land/std@0.205.0/path/basename.ts": "04bb5ef3e86bba8a35603b8f3b69537112cdd19ce64b77f2522006da2977a5f3", + "https://deno.land/std@0.205.0/path/common.ts": "f4d061c7d0b95a65c2a1a52439edec393e906b40f1caf4604c389fae7caa80f5", + "https://deno.land/std@0.205.0/path/dirname.ts": "88a0a71c21debafc4da7a4cd44fd32e899462df458fbca152390887d41c40361", + "https://deno.land/std@0.205.0/path/extname.ts": "2da4e2490f3b48b7121d19fb4c91681a5e11bd6bd99df4f6f47d7a71bb6ecdf2", + "https://deno.land/std@0.205.0/path/format.ts": "3457530cc85d1b4bab175f9ae73998b34fd456c830d01883169af0681b8894fb", + "https://deno.land/std@0.205.0/path/from_file_url.ts": "e7fa233ea1dff9641e8d566153a24d95010110185a6f418dd2e32320926043f8", + "https://deno.land/std@0.205.0/path/glob.ts": "9c77cf47db1d786e2ebf66670824d03fd84ecc7c807cac24441eb9d5cb6a2986", + "https://deno.land/std@0.205.0/path/is_absolute.ts": "67232b41b860571c5b7537f4954c88d86ae2ba45e883ee37d3dec27b74909d13", + "https://deno.land/std@0.205.0/path/join.ts": "98d3d76c819af4a11a81d5ba2dbb319f1ce9d63fc2b615597d4bcfddd4a89a09", + "https://deno.land/std@0.205.0/path/mod.ts": "2d62a0a8b78a60e8e6f485d881bac6b61d58573b11cf585fb7c8fc50d9b20d80", + "https://deno.land/std@0.205.0/path/normalize.ts": "aa95be9a92c7bd4f9dc0ba51e942a1973e2b93d266cd74f5ca751c136d520b66", + "https://deno.land/std@0.205.0/path/parse.ts": "d87ff0deef3fb495bc0d862278ff96da5a06acf0625ca27769fc52ac0d3d6ece", + "https://deno.land/std@0.205.0/path/posix/_util.ts": "ecf49560fedd7dd376c6156cc5565cad97c1abe9824f4417adebc7acc36c93e5", + "https://deno.land/std@0.205.0/path/posix/basename.ts": "a630aeb8fd8e27356b1823b9dedd505e30085015407caa3396332752f6b8406a", + "https://deno.land/std@0.205.0/path/posix/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b", + "https://deno.land/std@0.205.0/path/posix/dirname.ts": "f48c9c42cc670803b505478b7ef162c7cfa9d8e751b59d278b2ec59470531472", + "https://deno.land/std@0.205.0/path/posix/extname.ts": "ee7f6571a9c0a37f9218fbf510c440d1685a7c13082c348d701396cc795e0be0", + "https://deno.land/std@0.205.0/path/posix/format.ts": "b94876f77e61bfe1f147d5ccb46a920636cd3cef8be43df330f0052b03875968", + "https://deno.land/std@0.205.0/path/posix/from_file_url.ts": "b97287a83e6407ac27bdf3ab621db3fccbf1c27df0a1b1f20e1e1b5acf38a379", + "https://deno.land/std@0.205.0/path/posix/glob.ts": "86c3f06d1c98303613c74650961c3e24bdb871cde2a97c3ae7f0f6d4abbef445", + "https://deno.land/std@0.205.0/path/posix/is_absolute.ts": "159900a3422d11069d48395568217eb7fc105ceda2683d03d9b7c0f0769e01b8", + "https://deno.land/std@0.205.0/path/posix/join.ts": "0c0d84bdc344876930126640011ec1b888e6facf74153ffad9ef26813aa2a076", + "https://deno.land/std@0.205.0/path/posix/mod.ts": "6bfa8a42d85345b12dbe8571028ca2c62d460b6ef968125e498602b43b6cf6b6", + "https://deno.land/std@0.205.0/path/posix/normalize.ts": "11de90a94ab7148cc46e5a288f7d732aade1d616bc8c862f5560fa18ff987b4b", + "https://deno.land/std@0.205.0/path/posix/parse.ts": "199208f373dd93a792e9c585352bfc73a6293411bed6da6d3bc4f4ef90b04c8e", + "https://deno.land/std@0.205.0/path/posix/relative.ts": "e2f230608b0f083e6deaa06e063943e5accb3320c28aef8d87528fbb7fe6504c", + "https://deno.land/std@0.205.0/path/posix/resolve.ts": "51579d83159d5c719518c9ae50812a63959bbcb7561d79acbdb2c3682236e285", + "https://deno.land/std@0.205.0/path/posix/separator.ts": "0b6573b5f3269a3164d8edc9cefc33a02dd51003731c561008c8bb60220ebac1", + "https://deno.land/std@0.205.0/path/posix/to_file_url.ts": "08d43ea839ee75e9b8b1538376cfe95911070a655cd312bc9a00f88ef14967b6", + "https://deno.land/std@0.205.0/path/posix/to_namespaced_path.ts": "c9228a0e74fd37e76622cd7b142b8416663a9b87db643302fa0926b5a5c83bdc", + "https://deno.land/std@0.205.0/path/relative.ts": "23d45ede8b7ac464a8299663a43488aad6b561414e7cbbe4790775590db6349c", + "https://deno.land/std@0.205.0/path/resolve.ts": "5b184efc87155a0af9fa305ff68a109e28de9aee81fc3e77cd01380f19daf867", + "https://deno.land/std@0.205.0/path/separator.ts": "40a3e9a4ad10bef23bc2cd6c610291b6c502a06237c2c4cd034a15ca78dedc1f", + "https://deno.land/std@0.205.0/path/to_file_url.ts": "edaafa089e0bce386e1b2d47afe7c72e379ff93b28a5829a5885e4b6c626d864", + "https://deno.land/std@0.205.0/path/to_namespaced_path.ts": "cf8734848aac3c7527d1689d2adf82132b1618eff3cc523a775068847416b22a", + "https://deno.land/std@0.205.0/path/windows/_util.ts": "f32b9444554c8863b9b4814025c700492a2b57ff2369d015360970a1b1099d54", + "https://deno.land/std@0.205.0/path/windows/basename.ts": "8a9dbf7353d50afbc5b221af36c02a72c2d1b2b5b9f7c65bf6a5a2a0baf88ad3", + "https://deno.land/std@0.205.0/path/windows/common.ts": "e781d395dc76f6282e3f7dd8de13194abb8b04a82d109593141abc6e95755c8b", + "https://deno.land/std@0.205.0/path/windows/dirname.ts": "5c2aa541384bf0bd9aca821275d2a8690e8238fa846198ef5c7515ce31a01a94", + "https://deno.land/std@0.205.0/path/windows/extname.ts": "07f4fa1b40d06a827446b3e3bcc8d619c5546b079b8ed0c77040bbef716c7614", + "https://deno.land/std@0.205.0/path/windows/format.ts": "343019130d78f172a5c49fdc7e64686a7faf41553268961e7b6c92a6d6548edf", + "https://deno.land/std@0.205.0/path/windows/from_file_url.ts": "d53335c12b0725893d768be3ac6bf0112cc5b639d2deb0171b35988493b46199", + "https://deno.land/std@0.205.0/path/windows/glob.ts": "0286fb89ecd21db5cbf3b6c79e2b87c889b03f1311e66fb769e6b905d4142332", + "https://deno.land/std@0.205.0/path/windows/is_absolute.ts": "245b56b5f355ede8664bd7f080c910a97e2169972d23075554ae14d73722c53c", + "https://deno.land/std@0.205.0/path/windows/join.ts": "e6600bf88edeeef4e2276e155b8de1d5dec0435fd526ba2dc4d37986b2882f16", + "https://deno.land/std@0.205.0/path/windows/mod.ts": "c3d1a36fbf9f6db1320bcb4fbda8de011d25461be3497105e15cbea1e3726198", + "https://deno.land/std@0.205.0/path/windows/normalize.ts": "9deebbf40c81ef540b7b945d4ccd7a6a2c5a5992f791e6d3377043031e164e69", + "https://deno.land/std@0.205.0/path/windows/parse.ts": "120faf778fe1f22056f33ded069b68e12447668fcfa19540c0129561428d3ae5", + "https://deno.land/std@0.205.0/path/windows/relative.ts": "026855cd2c36c8f28f1df3c6fbd8f2449a2aa21f48797a74700c5d872b86d649", + "https://deno.land/std@0.205.0/path/windows/resolve.ts": "5ff441ab18a2346abadf778121128ee71bda4d0898513d4639a6ca04edca366b", + "https://deno.land/std@0.205.0/path/windows/separator.ts": "ae21f27015f10510ed1ac4a0ba9c4c9c967cbdd9d9e776a3e4967553c397bd5d", + "https://deno.land/std@0.205.0/path/windows/to_file_url.ts": "8e9ea9e1ff364aa06fa72999204229952d0a279dbb876b7b838b2b2fea55cce3", + "https://deno.land/std@0.205.0/path/windows/to_namespaced_path.ts": "e0f4d4a5e77f28a5708c1a33ff24360f35637ba6d8f103d19661255ef7bfd50d", + "https://deno.land/std@0.205.0/semver/_shared.ts": "8547ccf91b36c30fb2a8a17d7081df13f4ae694c4aa44c39799eba69ad0dcb23", + "https://deno.land/std@0.205.0/semver/cmp.ts": "12c30b5888afd9e414defef64f881a478ff9ab11bd329ed6c5844b74eea5c971", + "https://deno.land/std@0.205.0/semver/comparator_format.ts": "329e05d914c064590ded4801fc601bf1c5d0f461c5524b1578e10f180551ef6f", + "https://deno.land/std@0.205.0/semver/comparator_intersects.ts": "61920121a6c1600306dbcf8944c4cc55e45c3a1bdbefe41b79a0884bf02d9e1b", + "https://deno.land/std@0.205.0/semver/comparator_max.ts": "f4cc5f528abd8aab68c66bbead732e3c59102b13a318cd8e4f8a47aa3debec76", + "https://deno.land/std@0.205.0/semver/comparator_min.ts": "eea382428ebf0c50168f780898df8519c88da5a10d1f8babbfebdc89fb75942e", + "https://deno.land/std@0.205.0/semver/compare.ts": "782e03b5107648bebaaebf0e33a9a7d6a0481eb88d2f7be8e857e4abbfdf42c0", + "https://deno.land/std@0.205.0/semver/compare_build.ts": "5d6ebc0106f1ed46e391d6c234e071934ba30938fa818c9cc3da67c7c7494c02", + "https://deno.land/std@0.205.0/semver/constants.ts": "bb0c7652c433c7ec1dad5bf18c7e7e1557efe9ddfd5e70aa6305153e76dc318c", + "https://deno.land/std@0.205.0/semver/difference.ts": "966ef286f0bfde53ebfb74a727c607b05a7fdce623a678794d088166b9b9afdf", + "https://deno.land/std@0.205.0/semver/eq.ts": "6ddb84ce8c95f18e9b7a46d8a63b1e6ca5f0c0f651f1f46f20db6543b390c3f3", + "https://deno.land/std@0.205.0/semver/format.ts": "236cc8b5d2e8031258dcff3ca89e14ba926434d5b789730e2c781db172e76bd9", + "https://deno.land/std@0.205.0/semver/gt.ts": "8529cf2ae1bca95c22801cf38f93620dc802c5dcbc02f863437571b970de3705", + "https://deno.land/std@0.205.0/semver/gte.ts": "b54f7855ac37ff076d6df9a294e944356754171f94f5cb974af782480a9f1fd0", + "https://deno.land/std@0.205.0/semver/gtr.ts": "d2ec1f02ce6a566b7df76a188af7315d802c6069892d460d631a3b0d9e2b1a45", + "https://deno.land/std@0.205.0/semver/increment.ts": "a6e5ac018887244731a4b936743ae14476cc432ac874f1c9848711b4000c5991", + "https://deno.land/std@0.205.0/semver/is_semver.ts": "666f4e1d8e41994150d4326d515046bc5fc72e59cbbd6e756a0b60548dcd00b5", + "https://deno.land/std@0.205.0/semver/is_semver_comparator.ts": "035aa894415ad1c8f50a6b6f52ea49c62d6f3af62b5d6fca9c1f4cb84f1896fd", + "https://deno.land/std@0.205.0/semver/is_semver_range.ts": "6f9b4f1c937a202750cae9444900d8abe4a68cc3bf5bb90f0d49c08cf85308cb", + "https://deno.land/std@0.205.0/semver/lt.ts": "081614b5adbc5bc944649e09af946a90a4b4bdb3d65a67c005183994504f04c2", + "https://deno.land/std@0.205.0/semver/lte.ts": "f8605c17d620bfb3aa57775643e3c560c04f7c20f2e431f64ca5b2ea39e36217", + "https://deno.land/std@0.205.0/semver/ltr.ts": "975e672b5ca8aa67336660653f8c76e1db829c628fb08ea3e815a9a12fa7eb9c", + "https://deno.land/std@0.205.0/semver/max_satisfying.ts": "75406901818cd1127a6332e007e96285474e833d0e40dbbfddc01b08ee6e51f2", + "https://deno.land/std@0.205.0/semver/min_satisfying.ts": "58bd48033a00e63bea0709f78c33c66ea58bce2dbebda0d54d3fdc6db7d0d298", + "https://deno.land/std@0.205.0/semver/mod.ts": "442702e8a57cbf02e68868c46ffe66ecf6efbde58d72cfdfbdaa51ad0c4af513", + "https://deno.land/std@0.205.0/semver/neq.ts": "e91b699681c3b406fc3d661d4eac7aa36cd1cc8bf188f8e3c7b53cc340775b87", + "https://deno.land/std@0.205.0/semver/outside.ts": "1d225fdb42172d946c382e144ce97c402812741741bbe299561aa164cc956ec4", + "https://deno.land/std@0.205.0/semver/parse.ts": "5d24ec0c5f681db1742c31332f6007395c84696c88ff4b58287485ed3f6d8c84", + "https://deno.land/std@0.205.0/semver/parse_comparator.ts": "f07f9be8322b1f61a36b94c3c65a0dc4124958ee54cf744c92ca4028bf156d5e", + "https://deno.land/std@0.205.0/semver/parse_range.ts": "39a18608a8026004b218ef383e7ae624a9e663b82327948c1810f16d875113c2", + "https://deno.land/std@0.205.0/semver/range_format.ts": "3de31fd0b74dd565e052840e73a8e9ee1d9d289ca60b85749167710b978cc078", + "https://deno.land/std@0.205.0/semver/range_intersects.ts": "8672e603df1bb68a02452b634021c4913395f4d16d75c21b578d6f4175a2b2c1", + "https://deno.land/std@0.205.0/semver/range_max.ts": "9c10c65bbc7796347ce6f765a77865cead88870d17481ac78259400a2378af2e", + "https://deno.land/std@0.205.0/semver/range_min.ts": "b7849e70e0b0677b382eddaa822b6690521449a659c5b8ec84cbd438f6e6ca59", + "https://deno.land/std@0.205.0/semver/rcompare.ts": "b8b9f5108d40c64cf50ffe455199aba7ad64995829a17110301ae3f8290374ee", + "https://deno.land/std@0.205.0/semver/rsort.ts": "a9139a1fc37570f9d8b6517032d152cf69143cec89d4342f19174e48f06d8543", + "https://deno.land/std@0.205.0/semver/sort.ts": "c058a5b2c8e866fa8e6ef25c9d228133357caf4c140f129bfc368334fcd0813b", + "https://deno.land/std@0.205.0/semver/test_comparator.ts": "eff5394cb82d133ed18f96fe547de7e7264bf0d25d16cbc6126664aa06ef8f37", + "https://deno.land/std@0.205.0/semver/test_range.ts": "b236c276268e92bbbc65e7c4b4b6b685ea6b4534a71b2525b53093d094f631c6", + "https://deno.land/std@0.205.0/semver/types.ts": "d44f442c2f27dd89bd6695b369e310b80549746f03c38f241fe28a83b33dd429", + "https://deno.land/std@0.205.0/url/_strip.ts": "86f852d266b86e5867f50ac5d453bedea7b7e7a1919669df93d66a0b59b00e5b", + "https://deno.land/std@0.205.0/url/basename.ts": "1257643f9934b65696d8af3ad993b3269d55231e6258ac13fba3d4fe193f30be", + "https://deno.land/std@0.205.0/url/dirname.ts": "65a0c5d4a62a6505404ea992fb73a2201c66e208aa7dfeb76d34f275432eddd0", + "https://deno.land/std@0.205.0/url/extname.ts": "d16f2a3bdccd1ef389a0a066a8275fa59089a04ae98cb69d753e228845d6256f", + "https://deno.land/std@0.205.0/url/join.ts": "fbc3488c641c38832f0c900fcf99cb970164d8e32b84f1427581bb83cf35efeb", + "https://deno.land/std@0.205.0/url/mod.ts": "d4e4db2f85a4a1613d824367b750f36bbd1c0ff791daae2eb74795d292c722bb", + "https://deno.land/std@0.205.0/url/normalize.ts": "5c5803452521a36faec1a91bdb665e1cbdf7ce22bc0482388ad79f229b74cd45", "https://deno.land/std@0.213.0/archive/_common.ts": "85edd5cdd4324833f613c1bc055f8e2f935cc9229c6b3044421268d9959997ef", "https://deno.land/std@0.213.0/archive/untar.ts": "7677c136f2188cd8c33363ccaaee6e77d4ca656cca3e2093d08de8f294d4353d", "https://deno.land/std@0.213.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", diff --git a/.ghjk/lock.json b/.ghjk/lock.json index 9804ab46d9..68f6b154f3 100644 --- a/.ghjk/lock.json +++ b/.ghjk/lock.json @@ -563,6 +563,15 @@ "env": {}, "allowedPortDeps": [] } + }, + "test": { + "name": "test", + "dependsOn": [], + "env": { + "installs": [], + "env": {}, + "allowedPortDeps": [] + } } } } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 845f46a18c..e93f836096 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -13,6 +13,7 @@ "chrischinchilla.vale-vscode", "hediet.vscode-drawio", "davidlday.languagetool-linter", - "dtsvet.vscode-wasm" + "dtsvet.vscode-wasm", + "bierner.comment-tagged-templates" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 97ed0a4948..0f0d781169 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,6 @@ "website/.docusaurus": true, "website/node_modules": true }, - "deno.importMap": "typegate/import_map.json", "deno.enablePaths": [ "typegate", "dev", diff --git a/Cargo.lock b/Cargo.lock index e2d5826a76..43947f3a82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -894,9 +894,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.71" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +checksum = "95d8e92cac0961e91dbd517496b00f7e9b92363dbe6d42c3198268323798860c" dependencies = [ "addr2line", "cc", @@ -7550,9 +7550,9 @@ checksum = "a86ed3f5f244b372d6b1a00b72ef7f8876d0bc6a78a4c9985c53614041512063" [[package]] name = "os_info" -version = "3.8.2" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae99c7fa6dd38c7cafe1ec085e804f8f555a2f8659b0dbe03f1f9963a9b51092" +checksum = "6cbb46d5d01695d7a1fb8be5f0d1968bd2b2b8ba1d1b3e7062ce2a0593e57af1" dependencies = [ "log", "serde 1.0.200", @@ -9963,9 +9963,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.34+deprecated" +version = "0.9.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +checksum = "a0623d197252096520c6f2a5e1171ee436e5af99a5d7caa2891e55e61950e6d9" dependencies = [ "indexmap 2.2.6", "itoa", diff --git a/docs/workflows/artifact_upload_protocol.drawio.svg b/docs/workflows/artifact_upload_protocol.drawio.svg index cd0780c9e7..f8f1e63ade 100644 --- a/docs/workflows/artifact_upload_protocol.drawio.svg +++ b/docs/workflows/artifact_upload_protocol.drawio.svg @@ -1,4 +1,4 @@ - + @@ -20,11 +20,11 @@ - + -
+
@@ -34,16 +34,16 @@
- + Typegraph SDK - + -
+
@@ -53,16 +53,16 @@
- + Typegraph Core - + -
+
@@ -72,18 +72,18 @@
- + TgContext - - - + + + -
+
@@ -93,7 +93,7 @@
- + finalize_typegraph @@ -101,7 +101,7 @@ -
+
access context @@ -109,16 +109,16 @@
- + access con... - + -
+
@@ -128,17 +128,17 @@
- + Runtime - - + + -
+
@@ -148,18 +148,18 @@
- + save runtime artifacts meta... - - - + + + -
+
@@ -169,7 +169,7 @@
- + TgDeploy @@ -177,7 +177,7 @@ -
+
@@ -187,16 +187,16 @@
- + during tg finalization, retriev... - + -
+
@@ -206,16 +206,16 @@
- + Typegate - + -
+
@@ -225,16 +225,16 @@
- + Local Artifact Store - + -
+
@@ -244,16 +244,16 @@
- + Shared Artifact Store - + -
+
@@ -263,17 +263,17 @@
- + Generic Artifact Store - - + + -
+
@@ -283,17 +283,17 @@
- + Single replica mode - - + + -
+
@@ -303,7 +303,7 @@
- + Upload artifacts @@ -311,7 +311,7 @@ -
+
sync mode @@ -319,17 +319,17 @@
- + sync mode - - + + -
+
@@ -339,16 +339,16 @@
- + Fetch artifact upload URLs - + -
+
@@ -358,17 +358,17 @@
- + Redis - - + + -
+
@@ -378,16 +378,16 @@
- + Cache Upload URLs in Sync mode - + -
+
@@ -397,16 +397,16 @@
- + S3 Bucket - + -
+
@@ -416,17 +416,17 @@
- + Local typegate storage - - + + -
+
@@ -436,17 +436,17 @@
- + Save artifacts - - + + -
+
@@ -456,7 +456,7 @@
- + Save artifacts diff --git a/ghjk.ts b/ghjk.ts index 4f25f8c783..0afbebba24 100644 --- a/ghjk.ts +++ b/ghjk.ts @@ -43,7 +43,6 @@ const allowedPortDeps = [ defaultInst: thinInstallConfig(fat), })), ]; -export const secureConfig = ghjk.secureConfig({ allowedPortDeps }); const inCi = () => !!Deno.env.get("CI"); const inOci = () => !!Deno.env.get("OCI"); @@ -63,7 +62,7 @@ ghjk.install( crateName: "wasm-tools", version: WASM_TOOLS_VERSION, locked: true, - }), + }) ); if (!inOci()) { @@ -81,7 +80,7 @@ if (!inOci()) { packageName: "@bytecodealliance/jco", version: JCO_VERSION, })[0], - ports.npmi({ packageName: "node-gyp", version: "10.0.1" })[0], + ports.npmi({ packageName: "node-gyp", version: "10.0.1" })[0] ); } @@ -90,7 +89,7 @@ if (Deno.build.os == "linux" && !Deno.env.has("NO_MOLD")) { ports.mold({ version: MOLD_VERSION, replaceLd: Deno.env.has("CI") || Deno.env.has("OCI"), - }), + }) ); } @@ -100,12 +99,10 @@ if (!Deno.env.has("NO_PYTHON")) { ports.pipi({ packageName: "poetry", version: POETRY_VERSION, - })[0], + })[0] ); if (!inOci()) { - ghjk.install( - ports.pipi({ packageName: "pre-commit" })[0], - ); + ghjk.install(ports.pipi({ packageName: "pre-commit" })[0]); } } @@ -113,7 +110,7 @@ if (inDev()) { ghjk.install( ports.act({}), ports.cargobi({ crateName: "whiz", locked: true }), - installs.comp_py[0], + installs.comp_py[0] ); } @@ -122,13 +119,12 @@ ghjk.task("clean-deno-lock", { // jq ], async fn({ $ }) { - const jqOp1 = - `del(.packages.specifiers["npm:@typegraph/sdk@${METATYPE_VERSION}"])`; + const jqOp1 = `del(.packages.specifiers["npm:@typegraph/sdk@${METATYPE_VERSION}"])`; const jqOp2 = `del(.packages.npm["@typegraph/sdk@${METATYPE_VERSION}"])`; const jqOp = `${jqOp1} | ${jqOp2}`; await Deno.writeTextFile( "typegate/deno.lock", - await $`jq ${jqOp} typegate/deno.lock`.text(), + await $`jq ${jqOp} typegate/deno.lock`.text() ); }, }); @@ -138,8 +134,9 @@ ghjk.task("gen-pyrt-bind", { allowedPortDeps, async fn({ $ }) { await $.removeIfExists("./libs/pyrt_wit_wire/wit_wire"); - await $`componentize-py -d ../../wit/wit-wire.wit bindings .` - .cwd("./libs/pyrt_wit_wire"); + await $`componentize-py -d ../../wit/wit-wire.wit bindings .`.cwd( + "./libs/pyrt_wit_wire" + ); }, }); @@ -172,3 +169,20 @@ ghjk.task("dev", { await $`deno run --allow-all ${script} ${args}`; }, }); + +ghjk.task("test", { + async fn({ $, argv }) { + const script = $.path(projectDir).join("dev/test.ts"); + await $`deno run --allow-all ${script} ${argv}`; + }, +}); + +export const secureConfig = ghjk.secureConfig({ + allowedPortDeps: [ + ...ghjk.stdDeps(), + ...[installs.python_latest, installs.node].map((fat) => ({ + manifest: fat.port, + defaultInst: thinInstallConfig(fat), + })), + ], +}); diff --git a/meta-cli/src/cli/mod.rs b/meta-cli/src/cli/mod.rs index 8dfad76218..aa44915e7c 100644 --- a/meta-cli/src/cli/mod.rs +++ b/meta-cli/src/cli/mod.rs @@ -96,6 +96,9 @@ pub struct NodeArgs { #[clap(short, long, value_parser = UrlValueParser::new().http())] pub gate: Option, + #[clap(short, long)] + pub prefix: Option, + /// Username to use to connect to the typegate (basic auth). #[clap(long)] pub username: Option, diff --git a/meta-cli/src/config.rs b/meta-cli/src/config.rs index 6396a9c892..d5f7ac3b39 100644 --- a/meta-cli/src/config.rs +++ b/meta-cli/src/config.rs @@ -80,6 +80,9 @@ impl NodeConfig { if let Some(gate) = &args.gate { res.url = gate.clone(); } + if let Some(prefix) = &args.prefix { + res.prefix = Some(prefix.clone()); + } res.username = args.username.clone().or(res.username); res.password = args.password.clone().or(res.password); res diff --git a/typegate/deno.lock b/typegate/deno.lock index e33c9a5a62..703a99645c 100644 --- a/typegate/deno.lock +++ b/typegate/deno.lock @@ -309,6 +309,7 @@ "redirects": { "https://cdn.pika.dev/big.js/^5.2.2": "https://cdn.skypack.dev/big.js@^5.2.2", "https://crux.land/router@0.0.5": "https://crux.land/api/get/2KNRVU.ts", + "https://deno.land/x/dispose/mod.ts": "https://deno.land/x/dispose@1.1.0/mod.ts", "https://github.com/levibostian/deno-udd/raw/ignore-prerelease/mod.ts": "https://raw.githubusercontent.com/levibostian/deno-udd/ignore-prerelease/mod.ts" }, "remote": { @@ -709,6 +710,7 @@ "https://deno.land/std@0.205.0/encoding/_util.ts": "f368920189c4fe6592ab2e93bd7ded8f3065b84f95cd3e036a4a10a75649dcba", "https://deno.land/std@0.205.0/encoding/base64.ts": "cc03110d6518170aeaa68ec97f89c6d6e2276294b30807e7332591d7ce2e4b72", "https://deno.land/std@0.205.0/encoding/base64url.ts": "7608862858d28a003f9d6cb78dd61e645ecd1ae1f45faf0e09a306eafe66b16e", + "https://deno.land/std@0.208.0/async/deferred.ts": "a9d1e54c29a9ce8d949cc82e1e6cd901fa3f4d80ca8a2c5c6d2981d77ae798ca", "https://deno.land/std@0.211.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", "https://deno.land/std@0.211.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", "https://deno.land/std@0.211.0/async/delay.ts": "eab3187eee39ccc8cc76d411fb21fb1801250ddb1090e486d5aec2ace5403391", @@ -915,6 +917,287 @@ "https://deno.land/std@0.219.0/yaml/schema/mod.ts": "9bf7ff80c2a246f781bdcab979211d0389760831a974cf5883bf2016567e3507", "https://deno.land/std@0.219.0/yaml/stringify.ts": "580f8b2fa56e3233424520d8242f4fc0edf41ac54a6a6d2f6f8e0b6e99cd63c0", "https://deno.land/std@0.219.0/yaml/type.ts": "708dde5f20b01cc1096489b7155b6af79a217d585afb841128e78c3c2391eb5c", + "https://deno.land/std@0.221.0/archive/_common.ts": "5fcad5f7280cec1d20540b4a6e43ea9a6fd63daf9c4cf8a58c6321d07c32e317", + "https://deno.land/std@0.221.0/archive/untar.ts": "bd39dbeda737f6fd9409b5923d7172f8abd7826508f09c4a1c38886574f6da4a", + "https://deno.land/std@0.221.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.221.0/assert/_diff.ts": "4bf42969aa8b1a33aaf23eb8e478b011bfaa31b82d85d2ff4b5c4662d8780d2b", + "https://deno.land/std@0.221.0/assert/_format.ts": "0ba808961bf678437fb486b56405b6fefad2cf87b5809667c781ddee8c32aff4", + "https://deno.land/std@0.221.0/assert/assert.ts": "bec068b2fccdd434c138a555b19a2c2393b71dfaada02b7d568a01541e67cdc5", + "https://deno.land/std@0.221.0/assert/assert_almost_equals.ts": "8b96b7385cc117668b0720115eb6ee73d04c9bcb2f5d2344d674918c9113688f", + "https://deno.land/std@0.221.0/assert/assert_array_includes.ts": "1688d76317fd45b7e93ef9e2765f112fdf2b7c9821016cdfb380b9445374aed1", + "https://deno.land/std@0.221.0/assert/assert_equals.ts": "4497c56fe7d2993b0d447926702802fc0becb44e319079e8eca39b482ee01b4e", + "https://deno.land/std@0.221.0/assert/assert_exists.ts": "24a7bf965e634f909242cd09fbaf38bde6b791128ece08e33ab08586a7cc55c9", + "https://deno.land/std@0.221.0/assert/assert_false.ts": "6f382568e5128c0f855e5f7dbda8624c1ed9af4fcc33ef4a9afeeedcdce99769", + "https://deno.land/std@0.221.0/assert/assert_greater.ts": "4945cf5729f1a38874d7e589e0fe5cc5cd5abe5573ca2ddca9d3791aa891856c", + "https://deno.land/std@0.221.0/assert/assert_greater_or_equal.ts": "573ed8823283b8d94b7443eb69a849a3c369a8eb9666b2d1db50c33763a5d219", + "https://deno.land/std@0.221.0/assert/assert_instance_of.ts": "72dc1faff1e248692d873c89382fa1579dd7b53b56d52f37f9874a75b11ba444", + "https://deno.land/std@0.221.0/assert/assert_is_error.ts": "6596f2b5ba89ba2fe9b074f75e9318cda97a2381e59d476812e30077fbdb6ed2", + "https://deno.land/std@0.221.0/assert/assert_less.ts": "2b4b3fe7910f65f7be52212f19c3977ecb8ba5b2d6d0a296c83cde42920bb005", + "https://deno.land/std@0.221.0/assert/assert_less_or_equal.ts": "b93d212fe669fbde959e35b3437ac9a4468f2e6b77377e7b6ea2cfdd825d38a0", + "https://deno.land/std@0.221.0/assert/assert_match.ts": "ec2d9680ed3e7b9746ec57ec923a17eef6d476202f339ad91d22277d7f1d16e1", + "https://deno.land/std@0.221.0/assert/assert_not_equals.ts": "ac86413ab70ffb14fdfc41740ba579a983fe355ba0ce4a9ab685e6b8e7f6a250", + "https://deno.land/std@0.221.0/assert/assert_not_instance_of.ts": "8f720d92d83775c40b2542a8d76c60c2d4aeddaf8713c8d11df8984af2604931", + "https://deno.land/std@0.221.0/assert/assert_not_match.ts": "b4b7c77f146963e2b673c1ce4846473703409eb93f5ab0eb60f6e6f8aeffe39f", + "https://deno.land/std@0.221.0/assert/assert_not_strict_equals.ts": "da0b8ab60a45d5a9371088378e5313f624799470c3b54c76e8b8abeec40a77be", + "https://deno.land/std@0.221.0/assert/assert_object_match.ts": "e85e5eef62a56ce364c3afdd27978ccab979288a3e772e6855c270a7b118fa49", + "https://deno.land/std@0.221.0/assert/assert_rejects.ts": "5206ac37d883797d9504e3915a0c7b692df6efcdefff3889cc14bb5a325641dd", + "https://deno.land/std@0.221.0/assert/assert_strict_equals.ts": "0425a98f70badccb151644c902384c12771a93e65f8ff610244b8147b03a2366", + "https://deno.land/std@0.221.0/assert/assert_string_includes.ts": "dfb072a890167146f8e5bdd6fde887ce4657098e9f71f12716ef37f35fb6f4a7", + "https://deno.land/std@0.221.0/assert/assert_throws.ts": "31f3c061338aec2c2c33731973d58ccd4f14e42f355501541409ee958d2eb8e5", + "https://deno.land/std@0.221.0/assert/assertion_error.ts": "9f689a101ee586c4ce92f52fa7ddd362e86434ffdf1f848e45987dc7689976b8", + "https://deno.land/std@0.221.0/assert/equal.ts": "fae5e8a52a11d3ac694bbe1a53e13a7969e3f60791262312e91a3e741ae519e2", + "https://deno.land/std@0.221.0/assert/fail.ts": "f310e51992bac8e54f5fd8e44d098638434b2edb802383690e0d7a9be1979f1c", + "https://deno.land/std@0.221.0/assert/mod.ts": "7e41449e77a31fef91534379716971bebcfc12686e143d38ada5438e04d4a90e", + "https://deno.land/std@0.221.0/assert/unimplemented.ts": "47ca67d1c6dc53abd0bd729b71a31e0825fc452dbcd4fde4ca06789d5644e7fd", + "https://deno.land/std@0.221.0/assert/unreachable.ts": "3670816a4ab3214349acb6730e3e6f5299021234657eefe05b48092f3848c270", + "https://deno.land/std@0.221.0/bytes/concat.ts": "9cac3b4376afbef98ff03588eb3cf948e0d1eb6c27cfe81a7651ab6dd3adc54a", + "https://deno.land/std@0.221.0/bytes/copy.ts": "f29c03168853720dfe82eaa57793d0b9e3543ebfe5306684182f0f1e3bfd422a", + "https://deno.land/std@0.221.0/collections/_utils.ts": "b2ec8ada31b5a72ebb1d99774b849b4c09fe4b3a38d07794bd010bd218a16e0b", + "https://deno.land/std@0.221.0/collections/chunk.ts": "e6e533d6ae047f2082892831d463426f79cdd0d3f09ce298317a8469abf8467e", + "https://deno.land/std@0.221.0/collections/deep_merge.ts": "04f8d2a6cfa15c7580e788689bcb5e162512b9ccb18bab1241824b432a78551e", + "https://deno.land/std@0.221.0/collections/distinct.ts": "42d81633e4ccd5ea89118cb08f875bbfc683ac6620df2fa60f3d07270b177f4d", + "https://deno.land/std@0.221.0/collections/distinct_by.ts": "e895705decb0ce88b31c6679fd4e2bd08ac6d47cde7d00bf2016e3c2bac565a7", + "https://deno.land/std@0.221.0/collections/filter_values.ts": "ad8fae5751977598f8c794d65f0d42c9f9e47a7d774cf82b2449cd78ff4f729a", + "https://deno.land/std@0.221.0/collections/map_keys.ts": "2fd91963117d2376ea6dbbe473d77c20ede79b72437ed0dac36161b1af62f058", + "https://deno.land/std@0.221.0/collections/map_values.ts": "91d6ece4b4dc4b94abc378f51e0e309e7f7f18b2ce4c335dab673a31ebd17c75", + "https://deno.land/std@0.221.0/crypto/_wasm/lib/deno_std_wasm_crypto.generated.mjs": "f65ea775c52c5641f0154d98d6059e261ca3dc917a8856209d60bc6cb406e699", + "https://deno.land/std@0.221.0/crypto/_wasm/mod.ts": "e89fbbc3c4722602ff975dd85f18273c7741ec766a9b68f6de4fd1d9876409f8", + "https://deno.land/std@0.221.0/crypto/crypto.ts": "7ccd24e766d026d92ee1260b5a1639624775e94456d2a95c3a42fd3d49df78ab", + "https://deno.land/std@0.221.0/crypto/mod.ts": "9148fb70ca3d64977e9487b2002d3b1026e8ad8a2078774b807586ba3c77e3bb", + "https://deno.land/std@0.221.0/crypto/timing_safe_equal.ts": "bc3622b5aec05e2d8b735bf60633425c34333c06cfb6c4a9f102e4a0f3931ced", + "https://deno.land/std@0.221.0/crypto/unstable_keystack.ts": "c2a6f6ed67a4e78745e3c9b490ebb7c12f6066f5c2fe0c69d353961909dc82dd", + "https://deno.land/std@0.221.0/encoding/_util.ts": "beacef316c1255da9bc8e95afb1fa56ed69baef919c88dc06ae6cb7a6103d376", + "https://deno.land/std@0.221.0/encoding/base64.ts": "8ccae67a1227b875340a8582ff707f37b131df435b07080d3bb58e07f5f97807", + "https://deno.land/std@0.221.0/encoding/base64url.ts": "9cc46cf510436be63ac00ebf97a7de1993e603ca58e1853b344bf90d80ea9945", + "https://deno.land/std@0.221.0/encoding/hex.ts": "e939f50d55be48a1fe42fecaaecdb54353df38e831c47f374be7e6fdbe61510e", + "https://deno.land/std@0.221.0/flags/mod.ts": "9f13f3a49c54618277ac49195af934f1c7d235731bcf80fd33b8b234e6839ce9", + "https://deno.land/std@0.221.0/fmt/colors.ts": "d239d84620b921ea520125d778947881f62c50e78deef2657073840b8af9559a", + "https://deno.land/std@0.221.0/fs/_create_walk_entry.ts": "5d9d2aaec05bcf09a06748b1684224d33eba7a4de24cf4cf5599991ca6b5b412", + "https://deno.land/std@0.221.0/fs/_get_file_info_type.ts": "da7bec18a7661dba360a1db475b826b18977582ce6fc9b25f3d4ee0403fe8cbd", + "https://deno.land/std@0.221.0/fs/_is_same_path.ts": "709c95868345fea051c58b9e96af95cff94e6ae98dfcff2b66dee0c212c4221f", + "https://deno.land/std@0.221.0/fs/_is_subdir.ts": "c68b309d46cc8568ed83c000f608a61bbdba0943b7524e7a30f9e450cf67eecd", + "https://deno.land/std@0.221.0/fs/_to_path_string.ts": "29bfc9c6c112254961d75cbf6ba814d6de5349767818eb93090cecfa9665591e", + "https://deno.land/std@0.221.0/fs/copy.ts": "7ab12a16adb65d155d4943c88081ca16ce3b0b5acada64c1ce93800653678039", + "https://deno.land/std@0.221.0/fs/empty_dir.ts": "e400e96e1d2c8c558a5a1712063bd43939e00619c1d1cc29959babc6f1639418", + "https://deno.land/std@0.221.0/fs/ensure_dir.ts": "313e8a62b8bb20d900138ff794bde6a6ac0a6bebc91220fba6dfc3303bde56c6", + "https://deno.land/std@0.221.0/fs/ensure_file.ts": "67608cf550529f3d4aa1f8b6b36bf817bdc40b14487bf8f60e61cbf68f507cf3", + "https://deno.land/std@0.221.0/fs/ensure_link.ts": "5c98503ebfa9cc05e2f2efaa30e91e60b4dd5b43ebbda82f435c0a5c6e3ffa01", + "https://deno.land/std@0.221.0/fs/ensure_symlink.ts": "cafe904cebacb9a761977d6dbf5e3af938be946a723bb394080b9a52714fafe4", + "https://deno.land/std@0.221.0/fs/eol.ts": "18c4ac009d0318504c285879eb7f47942643f13619e0ff070a0edc59353306bd", + "https://deno.land/std@0.221.0/fs/exists.ts": "3d38cb7dcbca3cf313be343a7b8af18a87bddb4b5ca1bd2314be12d06533b50f", + "https://deno.land/std@0.221.0/fs/expand_glob.ts": "2e428d90acc6676b2aa7b5c78ef48f30641b13f1fe658e7976c9064fb4b05309", + "https://deno.land/std@0.221.0/fs/mod.ts": "c25e6802cbf27f3050f60b26b00c2d8dba1cb7fcdafe34c66006a7473b7b34d4", + "https://deno.land/std@0.221.0/fs/move.ts": "ca205d848908d7f217353bc5c623627b1333490b8b5d3ef4cab600a700c9bd8f", + "https://deno.land/std@0.221.0/fs/walk.ts": "cddf87d2705c0163bff5d7767291f05b0f46ba10b8b28f227c3849cace08d303", + "https://deno.land/std@0.221.0/http/cookie.ts": "a377fa60175ba5f61dd4b8a70b34f2bbfbc70782dfd5faf36d314c42e4306006", + "https://deno.land/std@0.221.0/io/_common.ts": "36705cdb4dfcd338d6131bca1b16e48a4d5bf0d1dada6ce397268e88c17a5835", + "https://deno.land/std@0.221.0/io/_constants.ts": "3c7ad4695832e6e4a32e35f218c70376b62bc78621ef069a4a0a3d55739f8856", + "https://deno.land/std@0.221.0/io/buf_reader.ts": "e6023dfdda5ab2393588e57a191c7c2a310df5f58abcb00abfd07c8a90c7b8ac", + "https://deno.land/std@0.221.0/io/buffer.ts": "4d1f805f350433e418002accec798bc6c33ce18f614afa65f987c202d7b2234e", + "https://deno.land/std@0.221.0/io/iterate_reader.ts": "1e5e4fea22d8965afb7df4ee9ab9adda0a0fc581adbea31bc2f2d25453f8a6e9", + "https://deno.land/std@0.221.0/io/read_all.ts": "876c1cb20adea15349c72afc86cecd3573335845ae778967aefb5e55fe5a8a4a", + "https://deno.land/std@0.221.0/io/reader_from_stream_reader.ts": "a75bbc93f39df8b0e372cc1fbdc416a7cbf2a39fc4c09ddb057f1241100191c5", + "https://deno.land/std@0.221.0/io/to_readable_stream.ts": "ed03a44a1ec1cc55a85a857acf6cac472035298f6f3b6207ea209f93b4aefb39", + "https://deno.land/std@0.221.0/io/to_writable_stream.ts": "ef422e0425963c8a1e0481674e66c3023da50f0acbe5ef51ec9789efc3c1e2ed", + "https://deno.land/std@0.221.0/io/types.ts": "acecb3074c730b5ff487ba4fe9ce51e67bd982aa07c95e5f5679b7b2f24ad129", + "https://deno.land/std@0.221.0/io/write_all.ts": "24aac2312bb21096ae3ae0b102b22c26164d3249dff96dbac130958aa736f038", + "https://deno.land/std@0.221.0/log/_config.ts": "489e11b6d3c917bf5fc954c5e914c095d3480efd924d1e85f2fc576468581c54", + "https://deno.land/std@0.221.0/log/_state.ts": "314c0c31ab9c8f4fb33326ad446757d35f75e5bb21746b7720ed4e3f3a939da1", + "https://deno.land/std@0.221.0/log/base_handler.ts": "fd03a8e0c58ca49c52bf51df6f8fe4eabbeb11a2d966840f563bcdc150441442", + "https://deno.land/std@0.221.0/log/console_handler.ts": "9a1e96b00b86d98e31def5439d27139efeaceb6fdfee567ee800eb90a5468442", + "https://deno.land/std@0.221.0/log/critical.ts": "6eb2290dbe40e42bd8f936a5453d254f36e1e30a737cddfec541573b70bb7cd2", + "https://deno.land/std@0.221.0/log/debug.ts": "8fd921996842aa19767d318a93848f800dad4d514bdc4fd36ebd0c0d9ff71414", + "https://deno.land/std@0.221.0/log/error.ts": "41e86c437c4ee4310950838c8d50ac63e9ed4e34b65766db8027382914f230dd", + "https://deno.land/std@0.221.0/log/file_handler.ts": "68d6d81ec53bdd6ba61eaceec19d12de59a8ad12ace0d7980a592a51f924a242", + "https://deno.land/std@0.221.0/log/formatters.ts": "d3d07d5e1e160adee7b8f0b493ee9b98d621b65754f49d1891ae089af3641288", + "https://deno.land/std@0.221.0/log/get_logger.ts": "9c153ea3642e7fdabad752f65211d0e147d1863a07aef5c28991bc1e89df042c", + "https://deno.land/std@0.221.0/log/info.ts": "7343716d8d08d3f40ac07844c2b96df4538dfcc92755559ee2a5fac166752185", + "https://deno.land/std@0.221.0/log/levels.ts": "632ba12baa2600750d004cc5cb4eabe10e410f3f2bdfcb9f7142b6d767f2fee6", + "https://deno.land/std@0.221.0/log/logger.ts": "a7a21f53c59f0d16227373d04ea302277a8d5be278105d71afe85b3d7b12067f", + "https://deno.land/std@0.221.0/log/mod.ts": "e4ad2e0925dbcb9047621e06560efe5285e3a589b56d5213d102f379c12a1c3e", + "https://deno.land/std@0.221.0/log/rotating_file_handler.ts": "a6e7c712e568b618303273ff95483f6ab86dec0a485c73c2e399765f752b5aa8", + "https://deno.land/std@0.221.0/log/setup.ts": "76142bb52b632452310760e99bb6637eeebd811144b8d2a3a07c72ce673e4365", + "https://deno.land/std@0.221.0/log/warn.ts": "3984feabb641d9ca8ef8abb200e0d4ff4a1ba3aedaa4666e3e43333e762bdbf4", + "https://deno.land/std@0.221.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", + "https://deno.land/std@0.221.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", + "https://deno.land/std@0.221.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", + "https://deno.land/std@0.221.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", + "https://deno.land/std@0.221.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.221.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", + "https://deno.land/std@0.221.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", + "https://deno.land/std@0.221.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", + "https://deno.land/std@0.221.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", + "https://deno.land/std@0.221.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", + "https://deno.land/std@0.221.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", + "https://deno.land/std@0.221.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", + "https://deno.land/std@0.221.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", + "https://deno.land/std@0.221.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", + "https://deno.land/std@0.221.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", + "https://deno.land/std@0.221.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", + "https://deno.land/std@0.221.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", + "https://deno.land/std@0.221.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", + "https://deno.land/std@0.221.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", + "https://deno.land/std@0.221.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", + "https://deno.land/std@0.221.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", + "https://deno.land/std@0.221.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", + "https://deno.land/std@0.221.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", + "https://deno.land/std@0.221.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", + "https://deno.land/std@0.221.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", + "https://deno.land/std@0.221.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", + "https://deno.land/std@0.221.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", + "https://deno.land/std@0.221.0/path/mod.ts": "2821a1bb3a4148a0ffe79c92aa41aa9319fef73c6d6f5178f52b2c720d3eb02d", + "https://deno.land/std@0.221.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", + "https://deno.land/std@0.221.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", + "https://deno.land/std@0.221.0/path/parse.ts": "3e172974e3c71025f5fbd2bd9db4307acb9cc2de14cf6f4464bf40957663cabe", + "https://deno.land/std@0.221.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", + "https://deno.land/std@0.221.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", + "https://deno.land/std@0.221.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.221.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", + "https://deno.land/std@0.221.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", + "https://deno.land/std@0.221.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", + "https://deno.land/std@0.221.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", + "https://deno.land/std@0.221.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", + "https://deno.land/std@0.221.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", + "https://deno.land/std@0.221.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", + "https://deno.land/std@0.221.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.221.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", + "https://deno.land/std@0.221.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.221.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.221.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", + "https://deno.land/std@0.221.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.221.0/path/posix/parse.ts": "0b1fc4cb890dbb699ec1d2c232d274843b4a7142e1ad976b69fe51c954eb6080", + "https://deno.land/std@0.221.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", + "https://deno.land/std@0.221.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", + "https://deno.land/std@0.221.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", + "https://deno.land/std@0.221.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", + "https://deno.land/std@0.221.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", + "https://deno.land/std@0.221.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", + "https://deno.land/std@0.221.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", + "https://deno.land/std@0.221.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", + "https://deno.land/std@0.221.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", + "https://deno.land/std@0.221.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", + "https://deno.land/std@0.221.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", + "https://deno.land/std@0.221.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", + "https://deno.land/std@0.221.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", + "https://deno.land/std@0.221.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", + "https://deno.land/std@0.221.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", + "https://deno.land/std@0.221.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", + "https://deno.land/std@0.221.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", + "https://deno.land/std@0.221.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", + "https://deno.land/std@0.221.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", + "https://deno.land/std@0.221.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", + "https://deno.land/std@0.221.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", + "https://deno.land/std@0.221.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", + "https://deno.land/std@0.221.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", + "https://deno.land/std@0.221.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", + "https://deno.land/std@0.221.0/path/windows/parse.ts": "dbdfe2bc6db482d755b5f63f7207cd019240fcac02ad2efa582adf67ff10553a", + "https://deno.land/std@0.221.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", + "https://deno.land/std@0.221.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", + "https://deno.land/std@0.221.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", + "https://deno.land/std@0.221.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", + "https://deno.land/std@0.221.0/semver/_constants.ts": "5ef89c5f33e6095546ae3e57920592feefcb8372d4cc05542f6bf15a1977e3c9", + "https://deno.land/std@0.221.0/semver/_shared.ts": "5c53a675225cba9ad74ae2e17c124e333728fc2b551a13e8a32b99433b90c1c2", + "https://deno.land/std@0.221.0/semver/can_parse.ts": "d4a26f74be078f3ab10293b07bf022021a2f362b3e21b58422c214e7268110b2", + "https://deno.land/std@0.221.0/semver/compare.ts": "e8871844a35cc8fe16e883c16e5237e06a93aa4830ae10d06501abe63586fc57", + "https://deno.land/std@0.221.0/semver/constants.ts": "a0daa58502949654af044928f86288d8b27bd1880218e9faba7733ec0bde63ab", + "https://deno.land/std@0.221.0/semver/difference.ts": "be4f01b7745406408a16b708185a48c1c652cc87e0244b12a5ca75c5585db668", + "https://deno.land/std@0.221.0/semver/equals.ts": "8b9b18260c9a55feee9d3f9250fba345be922380f2e8f8009e455c394ce5e81d", + "https://deno.land/std@0.221.0/semver/format.ts": "26d3a357ac5abd73dee0fe7dbbac6107fbdce0a844370c7b1bcb673c92e46bf6", + "https://deno.land/std@0.221.0/semver/format_range.ts": "6ad2d0c27aac63dfb7efca6286a6ab7742accfb986cc53662047740f17dacfe5", + "https://deno.land/std@0.221.0/semver/greater_or_equal.ts": "89c26f68070896944676eb9704cbb617febc6ed693720282741d6859c3d1fe80", + "https://deno.land/std@0.221.0/semver/greater_than.ts": "d8c4a227cd28ea80a1de9c80215d7f3f95786fe1b196f0cb5ec91d6567adad27", + "https://deno.land/std@0.221.0/semver/increment.ts": "427a043be71d6481e45c1a3939b955e800924d70779cb297b872d9cbf9f0e46d", + "https://deno.land/std@0.221.0/semver/is_range.ts": "15dd9a8d6a8dee56dea6799d8c8210e06c0a38cc1a9aa6152aeea39ce45e2111", + "https://deno.land/std@0.221.0/semver/is_semver.ts": "57914027d6141e593eb04418aaabbfd6f4562a1c53c6c33a1743fa50ada8d849", + "https://deno.land/std@0.221.0/semver/less_or_equal.ts": "7dbf8190f37f3281048c30cf11e072a7af18685534ae88d295baa170b485bd90", + "https://deno.land/std@0.221.0/semver/less_than.ts": "b0c7902c54cecadcc7c1c80afc2f6a0f1bf0b3f53c8d2bfd11f01a3a414cccfe", + "https://deno.land/std@0.221.0/semver/max_satisfying.ts": "03e5182a7424c308ddbb410e4b927da0dabc4e07d4b5a72f7e9b26fb18a02152", + "https://deno.land/std@0.221.0/semver/min_satisfying.ts": "b6fadc9af17278289481c416e1eb135614f88063f4fc2b7b72b43eb3baa2f08f", + "https://deno.land/std@0.221.0/semver/mod.ts": "6e1f8854cec50c027037a597d3dd54af72e063f763ec0cbc4ea1e534a627ffae", + "https://deno.land/std@0.221.0/semver/not_equals.ts": "17147a6f68b9d14f4643c1e2150378ccf6954710309f9618f75b411752a8e13d", + "https://deno.land/std@0.221.0/semver/parse.ts": "b64052ff8ce0b0bba9ed97b835a224c828fc7ad227585c3e8c2bac72a07bf572", + "https://deno.land/std@0.221.0/semver/parse_range.ts": "5190afffc90cc14410ce2478fb81ed83b55d7e96b6fbbd69fc715c457082ec94", + "https://deno.land/std@0.221.0/semver/range_intersects.ts": "2c358f7c27b51960a9889be5462ec1cac44feeb5e40041a5c5a03700c0ddc017", + "https://deno.land/std@0.221.0/semver/range_max.ts": "4c43d018841ba67d018e515f0aef32658dab611a39fdc74e31b1e48709be281c", + "https://deno.land/std@0.221.0/semver/range_min.ts": "37c5e3dac7bd63812ae249add9ea815a351826f510d2baf391c225d1d8272d17", + "https://deno.land/std@0.221.0/semver/test_range.ts": "72ba2af827e4ad94db9a29e22e86cbec9b3f8519fc36fd6ce0d4308717536c70", + "https://deno.land/std@0.221.0/semver/try_parse.ts": "7e2a3594212445d9d6f6154f02288d66a0c0b79ce3e859c41f3d47e29dfa439a", + "https://deno.land/std@0.221.0/semver/try_parse_range.ts": "4f211f0ff4f5fdaa85622ab96c360123bbcf0e5a91a57eb7a8258af6b7a3c704", + "https://deno.land/std@0.221.0/semver/types.ts": "13e1e0c64a4ac76f0add74afee6240c92a7dba184e63e0bd4cb456afed8c7291", + "https://deno.land/std@0.221.0/streams/_common.ts": "948735ef6d140cd6916dca861197b88fc57db52c2f923c392b7a14033d8fed4b", + "https://deno.land/std@0.221.0/streams/buffer.ts": "e012de72a53ad17c56512488e9afb6f4b6ed046b32fc1415ae7a4e6fc0efce38", + "https://deno.land/std@0.221.0/streams/byte_slice_stream.ts": "5bbdcadb118390affa9b3d0a0f73ef8e83754f59bb89df349add669dd9369713", + "https://deno.land/std@0.221.0/streams/delimiter_stream.ts": "4e4050740ff27a8824defa6c96126229ef9d794c4ace4ef9cabb10b5ad4a5d14", + "https://deno.land/std@0.221.0/streams/early_zip_readable_streams.ts": "21f5cf6dd36381c6a50c31a7727b5bd219f6382bbb7a413418595c3e466c4d14", + "https://deno.land/std@0.221.0/streams/iterate_reader.ts": "a50bed95514736c3c554e4c69ea2d8d2699252e4e74507769999d22c3886c777", + "https://deno.land/std@0.221.0/streams/limited_bytes_transform_stream.ts": "b22a45a337374e863c4eb1867ec6b8ad3e68620a6c52fe837746060ea610e6f1", + "https://deno.land/std@0.221.0/streams/limited_transform_stream.ts": "4c47da5ca38a30fa9f33b0f1a61d4548e7f52a9a58c294b0f430f680e44cc543", + "https://deno.land/std@0.221.0/streams/merge_readable_streams.ts": "73eed8ff54c9111b8b974b11a5a11c1ed0b7800e0157c39277ccac3ed14721e2", + "https://deno.land/std@0.221.0/streams/mod.ts": "d56624832b9649b680c74ab9c77e746e8be81ae1a24756cc04623e25a0d43ce9", + "https://deno.land/std@0.221.0/streams/readable_stream_from_reader.ts": "4289a63836f73901441c1879f2be76eea2a983920f4b10a4a9b8a6d8c29ece56", + "https://deno.land/std@0.221.0/streams/reader_from_iterable.ts": "82f1ecef32ec70c7841ca2a54b09c73167d940f0578bec91e874bb8cd42ba8f3", + "https://deno.land/std@0.221.0/streams/reader_from_stream_reader.ts": "dda702bd365a133be8bdbc5a1ba96c67b350c3504410632f3a833895bfc7bae3", + "https://deno.land/std@0.221.0/streams/text_delimiter_stream.ts": "94dfc900204e306496c1b58c80473db57b6097afdcb8ea9eaff453a193a659f1", + "https://deno.land/std@0.221.0/streams/text_line_stream.ts": "21f33d3922e019ec1a1676474beb543929cb564ec99b69cd2654e029e0f45bd5", + "https://deno.land/std@0.221.0/streams/to_array_buffer.ts": "1a9c07c4a396ce557ab205c44415815ab13b614fed94a12f62b80f8e650c726d", + "https://deno.land/std@0.221.0/streams/to_blob.ts": "bf5daaae50fa8f57e0c8bfd7474ebac16ac09e130e3d01ef2947ae5153912b4a", + "https://deno.land/std@0.221.0/streams/to_json.ts": "b6a908d0da7cd30956e5fbbfa7460747e50b8f307d1041282ed6fe9070d579ee", + "https://deno.land/std@0.221.0/streams/to_text.ts": "6f93593bdfc2cea5cca39755ea5caf0d4092580c0a713dfe04a1e85c60df331f", + "https://deno.land/std@0.221.0/streams/to_transform_stream.ts": "4c4836455ef89bab9ece55975ee3a819f07d3d8b0e43101ec7f4ed033c8a2b61", + "https://deno.land/std@0.221.0/streams/writable_stream_from_writer.ts": "62f2712d3a7bebd981fca8bd5140192c37450f9c4aa94283f7ca833e46bc7485", + "https://deno.land/std@0.221.0/streams/writer_from_stream_writer.ts": "b0e39ef607dfdc5abdfb627edf61a9672809463e2bb022afcbaf0cd006c40feb", + "https://deno.land/std@0.221.0/streams/zip_readable_streams.ts": "53eb10d7557539b489bd858907aab6dd28247f074b3446573801de3150cb932e", + "https://deno.land/std@0.221.0/testing/snapshot.ts": "35ca1c8e8bfb98d7b7e794f1b7be8d992483fcff572540e41396f22a5bddb944", + "https://deno.land/std@0.221.0/url/_strip.ts": "928fe9af16d7c5bf24816d1e90d84bfe702f3e059f9d63509b5a37087e947800", + "https://deno.land/std@0.221.0/url/basename.ts": "a2e6ef35d44da3764551cbc61cdd39004c778aaedc7a6c2559e571f018c42daa", + "https://deno.land/std@0.221.0/url/dirname.ts": "0915864aac7d2d0413c90dff7841b18b29c83ed102fa340e760af1fb2c0ad26c", + "https://deno.land/std@0.221.0/url/extname.ts": "b247eac636161c5e263220c6e5116ed10e0c1702b5e90fad258a88c0b3b6bf98", + "https://deno.land/std@0.221.0/url/join.ts": "00c7e9088cafaa24963ce4081119e58b3afe2c58f033701383f359ea02620dd2", + "https://deno.land/std@0.221.0/url/mod.ts": "e2621f6a0db6fdbe7fbbd240064095bb203014657e5e1ab81db1c44d80dce6c9", + "https://deno.land/std@0.221.0/url/normalize.ts": "6328c75df0fab300f74bc4a1c255062a0db882240e15ab646606d0009e7e40d7", + "https://deno.land/std@0.221.0/uuid/_common.ts": "05c787c5735776c4e48e30294878332c39cb7738f50b209df4eb9f2b0facce4d", + "https://deno.land/std@0.221.0/uuid/constants.ts": "eb6c96871e968adf3355507d7ae79adce71525fd6c1ca55c51d32ace0196d64e", + "https://deno.land/std@0.221.0/uuid/mod.ts": "cefc8e2f77d9e493739c8dc4ec141b12b855414bf757e778bf9b00f783506b76", + "https://deno.land/std@0.221.0/uuid/v1.ts": "cc45e7eb1d463d7d38b21a3c6e4de55ff98598ca442309321575fe841b323a54", + "https://deno.land/std@0.221.0/uuid/v3.ts": "689f2d64a9460a75877a2eed94662d9cb31bedb890d72fce0d161ef47d66cc26", + "https://deno.land/std@0.221.0/uuid/v4.ts": "1319a2eeff7259adda416ec5f7997ded80d3165ef0787012793fc8621c18c493", + "https://deno.land/std@0.221.0/uuid/v5.ts": "75f76d9e53583572fe3d4893168530986222d439b1545b56d4493c6d5d1cd81d", + "https://deno.land/std@0.221.0/yaml/_dumper/dumper.ts": "08b595b40841a2e1c75303f5096392323b6baf8e9662430a91e3b36fbe175fe9", + "https://deno.land/std@0.221.0/yaml/_dumper/dumper_state.ts": "9e29f700ea876ed230b43f11fa006fcb1a62eedc1e27d32baaeaf3210f19f1e7", + "https://deno.land/std@0.221.0/yaml/_error.ts": "f38cdebdb69cde16903d9aa2f3b8a3dd9d13e5f7f3570bf662bfaca69fef669e", + "https://deno.land/std@0.221.0/yaml/_loader/loader.ts": "bf9e8a99770b59bc887b43ebccea108cbe9146ae32d91f7ce558d62c946d3fe3", + "https://deno.land/std@0.221.0/yaml/_loader/loader_state.ts": "ee216de6040551940b85473c3185fdb7a6f3030b77153f87a6b7f63f82e489ea", + "https://deno.land/std@0.221.0/yaml/_mark.ts": "61097a614857fcebf7b2ecad057916d74c90cd160117a33c9e74bac60457410a", + "https://deno.land/std@0.221.0/yaml/_state.ts": "f3b1c1fd11860302f1f33e35e9ce089bf069d4943e8d67516cd6bedbba058c13", + "https://deno.land/std@0.221.0/yaml/_type/binary.ts": "f1a6e1d83dcc52b21cc3639cd98be44051cfc54065cc4f2a42065bce07ebc07d", + "https://deno.land/std@0.221.0/yaml/_type/bool.ts": "121743b23ba82a27ad6a3ec6298c7f5b0908f90e52707f8644a91f7ad51ed2ef", + "https://deno.land/std@0.221.0/yaml/_type/float.ts": "c5ed84b0aec1ec5dc05f6abfaaff672e8890d4d44a42120b4445c9754fca4eba", + "https://deno.land/std@0.221.0/yaml/_type/function.ts": "bbf705058942bf3370604b37eb77a10aadd72f986c237c9f69b43378a42202c1", + "https://deno.land/std@0.221.0/yaml/_type/int.ts": "c2dc88438a60fccc8d2226042bd18b9967753adaf6bd145feb8b99d567e432ce", + "https://deno.land/std@0.221.0/yaml/_type/map.ts": "ae2acb1cb837fb8e96c75c98611cfd45af847d0114ab5336333c318e7d4b12f4", + "https://deno.land/std@0.221.0/yaml/_type/merge.ts": "ad0d971f91d2fb9f4ab3eba0c837eae357b1804d6b798adc99dc917bc5306b11", + "https://deno.land/std@0.221.0/yaml/_type/mod.ts": "e8929d7b1c969a74f76338d4eb380ef8c4a26cd6441117d521f076b766e9c265", + "https://deno.land/std@0.221.0/yaml/_type/nil.ts": "cbe4387d02d5933322c21b25d8955c5e6228c492e391a6fb82dcf4f498cc421c", + "https://deno.land/std@0.221.0/yaml/_type/omap.ts": "cda915105ab22ba9e1d6317adacee8eec2d8ddaf864cc2f814e3e476946e72c6", + "https://deno.land/std@0.221.0/yaml/_type/pairs.ts": "dd39bb44c1b9abaf6172c63f73350475933151f07e05253b81f7860c9b507177", + "https://deno.land/std@0.221.0/yaml/_type/regexp.ts": "e49eb9e1c9356fd142bc15f7f323820d411fcc537b5ba3896df9a8b812d270a4", + "https://deno.land/std@0.221.0/yaml/_type/seq.ts": "2deffc7f970869bc01a1541b4961d076329a1c2b30b95e07918f3132db7c3fe2", + "https://deno.land/std@0.221.0/yaml/_type/set.ts": "be8a9e7237a7ffc92dfbe7f5e552d84b7eeba60f3f73cc77fc3c59d3506c74ea", + "https://deno.land/std@0.221.0/yaml/_type/str.ts": "88f0a1ba12295520cd57e96cd78d53aa0787d53c7a1c506155f418c496c2f550", + "https://deno.land/std@0.221.0/yaml/_type/timestamp.ts": "277a41a40fb93c3b2b3f5c373bf11b0b7856cc6a7b919e8ea130755e4029edc5", + "https://deno.land/std@0.221.0/yaml/_type/undefined.ts": "9d215953c65740f1764e0bdca021007573473f0c49e087f00d9ff02817ecfc97", + "https://deno.land/std@0.221.0/yaml/_utils.ts": "91bbe28b5e7000b9594e40ff5353f8fe7a7ba914eec917e1202cbaf5ac931c58", + "https://deno.land/std@0.221.0/yaml/mod.ts": "54e9bfad77c8cd58f49b65f4d568045ff08989ed36318a2ca733a43cb6f1bc00", + "https://deno.land/std@0.221.0/yaml/parse.ts": "f45278d9ebccb789af4eceeffa5c291e194bcf1fa9aab1b34ff52c2bd4a9d886", + "https://deno.land/std@0.221.0/yaml/schema.ts": "a0f7956d997852b5d1c6564bd73eb7352175cfba439707ac819b65b5a2ec173a", + "https://deno.land/std@0.221.0/yaml/schema/core.ts": "1222f9401e2a0c1d38e63d753da98be333e61a6032335e9c46a68bd45ecce85a", + "https://deno.land/std@0.221.0/yaml/schema/default.ts": "b77c71cfd453951dd828e5f2f02f9f37335c9c0a49c8051d1a9653fa82357740", + "https://deno.land/std@0.221.0/yaml/schema/extended.ts": "996da59626409047b5c1a2d68bdbeead43914cedede47c5923e80ae4febe7d24", + "https://deno.land/std@0.221.0/yaml/schema/failsafe.ts": "24b2b630cef6fcce7de6d29db651523b0f49e5691d690931c42ecf4823837fdb", + "https://deno.land/std@0.221.0/yaml/schema/json.ts": "0fb9268282d266c24d963e75ef77f51accbbb74f40713a99e83ad621a81bc9ae", + "https://deno.land/std@0.221.0/yaml/schema/mod.ts": "9bf7ff80c2a246f781bdcab979211d0389760831a974cf5883bf2016567e3507", + "https://deno.land/std@0.221.0/yaml/stringify.ts": "f0ed4e419cb40c807cf79ae4039d6cdf492be9a947121fff4d4b7cd1d4738bae", + "https://deno.land/std@0.221.0/yaml/type.ts": "708dde5f20b01cc1096489b7155b6af79a217d585afb841128e78c3c2391eb5c", "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5", "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff", "https://deno.land/x/color_util@1.0.1/colors/cmykcolor.ts": "f717cee02bdec255c7c2879b55033da7547d46c1fbb8ada7980d49bd2c1554ee", @@ -973,6 +1256,11 @@ "https://deno.land/x/deno_graph@0.26.0/lib/snippets/deno_graph-de651bc9c240ed8d/src/deno_apis.js": "41192baaa550a5c6a146280fae358cede917ae16ec4e4315be51bef6631ca892", "https://deno.land/x/deno_graph@0.26.0/mod.ts": "11131ae166580a1c7fa8506ff553751465a81c263d94443f18f353d0c320bc14", "https://deno.land/x/dir@1.5.1/data_local_dir/mod.ts": "91eb1c4bfadfbeda30171007bac6d85aadacd43224a5ed721bbe56bc64e9eb66", + "https://deno.land/x/dispose@1.1.0/async-disposable-stack.ts": "15bb66608a6145be63eabf12fa63462fb7553af9440671261424b6e3d4042405", + "https://deno.land/x/dispose@1.1.0/async-disposable.ts": "38399695820f15de40a36e732448f45bdd262ec0a6a281a8fd4f9449831655d3", + "https://deno.land/x/dispose@1.1.0/disposable-stack.ts": "97bd374fcbfd19cb5311302b7b3b4fd03c06a7aebf59f1ade735906eb5e2888c", + "https://deno.land/x/dispose@1.1.0/disposable.ts": "4e47a6d4229f9fe6fc500e7b88c1aa68e2e815e4d3b2c1d661570f4b0b2c41cc", + "https://deno.land/x/dispose@1.1.0/mod.ts": "90b3c6db83c29d3f2bcf4bd7c0a3676b79dd356708562339adb3e79daba34efd", "https://deno.land/x/djwt@v2.7/algorithm.ts": "ba9941961c46838f35a507414407e48aa9a4eca69c679b04fbbede55fe276a09", "https://deno.land/x/djwt@v2.7/deps.ts": "a5d7952aaf7fad421717c9a2db0b2e736b409632cb70f3f7f9e68f8e96e04f45", "https://deno.land/x/djwt@v2.7/mod.ts": "08cb2c745c9bc33883c2d027fc4af5c157f0a30564c3ba503a56fe0ab6959c8e", diff --git a/typegate/import_map.json b/typegate/import_map.json index 809d99a960..a3c853668e 100644 --- a/typegate/import_map.json +++ b/typegate/import_map.json @@ -1,7 +1,7 @@ { "imports": { "native": "./engine/bindings.ts", - "std/": "https://deno.land/std@0.202.0/", + "std/": "https://deno.land/std@0.221.0/", "compress/": "https://deno.land/x/compress@v0.4.5/", "graphql": "npm:graphql@16.8.1", "graphql/ast": "npm:graphql@16.8.1/language/ast.js", @@ -25,7 +25,9 @@ "ajv": "https://esm.sh/ajv@8.12.0?pin=v131", "@typegraph/sdk": "npm:@typegraph/sdk@0.3.7-0", "@typegraph/sdk/": "npm:/@typegraph/sdk@0.3.7-0/", + "@typegate/": "./src/", "test-utils/": "./tests/utils/", - "dax": "https://deno.land/x/dax@0.39.2/mod.ts" + "dax": "https://deno.land/x/dax@0.39.2/mod.ts", + "dispose": "https://deno.land/x/dispose@1.1.0/mod.ts" } } diff --git a/typegate/src/config.ts b/typegate/src/config.ts index 8618702076..1ac7fb6dcf 100644 --- a/typegate/src/config.ts +++ b/typegate/src/config.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { mapKeys } from "std/collections/map_keys.ts"; -import * as base64 from "std/encoding/base64.ts"; +import { decodeBase64 } from "std/encoding/base64.ts"; import { parse } from "std/flags/mod.ts"; import { join } from "std/path/mod.ts"; // This import ensure log loads before config, important for the version hydration @@ -18,7 +18,7 @@ const schema = { hostname: z.string(), tg_port: z.coerce.number().positive().max(65535), tg_secret: z.string().transform((s: string, ctx) => { - const bytes = base64.decode(s); + const bytes = decodeBase64(s); if (bytes.length != 64) { ctx.addIssue({ code: z.ZodIssueCode.custom, diff --git a/typegate/src/crypto.ts b/typegate/src/crypto.ts index 5347c87923..cbba296449 100644 --- a/typegate/src/crypto.ts +++ b/typegate/src/crypto.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Elastic-2.0 import { crypto } from "std/crypto/mod.ts"; -import * as base64 from "std/encoding/base64url.ts"; +import { decodeBase64Url, encodeBase64Url } from "std/encoding/base64url.ts"; import config from "./config.ts"; import * as jwt from "jwt"; @@ -12,7 +12,7 @@ export const sha1 = (text: string | Uint8Array): Promise => { "SHA-1", typeof text === "string" ? new TextEncoder().encode(text) : text, ) - .then(base64.encode); + .then(encodeBase64Url); }; export const sha256 = (text: string | Uint8Array): Promise => { @@ -21,7 +21,7 @@ export const sha256 = (text: string | Uint8Array): Promise => { "SHA-256", typeof text === "string" ? new TextEncoder().encode(text) : text, ) - .then(base64.encode); + .then(encodeBase64Url); }; export const signKey = await crypto.subtle.importKey( @@ -56,11 +56,11 @@ export async function encrypt(message: string): Promise { const buffer = new Uint8Array(ivLength + cipher.byteLength); buffer.set(iv, 0); buffer.set(new Uint8Array(cipher), ivLength); - return base64.encode(buffer.buffer); + return encodeBase64Url(buffer.buffer); } export async function decrypt(payload: string): Promise { - const buffer = base64.decode(payload); + const buffer = decodeBase64Url(payload); const iv = buffer.slice(0, ivLength); const cipher = buffer.slice(ivLength); const data = await crypto.subtle.decrypt( diff --git a/typegate/src/engine/query_engine.ts b/typegate/src/engine/query_engine.ts index d4eac71ffa..a6480926b2 100644 --- a/typegate/src/engine/query_engine.ts +++ b/typegate/src/engine/query_engine.ts @@ -104,17 +104,17 @@ class QueryCache { } const effectToMethod = { - "read": "GET", - "create": "POST", - "update": "PUT", - "delete": "DELETE", + read: "GET", + create: "POST", + update: "PUT", + delete: "DELETE", }; export interface EndpointToSchemaMap { [index: string]: { fnName: string; outputSchema: unknown }; } -export class QueryEngine { +export class QueryEngine implements AsyncDisposable { name: string; queryCache: QueryCache; logger: log.Logger; @@ -136,20 +136,21 @@ export class QueryEngine { return this.tg.rawName; } - public constructor( - public tg: TypeGraph, - ) { + public constructor(public tg: TypeGraph) { this.tg = tg; this.name = tg.name; this.queryCache = new QueryCache(); this.logger = log.getLogger("engine"); this.rest = { - "GET": {}, - "POST": {}, - "PUT": {}, - "DELETE": {}, + GET: {}, + POST: {}, + PUT: {}, + DELETE: {}, }; } + async [Symbol.asyncDispose]() { + await this.tg[Symbol.asyncDispose](); + } async registerEndpoints() { for (const query of this.tg.tg.meta.queries.endpoints) { @@ -169,11 +170,13 @@ export class QueryEngine { false, ); - const effects = Array.from(new Set( - plan.stages.filter((s) => s.props.parent == null).map((s) => - s.props.effect - ), - ).values()); + const effects = Array.from( + new Set( + plan.stages + .filter((s) => s.props.parent == null) + .map((s) => s.props.effect), + ).values(), + ); if (effects.length !== 1) { throw new Error("root fields in query must be of the same effect"); @@ -200,11 +203,13 @@ export class QueryEngine { if (fnName) { // Note: (query | mutation) { , , .. } const match = this.tg.tg.types - .filter((tpe) => - tpe.type == "object" && - (tpe.title == "Query" || tpe.title == "Mutation") && - tpe.properties[fnName] != undefined - ).shift() as ObjectNode; + .filter( + (tpe) => + tpe.type == "object" && + (tpe.title == "Query" || tpe.title == "Mutation") && + tpe.properties[fnName] != undefined, + ) + .shift() as ObjectNode; if (!match) { throw new Error( @@ -253,13 +258,15 @@ export class QueryEngine { return { type: "array", items: toJSONSchema(v.type) }; } const name = v.name.value; - const schema = ({ - "Integer": { type: "number" }, - "Float": { type: "number" }, - "Boolean": { type: "boolean" }, - "String": { type: "string" }, - "ID": { type: "string" }, - } as any)?.[name]; + const schema = ( + { + Integer: { type: "number" }, + Float: { type: "number" }, + Boolean: { type: "boolean" }, + String: { type: "string" }, + ID: { type: "string" }, + } as any + )?.[name]; if (schema) { return schema; } @@ -283,10 +290,6 @@ export class QueryEngine { } } - async terminate() { - return await this.tg.deinit(); - } - async materialize( stages: ComputeStage[], verbose: boolean, @@ -297,7 +300,7 @@ export class QueryEngine { while (waitlist.length > 0) { const stage = waitlist.shift()!; stagesMat.push( - ...await stage.props.runtime.materialize(stage, waitlist, verbose), + ...(await stage.props.runtime.materialize(stage, waitlist, verbose)), ); } diff --git a/typegate/src/log.ts b/typegate/src/log.ts index 5a02bda066..f1979abd62 100644 --- a/typegate/src/log.ts +++ b/typegate/src/log.ts @@ -1,7 +1,7 @@ // Copyright Metatype OÜ, licensed under the Elastic License 2.0. // SPDX-License-Identifier: Elastic-2.0 -import { handlers, LevelName, Logger } from "std/log/mod.ts"; +import { ConsoleHandler, LevelName, Logger } from "std/log/mod.ts"; import { basename, dirname } from "std/url/mod.ts"; import { extname } from "std/path/mod.ts"; import { z } from "zod"; @@ -11,9 +11,9 @@ export const configOrExit = async ( sources: Record[], schema: T, ) => { - const parsing = await z.object(schema).safeParse( - sources.reduce((a, b) => deepMerge(a, b), {}), - ); + const parsing = await z + .object(schema) + .safeParse(sources.reduce((a, b) => deepMerge(a, b), {})); if (!parsing.success) { console.error(parsing.error); @@ -37,35 +37,33 @@ export const zBooleanString = z.preprocess( // Those envs are split from the config as only a subset of them are shared with the workers const schema = { debug: zBooleanString, - log_level: z.enum([ - "NOTSET", - "DEBUG", - "INFO", - "WARNING", - "ERROR", - "CRITICAL", - ]).optional(), + log_level: z + .enum(["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]) + .optional(), rust_log: z.string().optional(), version: z.string(), deno_testing: zBooleanString, }; -export const envSharedWithWorkers = Object.keys(schema).map(( - k, -) => k.toUpperCase()); +export const envSharedWithWorkers = Object.keys(schema).map((k) => + k.toUpperCase() +); -const config = await configOrExit([ - { - sentry_sample_rate: 1, - sentry_traces_sample_rate: 1, - log_level: "INFO", - }, - Object.fromEntries( - envSharedWithWorkers - .map((k) => [k.toLocaleLowerCase(), Deno.env.get(k)]) - .filter(([_, v]) => v !== undefined), - ), -], schema); +const config = await configOrExit( + [ + { + sentry_sample_rate: 1, + sentry_traces_sample_rate: 1, + log_level: "INFO", + }, + Object.fromEntries( + envSharedWithWorkers + .map((k) => [k.toLocaleLowerCase(), Deno.env.get(k)]) + .filter(([_, v]) => v !== undefined), + ), + ], + schema, +); // set rust log level is not explicit set if (!config.rust_log) { @@ -93,21 +91,18 @@ if (!config.rust_log) { } } -const consoleHandler = new handlers.ConsoleHandler( - config.log_level as LevelName, - { - formatter: (log) => { - let msg = log.msg; - for (const arg of log.args) { - msg = msg.replace( - "{}", - typeof arg === "string" ? arg : JSON.stringify(arg), - ); - } - return `${log.datetime.toISOString()} [${log.levelName} ${log.loggerName}] ${msg}`; - }, +const consoleHandler = new ConsoleHandler(config.log_level as LevelName, { + formatter: (log) => { + let msg = log.msg; + for (const arg of log.args) { + msg = msg.replace( + "{}", + typeof arg === "string" ? arg : JSON.stringify(arg), + ); + } + return `${log.datetime.toISOString()} [${log.levelName} ${log.loggerName}] ${msg}`; }, -); +}); const loggers = new Map(); const defaultLogger = new Logger("default", "NOTSET", { diff --git a/typegate/src/runtimes/deno/deno.ts b/typegate/src/runtimes/deno/deno.ts index 8be37dd863..cd312e849e 100644 --- a/typegate/src/runtimes/deno/deno.ts +++ b/typegate/src/runtimes/deno/deno.ts @@ -63,11 +63,9 @@ export class DenoRuntime extends Runtime { const ops = new Map(); const uuid = crypto.randomUUID(); - // (user) tg_root/* - // => (gate) tmp/scripts/{tgname}/deno/* const basePath = path.join( - config.tmp_dir, - "artifacts", // TODO: improve sanitization + typegate.tmpDir, + "artifacts", ); let registryCount = 0; diff --git a/typegate/src/runtimes/http.ts b/typegate/src/runtimes/http.ts index c15985e3da..9cf7a8c2a4 100644 --- a/typegate/src/runtimes/http.ts +++ b/typegate/src/runtimes/http.ts @@ -6,7 +6,7 @@ import { Runtime } from "./Runtime.ts"; import { createUrl } from "../utils.ts"; import { MatOptions, replaceDynamicPathParams } from "./utils/http.ts"; import { Resolver, RuntimeInitParams } from "../types.ts"; -import * as base64 from "std/encoding/base64.ts"; +import { encodeBase64 } from "std/encoding/base64.ts"; import { getLogger } from "../log.ts"; import { Logger } from "std/log/logger.ts"; import { HTTPRuntimeData } from "../typegraph/types.ts"; @@ -68,7 +68,7 @@ export class HTTPRuntime extends Runtime { headers.set( "authorization", `basic ${ - base64.encode( + encodeBase64( secretManager.secretOrFail(args.basic_auth_secret as string), ) }`, @@ -165,7 +165,7 @@ export class HTTPRuntime extends Runtime { ); if (res.status >= 400) { - this.logger.warning( + this.logger.warn( `${pathname} - ${searchParams} - ${body} => ${res.status} : ${ Deno.inspect({ res, options, args, bodyFields, hasBody, method }) }`, diff --git a/typegate/src/runtimes/patterns/messenger/async_messenger.ts b/typegate/src/runtimes/patterns/messenger/async_messenger.ts index e1793ad063..8c7850d3e6 100644 --- a/typegate/src/runtimes/patterns/messenger/async_messenger.ts +++ b/typegate/src/runtimes/patterns/messenger/async_messenger.ts @@ -1,7 +1,6 @@ // Copyright Metatype OÜ, licensed under the Elastic License 2.0. // SPDX-License-Identifier: Elastic-2.0 -import { deferred } from "std/async/deferred.ts"; import { getLogger } from "../../../log.ts"; import { Answer, Message, TaskData } from "./types.ts"; import { maxi32 } from "../../../utils.ts"; @@ -101,13 +100,13 @@ export class AsyncMessenger { pulseCount = 0, ): Promise { const id = this.nextId(); - const promise = deferred(); + const promise = Promise.withResolvers(); this.#tasks.set(id, { promise, hooks }); const message = { id, op, data, remainingPulseCount: pulseCount }; this.#operationQueues[this.#queueIndex].push(message); void this.#send(this.broker, message); - return promise; + return promise.promise; } async receive(answer: Answer): Promise { diff --git a/typegate/src/runtimes/patterns/messenger/types.ts b/typegate/src/runtimes/patterns/messenger/types.ts index 2e12807021..fd75e012b8 100644 --- a/typegate/src/runtimes/patterns/messenger/types.ts +++ b/typegate/src/runtimes/patterns/messenger/types.ts @@ -1,8 +1,6 @@ // Copyright Metatype OÜ, licensed under the Elastic License 2.0. // SPDX-License-Identifier: Elastic-2.0 -import { Deferred } from "std/async/deferred.ts"; - export interface Message { id: number; op: string | number | null; @@ -10,11 +8,13 @@ export interface Message { remainingPulseCount?: number; } +type PromiseWithResolvers = ReturnType>; + export type Answer = | ({ id: number; data: T; error?: never }) | ({ id: number; data?: never; error: string }); export interface TaskData { - promise: Deferred; + promise: PromiseWithResolvers; hooks: Array<() => void | Promise>; } diff --git a/typegate/src/runtimes/typegate.ts b/typegate/src/runtimes/typegate.ts index 2cfcc42546..23be1e9190 100644 --- a/typegate/src/runtimes/typegate.ts +++ b/typegate/src/runtimes/typegate.ts @@ -177,7 +177,10 @@ export class TypeGateRuntime extends Runtime { addTypegraph: Resolver = async ({ fromString, secrets, targetVersion }) => { logger.info("Adding typegraph"); if ( - !semver.gte(semver.parse(targetVersion), semver.parse(config.version)) + !semver.greaterOrEqual( + semver.parse(targetVersion), + semver.parse(config.version), + ) ) { throw new Error( `Typegraph SDK version ${targetVersion} must be greater than typegate version ${config.version} (until the releases are stable)`, @@ -206,7 +209,7 @@ export class TypeGateRuntime extends Runtime { if (SystemTypegraph.check(name)) { throw new Error(`Typegraph ${name} cannot be removed`); } - await this.typegate.register.remove(name); + await this.typegate.removeTypegraph(name); } return true; }; diff --git a/typegate/src/services/artifact_service.ts b/typegate/src/services/artifact_service.ts index 7ee39cc637..c22a735355 100644 --- a/typegate/src/services/artifact_service.ts +++ b/typegate/src/services/artifact_service.ts @@ -109,9 +109,9 @@ export class ArtifactService { } // TODO key? - const hash = await this.store.persist(stream); + const hash = await this.store.persistence.save(stream); if (hash !== meta.hash) { - await this.store.delete(hash); + await this.store.persistence.delete(hash); return new Response(JSON.stringify({ error: "hash mismatch" }), { status: 403, headers: { "Content-Type": "application/json" }, diff --git a/typegate/src/services/auth/mod.ts b/typegate/src/services/auth/mod.ts index d7675213e7..7601af77ed 100644 --- a/typegate/src/services/auth/mod.ts +++ b/typegate/src/services/auth/mod.ts @@ -84,7 +84,7 @@ export async function ensureJWT( auth = engine.tg.auths.get(provider as string) ?? null; } } catch (e) { - logger.warning(`malformed jwt: ${e}`); + logger.warn(`malformed jwt: ${e}`); } } diff --git a/typegate/src/services/auth/protocols/jwt.ts b/typegate/src/services/auth/protocols/jwt.ts index 6e7ac98f9e..2f4b0525d5 100644 --- a/typegate/src/services/auth/protocols/jwt.ts +++ b/typegate/src/services/auth/protocols/jwt.ts @@ -64,7 +64,7 @@ export class JWTAuth extends Protocol { if (e.message.includes("jwt is used too early")) { throw new Error("jwt used too early"); } - logger.warning(`jwt auth failed: ${e}`); + logger.warn(`jwt auth failed: ${e}`); throw new Error("jwt is invalid"); } } diff --git a/typegate/src/services/auth/protocols/oauth2.ts b/typegate/src/services/auth/protocols/oauth2.ts index df626ba982..4f80da5017 100644 --- a/typegate/src/services/auth/protocols/oauth2.ts +++ b/typegate/src/services/auth/protocols/oauth2.ts @@ -178,7 +178,7 @@ export class OAuth2Auth extends Protocol { headers, }); } catch (e) { - logger.warning(e); + logger.warn(e); const headers = clearCookie( url.hostname, this.typegraphName, diff --git a/typegate/src/services/middlewares.ts b/typegate/src/services/middlewares.ts index adad5c300b..11837a15e6 100644 --- a/typegate/src/services/middlewares.ts +++ b/typegate/src/services/middlewares.ts @@ -32,7 +32,7 @@ export function resolveIdentifier( if (typeof contextId === "string") { return contextId; } - logger.warning( + logger.warn( `invalid context identifier type at ${engine.tg.tg.meta.rate?.context_identifier}, only string is supported, got: ${contextId}`, ); } diff --git a/typegate/src/sync/replicated_map.ts b/typegate/src/sync/replicated_map.ts index 8d6763f6d9..826d7583f7 100644 --- a/typegate/src/sync/replicated_map.ts +++ b/typegate/src/sync/replicated_map.ts @@ -38,7 +38,7 @@ type RedisReplicatedMapOptions = { terminate: TerminateHook; }; -export class RedisReplicatedMap { +export class RedisReplicatedMap implements AsyncDisposable { private instance: string; public memory: Map; @@ -47,21 +47,6 @@ export class RedisReplicatedMap { sync: SyncContext | null; - private constructor( - name: string, - private redis: Redis, - private redisObs: Redis, - private serializer: Serializer, - private deserializer: Deserializer, - private terminateHook: TerminateHook, - ) { - this.instance = crypto.randomUUID(); - this.memory = new Map(); - this.key = name; - this.ekey = `${name}_event`; - this.sync = null; - } - static async init( name: string, connection: RedisConnectOptions, @@ -83,6 +68,31 @@ export class RedisReplicatedMap { ); } + private constructor( + name: string, + private redis: Redis, + private redisObs: Redis, + private serializer: Serializer, + private deserializer: Deserializer, + private terminateHook: TerminateHook, + ) { + this.instance = crypto.randomUUID(); + this.memory = new Map(); + this.key = name; + this.ekey = `${name}_event`; + this.sync = null; + } + + async [Symbol.asyncDispose]() { + if (this.sync) { + await this.sync.cancel(); + this.sync = null; + } + + this.redis.close(); + this.redisObs.close(); + } + async historySync(): Promise { const { key, redis, deserializer } = this; @@ -152,16 +162,6 @@ export class RedisReplicatedMap { } } - async stopSync() { - if (this.sync) { - await this.sync.cancel(); - this.sync = null; - } - - this.redis.close(); - this.redisObs.close(); - } - private subscribe(): SyncContext { const { ekey, redisObs } = this; diff --git a/typegate/src/typegate/artifacts/local.ts b/typegate/src/typegate/artifacts/local.ts index 9dc1b10235..3112c6ba76 100644 --- a/typegate/src/typegate/artifacts/local.ts +++ b/typegate/src/typegate/artifacts/local.ts @@ -5,14 +5,17 @@ import { resolve } from "std/path/resolve.ts"; import { HashTransformStream } from "../../utils/hash.ts"; import { ArtifactMeta, + ArtifactPersistence, ArtifactStore, - getLocalPath, - STORE_DIR, - STORE_TEMP_DIR, + Dirs, + RefCounter, + UploadEndpointManager, } from "./mod.ts"; import { getLogger } from "../../log.ts"; import { createHash } from "node:crypto"; import * as jwt from "jwt"; +import { join } from "std/path/join.ts"; +import { exists } from "std/fs/exists.ts"; const logger = getLogger(import.meta); @@ -22,51 +25,29 @@ export interface UploadUrlStore { expirationTimerId: number; } -/** - * Initialize Upload URLs store. The URL store holds the upload URLs used for persisiting artifacts. Each URL is mapped to the Artifact Meta to be uploaded. The store uses an Expiration Queue to keep track of expiration status of URLs and discards the URLs upon expiration. - * @returns URL to Artifact Meta map, URL expiration queue and expiration timer Id - */ -function initUploadUrlStore() { - // stores one to one mapping between upload URLs and - const mapToMeta = new Map(); - const expirationQueue: [string, number][] = []; - const expirationTimerId = setInterval(() => { - const now = jwt.getNumericDate(new Date()); - while (expirationQueue.length > 0) { - const [url, expirationTime] = expirationQueue[0]; - if (expirationTime > now) { - break; - } - expirationQueue.shift(); - mapToMeta.delete(url); - } - }, 5000); - return { mapToMeta, expirationQueue, expirationTimerId }; -} - -function deinitUploadUrlStore(uploadUrls: UploadUrlStore) { - clearInterval(uploadUrls.expirationTimerId); - uploadUrls.mapToMeta.clear(); - uploadUrls.expirationQueue = []; -} - -export class LocalArtifactStore extends ArtifactStore { - #uploadUrls: UploadUrlStore; - - static async init() { - await Deno.mkdir(STORE_DIR, { recursive: true }); - await Deno.mkdir(STORE_TEMP_DIR, { recursive: true }); - - return new LocalArtifactStore(); +export class LocalArtifactPersistence implements ArtifactPersistence { + static async init(baseDir: string) { + const cacheDir = join(baseDir, "artifacts-cache"); + const tempDir = join(cacheDir, "tmp"); + const artifactsDir = join(baseDir, "artifacts"); + await Deno.mkdir(tempDir, { recursive: true }); + await Deno.mkdir(artifactsDir, { recursive: true }); + return new LocalArtifactPersistence({ + cache: cacheDir, + temp: tempDir, + artifacts: artifactsDir, + }); } - constructor() { - super(); - this.#uploadUrls = initUploadUrlStore(); + constructor(public dirs: Dirs) {} + + async [Symbol.asyncDispose]() { + await Deno.remove(this.dirs.cache, { recursive: true }); + await Deno.remove(this.dirs.artifacts, { recursive: true }); } - override async persist(stream: ReadableStream): Promise { - const tmpFile = await Deno.makeTempFile({ dir: STORE_TEMP_DIR }); + async save(stream: ReadableStream): Promise { + const tmpFile = await Deno.makeTempFile({ dir: this.dirs.temp }); const file = await Deno.open(tmpFile, { write: true, truncate: true }); const hasher = createHash("sha256"); await stream @@ -74,79 +55,163 @@ export class LocalArtifactStore extends ArtifactStore { .pipeTo(file.writable); const hash = hasher.digest("hex"); - const targetFile = resolve(STORE_DIR, hash); - logger.debug(`persisting artifact {}`, { hash }); + const targetFile = this.resolveCache(hash); + logger.info(`persisting artifact to ${targetFile}`); await Deno.rename(tmpFile, targetFile); return hash; } - override async delete(hash: string) { - logger.debug(`deleting artifact {}`, { hash }); - await Deno.remove(resolve(STORE_DIR, hash)); + async delete(hash: string) { + // TODO track and remove localPaths?? + logger.info(`deleting artifact ${hash}`); + await Deno.remove(this.resolveCache(hash)); } - override async has(hash: string) { - try { - await Deno.stat(resolve(STORE_DIR, hash)); - return true; - } catch { - return false; - } + has(hash: string) { + return exists(this.resolveCache(hash)); } - override async getLocalPath(meta: ArtifactMeta, deps: ArtifactMeta[] = []) { - for (const dep of deps) { - await this.#assertArtifactExist(dep.hash, dep.typegraphName); - await getLocalPath(dep, meta); + async fetch(hash: string) { + const cache = this.resolveCache(hash); + if (await exists(cache)) { + return cache; + } else { + throw new Error(`Artifact '${hash}' not found at ${cache}`); } + } - await this.#assertArtifactExist(meta.hash, meta.typegraphName); - return await getLocalPath(meta, meta); + resolveCache(hash: string) { + return resolve(this.dirs.cache, hash); + } +} + +class LocalUploadEndpointManager implements UploadEndpointManager { + #mapToMeta: Map; + #expirationQueue: [string, number][]; + #expirationTimerId: number; + + constructor(private expireSec = 5 * 60) { + this.#mapToMeta = new Map(); + this.#expirationQueue = []; + + // Clean up expired upload URLs every 5 seconds + this.#expirationTimerId = setInterval(() => { + const now = jwt.getNumericDate(new Date()); + while (this.#expirationQueue.length > 0) { + const [url, expirationTime] = this.#expirationQueue[0]; + if (expirationTime > now) { + break; + } + this.#expirationQueue.shift(); + this.#mapToMeta.delete(url); + } + }, 5000); } - override async prepareUpload(meta: ArtifactMeta, origin: URL) { - // should not be uploaded again - if (await this.has(ArtifactStore.getArtifactKey(meta))) { - logger.debug( - "artifact already exists, skipping upload preparation {}", - { meta }, - ); + async [Symbol.asyncDispose]() { + clearInterval(this.#expirationTimerId); + await Promise.resolve(void null); + } + + async prepareUpload( + meta: ArtifactMeta, + origin: URL, + persistence: ArtifactPersistence, + ) { + if (await persistence.has(meta.hash)) { return null; } - - const [url, expirationTime] = await ArtifactStore.createUploadUrl( + const url = await ArtifactStore.createUploadUrl( origin, meta.typegraphName, + this.expireSec, ); - this.#uploadUrls.mapToMeta.set(url, meta); - this.#uploadUrls.expirationQueue.push([url, expirationTime]); + const token = url.searchParams.get("token"); + if (!token) { + throw new Error("Invalid upload URL generated"); + } + this.#mapToMeta.set(token, meta); + this.#expirationQueue.push([token, jwt.getNumericDate(this.expireSec)]); - return url; + return url.toString(); } - override takeUploadUrl(url: URL): Promise { - ArtifactStore.validateUploadUrl(url); + async takeUploadUrl(url: URL) { + const token = await ArtifactStore.validateUploadUrl(url); - const meta = this.#uploadUrls.mapToMeta.get(url.toString()); + const meta = this.#mapToMeta.get(token); if (!meta) { throw new Error("Invalid upload URL"); } - this.#uploadUrls.mapToMeta.delete(url.toString()); + this.#mapToMeta.delete(token); return Promise.resolve(meta); } +} + +class InMemoryRefCounter implements RefCounter { + #refCounts: Map = new Map(); + #byRefCounts: Map> = new Map(); + + async [Symbol.asyncDispose]() { + await Promise.resolve(void null); + } + + async increment(key: string) { + this.#updateRefCount(key, (count) => count + 1); + await Promise.resolve(void null); + } + + async decrement(key: string) { + this.#updateRefCount(key, (count) => (count ? count - 1 : 0)); + await Promise.resolve(void null); + } + + async resetAll() { + this.#refCounts.clear(); + this.#byRefCounts.clear(); + await Promise.resolve(void null); + } - async #assertArtifactExist(key: string, tgName: string) { - if (!(await this.has(key))) { - throw new Error( - `Artifact with key '${key}' is not available for typegraph '${tgName}'`, - ); + takeGarbage(): Promise { + const garbage = [...(this.#byRefCounts.get(0) ?? [])]; + for (const key of garbage) { + this.#refCounts.delete(key); } + this.#byRefCounts.delete(0); + return Promise.resolve(garbage); } - override close() { - deinitUploadUrlStore(this.#uploadUrls); + #updateRefCount(key: string, update: (count: number) => number) { + const oldCount = this.#refCounts.get(key) ?? 0; + const newCount = update(oldCount); + if (oldCount === newCount) return; + + this.#refCounts.set(key, newCount); + if (oldCount > 0 && !this.#byRefCounts.get(oldCount)?.delete(key)) { + throw new Error("RefCountStore: inconsistent state"); + } + + let set = this.#byRefCounts.get(newCount); + if (!set) { + set = new Set(); + this.#byRefCounts.set(newCount, set); + } + set.add(key); + } + + inspect(label: string) { + console.log("refCounts (local)", label, this.#refCounts); return Promise.resolve(void null); } } + +export async function createLocalArtifactStore( + baseDir: string, +): Promise { + const persistence = await LocalArtifactPersistence.init(baseDir); + const uploadEndpoints = new LocalUploadEndpointManager(); + const refCounter = new InMemoryRefCounter(); + return ArtifactStore.init(persistence, uploadEndpoints, refCounter); +} diff --git a/typegate/src/typegate/artifacts/mod.ts b/typegate/src/typegate/artifacts/mod.ts index 0a1ed465da..b4ab7fb6a6 100644 --- a/typegate/src/typegate/artifacts/mod.ts +++ b/typegate/src/typegate/artifacts/mod.ts @@ -1,46 +1,63 @@ // Copyright Metatype OÜ, licensed under the Elastic License 2.0. // SPDX-License-Identifier: Elastic-2.0 -import { signJWT, verifyJWT } from "../../crypto.ts"; +import { sha256, signJWT, verifyJWT } from "@typegate/crypto.ts"; +import { getLogger } from "@typegate/log.ts"; import * as jwt from "jwt"; import { z } from "zod"; -import config from "../../config.ts"; import { dirname } from "std/path/dirname.ts"; import { resolve } from "std/path/resolve.ts"; +import { exists } from "std/fs/exists.ts"; +// until deno supports it... +import { AsyncDisposableStack } from "dispose"; -// The directory where artifacts are stored -- by hash -export const STORE_DIR = `${config.tmp_dir}/artifacts-cache`; -export const STORE_TEMP_DIR = `${config.tmp_dir}/artifacts-cache/tmp`; -const ARTIFACTS_DIR = `${config.tmp_dir}/artifacts`; +const logger = getLogger(import.meta); + +export interface Dirs { + cache: string; + temp: string; + artifacts: string; +} function getUploadPath(tgName: string) { return `/${tgName}/artifacts`; } +async function getLocalParentDir( + entrypoint: ArtifactMeta, + deps: ArtifactMeta[], +) { + const uniqueStr = deps + .sort((a, b) => a.relativePath.localeCompare(b.relativePath)) + .reduce( + (acc, dep) => `${acc};${dep.relativePath}.${dep.hash}`, + `${entrypoint.relativePath}.${entrypoint.hash}`, + ); + + return await sha256(uniqueStr); +} + export async function getLocalPath( meta: ArtifactMeta, - mainModuleMeta: ArtifactMeta, + parentDirName: string, + dirs: Dirs, ) { - const cachedPath = resolve(STORE_DIR, meta.hash); + const cachedPath = resolve(dirs.cache, meta.hash); const localPath = resolve( - ARTIFACTS_DIR, - mainModuleMeta.hash, + dirs.artifacts, + parentDirName, meta.typegraphName, meta.relativePath, ); - // TODO: what happens when symlink already exists? or when same local path artifacts with different cachedPath - try { - const fileInfo = await Deno.lstat(localPath); - if (fileInfo.isFile || fileInfo.isSymlink) { - await Deno.remove(localPath); - } - console.log(`Removed existing link on: ${localPath}`); - } catch { - // link doesn't exist before + if (await exists(localPath)) { + // always assume same versions of the arifacts - no need to check hash. + return localPath; } await Deno.mkdir(dirname(localPath), { recursive: true }); + // the old artifacts are always removed on typegraph update. + await Deno.remove(localPath, { recursive: true }).catch(() => {}); await Deno.link(cachedPath, localPath); return localPath; @@ -55,60 +72,122 @@ export const artifactMetaSchema = z.object({ export type ArtifactMeta = z.infer; -export abstract class ArtifactStore { - /** - * Persist an artifact to the store. - * @param stream The artifact content. - * @returns The hash of the artifact. - */ - abstract persist(stream: ReadableStream): Promise; +export interface ArtifactPersistence extends AsyncDisposable { + dirs: Dirs; + save(stream: ReadableStream): Promise; + delete(hash: string): Promise; + has(hash: string): Promise; + /** Fetch the artifact to local file system and returns the path */ + fetch(hash: string): Promise; +} - /** - * Delete an artifact from the store. - * @param hash The hash of the artifact. - * @throws If the artifact does not exist. - */ - abstract delete(hash: string): Promise; +export interface UploadEndpointManager extends AsyncDisposable { + prepareUpload( + meta: ArtifactMeta, + origin: URL, + persistence: ArtifactPersistence, + ): Promise; + takeUploadUrl(url: URL): Promise; +} - /** - * Check if the artifact is available in the store. - * @param hash The hash of the artifact. - * @returns Whether the artifact is available. - */ - abstract has(hash: string): Promise; +export class ArtifactStore implements AsyncDisposable { + #disposed = false; + + static async init( + persistence: ArtifactPersistence, + uploadEndpoints: UploadEndpointManager, + refCounter: RefCounter, + ) { + await using stack = new AsyncDisposableStack(); + stack.use(persistence); + stack.use(uploadEndpoints); + stack.use(refCounter); + return await Promise.resolve( + new ArtifactStore( + persistence, + uploadEndpoints, + refCounter, + stack.move(), + ), + ); + } - /** - * Ensure that the artifact is available locally (in the file system). - * @param meta The artifact metadata. - * @param deps The dependencies of the artifact. - * @returns The local path to the artifact. - * @throws If the artifact is not available (not persisted on the store). - */ - abstract getLocalPath( - meta: ArtifactMeta, - deps?: ArtifactMeta[], - ): Promise; + constructor( + public persistence: ArtifactPersistence, + private uploadEndpoints: UploadEndpointManager, + private refCounter: RefCounter, + private disposables: AsyncDisposableStack, + ) { + } - /** - * Create a new upload URL for the given artifact. - * @param meta The artifact metadata. - * @param origin The origin of the request. - * @returns The URL to upload the artifact to, or null if the artifact is already uploaded. - */ - abstract prepareUpload( + async [Symbol.asyncDispose]() { + if (this.#disposed) return; + this.#disposed = true; + await this.disposables[Symbol.asyncDispose](); + } + + async updateRefCounts( + added: Set, + removed: Set, + ): Promise { + const increments: string[] = []; + const decrements: string[] = []; + + for (const hash of added) { + if (!removed.has(hash)) { + increments.push(hash); + } else { + removed.delete(hash); + } + } + decrements.push(...removed); + + await Promise.all([ + ...increments.map((hash) => this.refCounter.increment(hash)), + ...decrements.map((hash) => this.refCounter.decrement(hash)), + ]); + } + + async runArtifactGC(full = false) { + logger.info("Running artifact GC"); + if (full) { + throw new Error("Not implemented"); + } + const garbage = await this.refCounter.takeGarbage(); + logger.info(`Found ${garbage.length} garbage artifacts: ${garbage}`); + + const res = await Promise.allSettled( + garbage.map((hash) => this.persistence.delete(hash)), + ); + + res.forEach((r, i) => { + if (r.status === "rejected") { + logger.error("Error when deleting artifact", garbage[i], r.reason); + } + }); + } + + async getLocalPath( meta: ArtifactMeta, - origin: URL, - ): Promise; + deps: ArtifactMeta[] = [], + ): Promise { + const parentDirName = await getLocalParentDir(meta, deps); + for (const dep of deps) { + await this.persistence.fetch(dep.hash); + await getLocalPath(dep, parentDirName, this.persistence.dirs); + } - /** - * Remove the given upload URL from the store. - * @param url The URL to remove. - * @returns The artifact metadata. - * @throws If the URL is invalid or expired. - */ - abstract takeUploadUrl(url: URL): Promise; + await this.persistence.fetch(meta.hash); + return getLocalPath(meta, parentDirName, this.persistence.dirs); + } - abstract close(): Promise; + prepareUpload(meta: ArtifactMeta, origin: URL) { + return this.uploadEndpoints.prepareUpload(meta, origin, this.persistence); + } + + takeUploadUrl(url: URL) { + return this.uploadEndpoints.takeUploadUrl(url); + } /** unique identifier for an artifact (file content) */ static getArtifactKey(meta: ArtifactMeta) { @@ -124,13 +203,13 @@ export abstract class ArtifactStore { static async createUploadUrl( origin: URL, tgName: string, - ): Promise<[string, number]> { - const expiresIn = 5 * 60; + expireSec: number, + ): Promise { const uuid = crypto.randomUUID(); - const token = await signJWT({ uuid }, expiresIn); - const url = new URL(`${getUploadPath(tgName)}`, origin); + const token = await signJWT({ uuid, expiresIn: expireSec }, expireSec); + const url = new URL(getUploadPath(tgName), origin); url.searchParams.set("token", token); - return [url.toString(), jwt.getNumericDate(expiresIn)]; + return url; } static async validateUploadUrl(url: URL) { @@ -143,5 +222,17 @@ export abstract class ArtifactStore { if ((context.exp as number) < jwt.getNumericDate(new Date())) { throw new Error("Expired upload URL"); } + + return token; } } + +export interface RefCounter extends AsyncDisposable { + increment(key: string): Promise; + decrement(key: string): Promise; + resetAll(): Promise; + takeGarbage(): Promise>; + + // for debugging purpose; output the current state of the ref counter + inspect(label: string): Promise; +} diff --git a/typegate/src/typegate/artifacts/shared.ts b/typegate/src/typegate/artifacts/shared.ts index dde753892e..a1fff7170f 100644 --- a/typegate/src/typegate/artifacts/shared.ts +++ b/typegate/src/typegate/artifacts/shared.ts @@ -2,49 +2,30 @@ // SPDX-License-Identifier: Elastic-2.0 import { connect, Redis, RedisConnectOptions } from "redis"; +import { getLogger } from "@typegate/log.ts"; import { createHash } from "node:crypto"; import { S3 } from "aws-sdk/client-s3"; -import { getLocalPath, STORE_DIR, STORE_TEMP_DIR } from "./mod.ts"; +import { + ArtifactPersistence, + RefCounter, + UploadEndpointManager, +} from "./mod.ts"; import { ArtifactMeta, ArtifactStore } from "./mod.ts"; import { HashTransformStream } from "../../utils/hash.ts"; -import { resolve } from "std/path/resolve.ts"; import { SyncConfig } from "../../sync/config.ts"; -import { readAll } from "https://deno.land/std@0.129.0/streams/conversion.ts"; -import config from "../../config.ts"; +import { LocalArtifactPersistence } from "./local.ts"; +import { exists } from "std/fs/exists.ts"; +import { dirname } from "std/path/mod.ts"; +import { chunk } from "std/collections/chunk.ts"; + +const logger = getLogger(import.meta); export interface RemoteUploadUrlStore { redisClient: Redis; } -const setCmd = ` -local key = KEYS[1] -local value = ARGV[1] -local expirationTime = ARGV[2] - -redis.call('HSET', key, 'url', value) -redis.call('EXPIRE', key, expirationTime) -`.trim(); -const existsCmd = ` -local key = KEYS[1] - -local exists = redis.call('EXISTS', key) -return exists -`.trim(); - -function resolveRedisUrlKey(url: string) { - return `articact-upload-urls:${url}`; -} - -async function initRemoteUploadUrlStore( - redisConfig: RedisConnectOptions, -): Promise { - const redisClient = await connect(redisConfig); - - return { redisClient }; -} - -async function deinitRemoteUploadUrlStore(urlStore: RemoteUploadUrlStore) { - await urlStore.redisClient.quit(); +function getRedisUploadUrlKey(token: string) { + return `typegate:artifacts:upload-urls:${token}`; } function serializeToRedisValue(value: T): string { @@ -55,73 +36,96 @@ function deserializeToCustom(value: string): T { return JSON.parse(value) as T; } -function resolveS3Key(hash: string) { +export function resolveS3Key(hash: string) { return `${REMOTE_ARTIFACT_DIR}/${hash}`; } const REMOTE_ARTIFACT_DIR = "artifacts-cache"; -export class SharedArtifactStore extends ArtifactStore { - #uploadUrls: RemoteUploadUrlStore; - #s3!: S3; - #syncConfig: SyncConfig; +class SharedArtifactPersistence implements ArtifactPersistence { + static async init( + baseDir: string, + syncConfig: SyncConfig, + ): Promise { + const localShadow = await LocalArtifactPersistence.init(baseDir); + const s3 = new S3(syncConfig.s3); + return new SharedArtifactPersistence(localShadow, s3, syncConfig.s3Bucket); + } - static async init(syncConfig: SyncConfig) { - const urlStore = await initRemoteUploadUrlStore(syncConfig.redis); - await Deno.mkdir(STORE_DIR, { recursive: true }); - await Deno.mkdir(STORE_TEMP_DIR, { recursive: true }); + constructor( + private localShadow: LocalArtifactPersistence, + private s3: S3, + private s3Bucket: string, + ) {} - return new SharedArtifactStore(syncConfig, urlStore); + get dirs() { + return this.localShadow.dirs; } - constructor(syncConfig: SyncConfig, urlStore: RemoteUploadUrlStore) { - super(); - this.#s3 = new S3(syncConfig.s3); - this.#syncConfig = syncConfig; - this.#uploadUrls = urlStore; + async [Symbol.asyncDispose]() { + await this.localShadow[Symbol.asyncDispose](); + this.s3.destroy(); } - override async persist(stream: ReadableStream): Promise { - const tmpFile = await Deno.makeTempFile({ - dir: STORE_TEMP_DIR, - }); - const file = await Deno.open(tmpFile, { - write: true, - truncate: true, - }); - + async save(stream: ReadableStream): Promise { const hasher = createHash("sha256"); - await stream - .pipeThrough(new HashTransformStream(hasher)) - .pipeTo(file.writable); - const hash = hasher.digest("hex"); + // TODO compatibility with Node.js streams? + // const stream2 = stream.pipeThrough(new HashTransformStream(hasher)); + // + // const tempKey = resolveS3Key( + // `tmp/${Math.random().toString(36).substring(2)}`, + // ); + // + // const _ = await this.s3.putObject({ + // Bucket: this.s3Bucket, + // Body: stream2, + // Key: tempKey, + // }); + // const hash = hasher.digest("hex"); + // + // await this.s3.copyObject({ + // Bucket: this.s3Bucket, + // CopySource: tempKey, + // Key: resolveS3Key(hash), + // }); + // + // await this.s3.deleteObject({ + // Bucket: this.s3Bucket, + // Key: tempKey, + // }); + // + // return hash; + + const tmpFile = await Deno.makeTempFile({ dir: this.dirs.temp }); + const file = await Deno.open(tmpFile, { write: true, truncate: true }); + await stream.pipeThrough(new HashTransformStream(hasher)).pipeTo( + file.writable, + ); - const readFile = await Deno.open(tmpFile, { read: true }); - // Read file content into a Uint8Array - const fileContent = await readAll(readFile); - console.log(`Persisting artifact to S3`); - const _ = await this.#s3.putObject({ - Bucket: this.#syncConfig.s3Bucket, - Body: fileContent, + const hash = hasher.digest("hex"); + const body = await Deno.readFile(tmpFile); + logger.info(`persisting artifact to S3: ${hash}`); + const _ = await this.s3.putObject({ + Bucket: this.s3Bucket, + Body: body, Key: resolveS3Key(hash), }); - readFile.close(); return hash; } - override async delete(hash: string): Promise { - const _ = await this.#s3.deleteObject({ - Bucket: this.#syncConfig.s3Bucket, + async delete(hash: string): Promise { + await this.s3.deleteObject({ + Bucket: this.s3Bucket, Key: resolveS3Key(hash), }); } - override async has(hash: string): Promise { + async has(hash: string): Promise { try { - const _ = await this.#s3.headObject({ - Bucket: this.#syncConfig.s3Bucket, + const _ = await this.s3.headObject({ + Bucket: this.s3Bucket, Key: resolveS3Key(hash), }); return true; @@ -130,126 +134,163 @@ export class SharedArtifactStore extends ArtifactStore { } } - override async getLocalPath( - meta: ArtifactMeta, - deps?: ArtifactMeta[] | undefined, - ): Promise { - for (const dep of deps ?? []) { - await this.#downloadFromRemote(dep.hash, dep.relativePath); - await getLocalPath(dep, meta); - } + async fetch(hash: string): Promise { + const targetFile = this.localShadow.resolveCache(hash); - await this.#downloadFromRemote(meta.hash, meta.relativePath); - return await getLocalPath(meta, meta); - } - - async #downloadFromRemote(hash: string, relativePath: string) { - if (await this.#existsLocally(hash)) { - return; + if (await exists(targetFile)) { + return targetFile; } - const targetFile = resolve(STORE_DIR, hash); - const response = await this.#s3.getObject({ - Bucket: this.#syncConfig.s3Bucket, + + const response = await this.s3.getObject({ + Bucket: this.s3Bucket, Key: resolveS3Key(hash), }); if (response.$metadata.httpStatusCode === 404) { - throw new Error( - `Artifact ${relativePath} with hash ${relativePath} not found`, - ); + throw new Error(`Artifact '${hash}' not found`); } if (response.Body) { - const file = - (await Deno.open(targetFile, { write: true, create: true })).writable; + await Deno.mkdir(dirname(targetFile), { recursive: true }); + const file = (await Deno.open(targetFile, { write: true, create: true })) + .writable; await response.Body.transformToWebStream().pipeTo(file); } else { - throw new Error(`Failed to download artifact ${relativePath} from s3`); + throw new Error(`Failed to download artifact with hash ${hash} from s3`); } + + return this.localShadow.fetch(hash); } +} - async #existsLocally(hash: string) { - try { - await Deno.stat(resolve(STORE_DIR, hash)); - return true; - } catch { - return false; - } +class SharedUploadEndpointManager implements UploadEndpointManager { + static async init(syncConfig: SyncConfig, expireSec = 5 * 60) { + const redis = await connect(syncConfig.redis); + return new SharedUploadEndpointManager(redis, expireSec); } - override async prepareUpload( + private constructor(private redis: Redis, private expireSec: number) {} + + async [Symbol.asyncDispose]() { + await this.redis.quit(); + } + + async prepareUpload( meta: ArtifactMeta, origin: URL, + persistence: ArtifactPersistence, ): Promise { // should not be uploaded again - if (await this.has(ArtifactStore.getArtifactKey(meta))) { + if (await persistence.has(meta.hash)) { return null; } - const [url, _] = await ArtifactStore.createUploadUrl( + + const url = await ArtifactStore.createUploadUrl( origin, meta.typegraphName, + this.expireSec, ); - await this.#addUrlToRedis( - url, - serializeToRedisValue(meta), - config.redis_url_queue_expire_sec, + const token = url.searchParams.get("token"); + if (!token) { + throw new Error("Invalid upload URL generated"); + } + const _ = await this.redis.eval( + /* lua */ ` + redis.call('SET', KEYS[1], ARGV[1]) + redis.call('EXPIRE', KEYS[1], ARGV[2]) + `, + [getRedisUploadUrlKey(token)], + [serializeToRedisValue(meta), this.expireSec], ); - return url; + return url.toString(); } - override async takeUploadUrl( - url: URL, - ): Promise< - ArtifactMeta - > { - ArtifactStore.validateUploadUrl(url); - - const meta = await this.#getUrlFromRedis(url.toString()); - if (!meta) { - throw new Error("Invalid upload URL"); - } + async takeUploadUrl(url: URL): Promise { + const token = await ArtifactStore.validateUploadUrl(url); - await this.#removeFromRedis(url.toString()); + const meta = await this.redis.eval( + /* lua */ ` + local meta = redis.call('GET', KEYS[1]) + redis.call('DEL', KEYS[1]) + return meta + `, + [getRedisUploadUrlKey(token)], + [], + ); return Promise.resolve(deserializeToCustom(meta as string)); } +} - async #addUrlToRedis(url: string, value: string, expirationDuration: number) { - const _ = await this.#uploadUrls.redisClient.eval( - setCmd, - [resolveRedisUrlKey(url)], - [value, expirationDuration], - ); +export const REDIS_REF_COUNTER = "typegate:artifacts:refcounts"; + +export class SharedArtifactRefCounter implements RefCounter { + static async init( + redisConfig: RedisConnectOptions, + ): Promise { + return new SharedArtifactRefCounter(await connect(redisConfig)); } - async #getUrlFromRedis(url: string) { - return await this.#uploadUrls.redisClient.eval( - "return redis.call('HGET', KEYS[1], ARGV[1])", - [resolveRedisUrlKey(url)], - ["url"], - ); + #redisClient: Redis; + + private constructor(redisClient: Redis) { + this.#redisClient = redisClient; } - async #existsInRedis(url: string) { - return Boolean( - await this.#uploadUrls.redisClient.eval( - existsCmd, - [resolveRedisUrlKey(url)], - [], - ), - ); + async [Symbol.asyncDispose]() { + await this.#redisClient.quit(); + } + + async increment(key: string) { + await this.#redisClient.zincrby(REDIS_REF_COUNTER, 1, key); + } + + async decrement(key: string) { + // TODO what for negative values? + await this.#redisClient.zincrby(REDIS_REF_COUNTER, -1, key); + } + + async resetAll() { + await this.#redisClient.del(REDIS_REF_COUNTER); } - async #removeFromRedis(url: string) { - const _ = await this.#uploadUrls.redisClient.eval( - "redis.call('DEL', KEYS[1])", - [resolveRedisUrlKey(url)], + // TODO we should not remove them until they are garbage collected + async takeGarbage(): Promise { + const keys = await this.#redisClient.eval( + /* lua */ ` + local keys = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', '0') + redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', '0') + return keys + `, + [REDIS_REF_COUNTER], [], ); + + return keys as string[]; } - override async close(): Promise { - this.#s3.destroy(); - await deinitRemoteUploadUrlStore(this.#uploadUrls); + // for debugging purpose + async inspect(label = "") { + const data = await this.#redisClient.zrange(REDIS_REF_COUNTER, 0, -1, { + withScore: true, + }); + console.log( + "refCounts", + label, + chunk(data, 2).reduce( + (acc, [key, value]) => ({ ...acc, [key]: value }), + {}, + ), + ); } } + +export async function createSharedArtifactStore( + baseDir: string, + syncConfig: SyncConfig, +): Promise { + const persistence = await SharedArtifactPersistence.init(baseDir, syncConfig); + const uploadEndpoints = await SharedUploadEndpointManager.init(syncConfig); + const refCounter = await SharedArtifactRefCounter.init(syncConfig.redis); + return ArtifactStore.init(persistence, uploadEndpoints, refCounter); +} diff --git a/typegate/src/typegate/mod.ts b/typegate/src/typegate/mod.ts index 44509e86f7..c8067ee65d 100644 --- a/typegate/src/typegate/mod.ts +++ b/typegate/src/typegate/mod.ts @@ -37,12 +37,14 @@ import introspectionJson from "../typegraphs/introspection.json" with { }; import { ArtifactService } from "../services/artifact_service.ts"; import { ArtifactStore } from "./artifacts/mod.ts"; -import { LocalArtifactStore } from "./artifacts/local.ts"; import { SyncConfig } from "../sync/config.ts"; +// TODO move from tests (MET-497) import { MemoryRegister } from "test-utils/memory_register.ts"; import { NoLimiter } from "test-utils/no_limiter.ts"; import { TypegraphStore } from "../sync/typegraph.ts"; -import { SharedArtifactStore } from "./artifacts/shared.ts"; +import { createLocalArtifactStore } from "./artifacts/local.ts"; +import { createSharedArtifactStore } from "./artifacts/shared.ts"; +import { AsyncDisposableStack } from "dispose"; const INTROSPECTION_JSON_STR = JSON.stringify(introspectionJson); @@ -70,31 +72,37 @@ export interface DeinitOptions { engines?: boolean; } -export interface DeinitOptions { - engines?: boolean; -} - -export class Typegate { +export class Typegate implements AsyncDisposable { #onPushHooks: PushHandler[] = []; #artifactService: ArtifactService; + #disposed = false; static async init( syncConfig: SyncConfig | null = null, customRegister: Register | null = null, + tmpDir = config.tmp_dir, ): Promise { if (syncConfig == null) { - logger.warning("Entering no-sync mode..."); - logger.warning( + logger.warn("Entering no-sync mode..."); + logger.warn( "Enable sync if you want to use accross multiple instances or if you want persistence.", ); + await using stack = new AsyncDisposableStack(); + const register = customRegister ?? new MemoryRegister(); - const artifactStore = await LocalArtifactStore.init(); + const artifactStore = await createLocalArtifactStore(tmpDir); + + stack.use(register); + stack.use(artifactStore); + return new Typegate( register, new NoLimiter(), artifactStore, null, + tmpDir, + stack.move(), ); } else { if (customRegister) { @@ -102,14 +110,36 @@ export class Typegate { "Custom register is not supported in sync mode", ); } + + await using stack = new AsyncDisposableStack(); + const limiter = await RedisRateLimiter.init(syncConfig.redis); - const artifactStore = await SharedArtifactStore.init(syncConfig); - const typegate = new Typegate(null!, limiter, artifactStore, syncConfig); + // stack.use(limiter); + stack.defer(async () => { + await limiter.terminate(); + }); + + const artifactStore = await createSharedArtifactStore( + tmpDir, + syncConfig, + ); + stack.use(artifactStore); + + const typegate = new Typegate( + null!, + limiter, + artifactStore, + syncConfig, + tmpDir, + stack.move(), + ); + const register = await ReplicatedRegister.init( typegate, syncConfig.redis, TypegraphStore.init(syncConfig), ); + typegate.disposables.use(register); (typegate as { register: Register }).register = register; @@ -130,6 +160,8 @@ export class Typegate { private limiter: RateLimiter, public artifactStore: ArtifactStore, public syncConfig: SyncConfig | null = null, + public tmpDir: string, + private disposables: AsyncDisposableStack, ) { this.#onPush((tg) => Promise.resolve(upgradeTypegraph(tg))); this.#onPush((tg) => Promise.resolve(parseGraphQLTypeGraph(tg))); @@ -138,16 +170,10 @@ export class Typegate { this.#artifactService = new ArtifactService(artifactStore); } - async deinit(opts: DeinitOptions = {}) { - const engines = opts.engines ?? true; - if (engines) { - await Promise.all(this.register.list().map((e) => e.terminate())); - } - await this.artifactStore.close(); - if (this.syncConfig) { - await (this.register as ReplicatedRegister).stopSync(); - await (this.limiter as RedisRateLimiter).terminate(); - } + async [Symbol.asyncDispose]() { + if (this.#disposed) return; + this.#disposed = true; + await this.disposables[Symbol.asyncDispose](); } #onPush(handler: PushHandler) { @@ -322,9 +348,24 @@ export class Typegate { enableIntrospection, ); + const oldArtifacts = new Set( + Object.values(this.register.get(name)?.tg.tg.meta.artifacts ?? {}) + .map((m) => m.hash), + ); + logger.info(`Registering engine '${name}'`); await this.register.add(engine); + const newArtifacts = new Set( + Object.values(engine.tg.tg.meta.artifacts) + .map((m) => m.hash), + ); + + await this.artifactStore.updateRefCounts( + newArtifacts, + oldArtifacts, + ); + return { name, engine, @@ -332,6 +373,22 @@ export class Typegate { }; } + async removeTypegraph(name: string) { + const engine = this.register.get(name); + if (!engine) { + throw new Error(`Engine '${name}' not found`); + } + + await this.register.remove(name); + + const artifacts = new Set( + Object.values(engine.tg.tg.meta.artifacts) + .map((m) => m.hash), + ); + await this.artifactStore.updateRefCounts(new Set(), artifacts); + await this.artifactStore.runArtifactGC(); + } + async initQueryEngine( tgDS: TypeGraphDS, secretManager: SecretManager, @@ -370,9 +427,4 @@ export class Typegate { await engine.registerEndpoints(); return engine; } - - async terminate() { - await Promise.all(this.register.list().map((e) => e.terminate())); - await this.artifactStore.close(); - } } diff --git a/typegate/src/typegate/rate_limiter.ts b/typegate/src/typegate/rate_limiter.ts index 505cdd1036..71b3d0e5f8 100644 --- a/typegate/src/typegate/rate_limiter.ts +++ b/typegate/src/typegate/rate_limiter.ts @@ -2,7 +2,10 @@ // SPDX-License-Identifier: Elastic-2.0 import { connect, Redis, RedisConnectOptions } from "redis"; -import { Deferred, deferred } from "std/async/deferred.ts"; +import { + Deferred, + deferred, +} from "https://deno.land/std@0.208.0/async/deferred.ts"; import { QueryEngine } from "../engine/query_engine.ts"; // keys: tokens, latest diff --git a/typegate/src/typegate/register.ts b/typegate/src/typegate/register.ts index a6d62ab2c2..c7528a6314 100644 --- a/typegate/src/typegate/register.ts +++ b/typegate/src/typegate/register.ts @@ -22,7 +22,7 @@ export interface Migrations { migrations: string; } -export abstract class Register { +export abstract class Register implements AsyncDisposable { abstract add(engine: QueryEngine): Promise; abstract remove(name: string): Promise; @@ -32,6 +32,8 @@ export abstract class Register { abstract get(name: string): QueryEngine | undefined; abstract has(name: string): boolean; + + abstract [Symbol.asyncDispose](): Promise; } export class ReplicatedRegister extends Register { @@ -76,7 +78,7 @@ export class ReplicatedRegister extends Register { return engine; }, async terminate(engine: QueryEngine) { - await engine.terminate(); + await engine[Symbol.asyncDispose](); }, }, ); @@ -88,6 +90,11 @@ export class ReplicatedRegister extends Register { super(); } + async [Symbol.asyncDispose](): Promise { + await this.replicatedMap[Symbol.asyncDispose](); + await Promise.all(this.list().map((e) => e[Symbol.asyncDispose]())); + } + async add(engine: QueryEngine): Promise { if (SystemTypegraph.check(engine.name)) { // no need for a sync @@ -103,7 +110,7 @@ export class ReplicatedRegister extends Register { const old = this.replicatedMap.memory.get(name); if (old) { this.replicatedMap.memory.delete(name); - await old.terminate(); + await old[Symbol.asyncDispose](); } } else { await this.replicatedMap.delete(name); @@ -129,8 +136,4 @@ export class ReplicatedRegister extends Register { startSync(xid: XIdInput): void { void this.replicatedMap.startSync(xid); } - - async stopSync() { - await this.replicatedMap.stopSync(); - } } diff --git a/typegate/src/typegraph/mod.ts b/typegate/src/typegraph/mod.ts index a6907b5eb1..8953acccc1 100644 --- a/typegate/src/typegraph/mod.ts +++ b/typegate/src/typegraph/mod.ts @@ -84,7 +84,7 @@ const GRAPHQL_SCALAR_TYPES = { [Type.STRING]: "String", } as Partial>; -export class TypeGraph { +export class TypeGraph implements AsyncDisposable { static readonly emptyArgs: ast.ArgumentNode[] = []; static emptyFields: ast.SelectionSetNode = { kind: Kind.SELECTION_SET, @@ -122,6 +122,13 @@ export class TypeGraph { this.typeByName = typeByName; } + async [Symbol.asyncDispose](): Promise { + await Promise.all(this.runtimeReferences.map((r) => r.deinit())); + if (this.introspection) { + await this.introspection[Symbol.asyncDispose](); + } + } + get rawName() { return this.root.title; } @@ -261,13 +268,6 @@ export class TypeGraph { return tg; } - async deinit(): Promise { - await Promise.all(this.runtimeReferences.map((r) => r.deinit())); - if (this.introspection) { - await this.introspection.deinit(); - } - } - type(idx: number): TypeNode; type( idx: number, diff --git a/typegate/src/typegraph/versions.ts b/typegate/src/typegraph/versions.ts index c0f34f4511..fee68c2943 100644 --- a/typegate/src/typegraph/versions.ts +++ b/typegate/src/typegraph/versions.ts @@ -42,7 +42,10 @@ const typegraphChangelog: Record< export function isTypegraphUpToDate(typegraph: TypeGraphDS): boolean { const { meta } = typegraph; - return semver.eq(semver.parse(typegraphVersion), semver.parse(meta.version)); + return semver.equals( + semver.parse(typegraphVersion), + semver.parse(meta.version), + ); } export function upgradeTypegraph(typegraph: TypeGraphDS): TypeGraphDS { @@ -51,7 +54,10 @@ export function upgradeTypegraph(typegraph: TypeGraphDS): TypeGraphDS { let currentVersion = meta.version; while ( - semver.neq(semver.parse(typegraphVersion), semver.parse(currentVersion)) + semver.notEquals( + semver.parse(typegraphVersion), + semver.parse(currentVersion), + ) ) { const migration = typegraphChangelog[currentVersion]; if (!migration) { diff --git a/typegate/src/utils.ts b/typegate/src/utils.ts index eff09f8587..c7732a146e 100644 --- a/typegate/src/utils.ts +++ b/typegate/src/utils.ts @@ -3,7 +3,7 @@ import type { ComputeStage } from "./engine/query_engine.ts"; import * as ast from "graphql/ast"; -import * as base64 from "std/encoding/base64.ts"; +import { decodeBase64, encodeBase64 } from "std/encoding/base64.ts"; import levenshtein from "levenshtein"; import { None, Option, Some } from "monads"; @@ -12,7 +12,8 @@ import type { TypeGraph } from "./typegraph/mod.ts"; import { ensureDir, ensureFile } from "std/fs/mod.ts"; import { Untar } from "std/archive/untar.ts"; -import * as streams from "std/streams/mod.ts"; +import { readerFromStreamReader } from "std/io/reader_from_stream_reader.ts"; +import { toReadableStream } from "std/io/to_readable_stream.ts"; import { path } from "compress/deps.ts"; import { sha1 } from "./crypto.ts"; import { BRANCH_NAME_SEPARATOR } from "./engine/computation_engine.ts"; @@ -134,11 +135,11 @@ export function iterParentStages( } export const b64decode = (v: string): string => { - return new TextDecoder().decode(base64.decode(v)); + return new TextDecoder().decode(decodeBase64(v)); }; export const b64encode = (v: string): string => { - return base64.encode(v); + return encodeBase64(v); }; export function nativeResult( @@ -223,12 +224,12 @@ export async function uncompress( dir: string, tarb64: string, ): Promise { - const buffer = base64.decode(tarb64); + const buffer = decodeBase64(tarb64); const streamReader = new Blob([buffer]) .stream() .pipeThrough(new DecompressionStream("gzip")) .getReader(); - const denoReader = streams.readerFromStreamReader(streamReader); + const denoReader = readerFromStreamReader(streamReader); const untar = new Untar(denoReader); const entries = []; @@ -248,7 +249,7 @@ export async function uncompress( await ensureFile(resFilePath); file = await Deno.open(resFilePath, { write: true }); - await streams.copy(entry, file); + await toReadableStream(entry).pipeTo(file.writable); } catch (e) { throw e; } finally { diff --git a/typegate/tests/artifacts/artifacts_test.ts b/typegate/tests/artifacts/artifacts_test.ts new file mode 100644 index 0000000000..4597c2234b --- /dev/null +++ b/typegate/tests/artifacts/artifacts_test.ts @@ -0,0 +1,161 @@ +// Copyright Metatype OÜ, licensed under the Elastic License 2.0. +// SPDX-License-Identifier: Elastic-2.0 + +import { Meta } from "test-utils/mod.ts"; +import { MetaTest } from "test-utils/test.ts"; +import { testDir } from "test-utils/dir.ts"; +import { join } from "std/path/join.ts"; +import { exists } from "std/fs/exists.ts"; +import { assert, assertFalse } from "std/assert/mod.ts"; +import { connect } from "redis"; +import { S3Client } from "aws-sdk/client-s3"; +import { createBucket, hasObject, tryDeleteBucket } from "test-utils/s3.ts"; +import { + REDIS_REF_COUNTER, + resolveS3Key, +} from "@typegate/typegate/artifacts/shared.ts"; + +const syncConfig = { + redis: { + hostname: "localhost", + port: 6379, + password: "password", + db: 1, + }, + s3: { + endpoint: "http://localhost:9000", + region: "local", + credentials: { + accessKeyId: "minio", + secretAccessKey: "password", + }, + forcePathStyle: true, + }, + s3Bucket: "artifact-sync-test", +}; + +async function cleanUp() { + using redis = await connect(syncConfig.redis); + await redis.del(REDIS_REF_COUNTER); + + const s3 = new S3Client(syncConfig.s3); + await tryDeleteBucket(s3, syncConfig.s3Bucket); + await createBucket(s3, syncConfig.s3Bucket); + s3.destroy(); + await redis.quit(); +} + +const variants = [ + { nameSuffix: "" }, + { + syncConfig, + async setup() { + await cleanUp(); + }, + async teardown() { + await cleanUp(); + }, + nameSuffix: " (sync)", + }, +] as const; + +async function hasArtifact(t: MetaTest, hash: string, sync: boolean) { + if (sync) { + const s3 = new S3Client(syncConfig.s3); + const res = await hasObject(s3, syncConfig.s3Bucket, resolveS3Key(hash)); + s3.destroy(); + return res; + } else { + return await exists(join(t.tempDir, "artifacts-cache", hash)); + } +} + +for (const { nameSuffix, ...options } of variants) { + Meta.test({ + name: "Upload protocol" + nameSuffix, + ...options, + }, async (t) => { + const e = await t.engine("runtimes/deno/deno.py"); + const artifacts = e.tg.tg.meta.artifacts; + + await t.should("have uploaded artifacts on deploy", async () => { + for (const [_, meta] of Object.entries(artifacts)) { + assert(await hasArtifact(t, meta.hash, "syncConfig" in options)); + } + }); + + await t.undeploy(e.name); + + await t.should("have removed artifacts on undeploy", async () => { + for (const [_, meta] of Object.entries(artifacts)) { + assertFalse(await hasArtifact(t, meta.hash, "syncConfig" in options)); + } + }); + }); + + Meta.test({ + name: "Upload protocol: tg_deploy (NodeJs SDK)" + nameSuffix, + ...options, + }, async (_t) => { + // TODO + }); + + Meta.test({ + name: "Upload protocol: tg_deploy (Python SDK)" + nameSuffix, + ...options, + }, async (t) => { + const e = await t.engineFromTgDeployPython( + "runtimes/deno/deploy_deno.py", + join(testDir, "runtimes/deno"), + ); + const artifacts = e.tg.tg.meta.artifacts; + + await t.should("have uploaded artifacts on deploy", async () => { + for (const [_, meta] of Object.entries(artifacts)) { + assert(await hasArtifact(t, meta.hash, "syncConfig" in options)); + } + }); + + await t.undeploy(e.name); + + await t.should("have removed artifacts on undeploy", async () => { + for (const [_, meta] of Object.entries(artifacts)) { + assertFalse(await hasArtifact(t, meta.hash, "syncConfig" in options)); + } + }); + }); + + Meta.test({ + name: "Artifact GC: shared artifacts" + nameSuffix, + ...options, + }, async (t) => { + const engine = await t.engine("runtimes/deno/deno.py"); + const artifacts = engine.tg.tg.meta.artifacts; + + const enginePartial = await t.engine("runtimes/deno/deno_partial.py"); + const sharedArtifacts = Object.keys(enginePartial.tg.tg.meta.artifacts) + .filter((art) => art in artifacts); + + await t.undeploy(engine.name); + + await t.should("have removed shared artifacts", async () => { + for (const [art, meta] of Object.entries(artifacts)) { + if (sharedArtifacts.includes(art)) { + assert(await hasArtifact(t, meta.hash, "syncConfig" in options)); + } else { + assertFalse(await hasArtifact(t, meta.hash, "syncConfig" in options)); + } + } + }); + + await t.undeploy(enginePartial.name); + + await t.should("have removed all artifacts", async () => { + for (const [_, meta] of Object.entries(artifacts)) { + assertFalse( + await hasArtifact(t, meta.hash, "syncConfig" in options), + ); + } + }); + }); +} diff --git a/typegate/tests/e2e/cli/deploy_test.ts b/typegate/tests/e2e/cli/deploy_test.ts index 93ac72a629..f1e9dd7bac 100644 --- a/typegate/tests/e2e/cli/deploy_test.ts +++ b/typegate/tests/e2e/cli/deploy_test.ts @@ -77,8 +77,6 @@ async function deploy( Meta.test({ name: "meta deploy: fails migration for new columns without default value", - port: true, - systemTypegraphs: true, }, async (t) => { const schema = randomSchema(); const secrets = { @@ -137,8 +135,6 @@ Meta.test({ Meta.test({ name: "meta deploy: succeeds migration for new columns with default value", - port: true, - systemTypegraphs: true, }, async (t) => { const port = t.port!; const schema = randomSchema(); @@ -186,8 +182,6 @@ Meta.test({ Meta.test({ name: "cli:deploy - automatic migrations", - systemTypegraphs: true, - port: true, gitRepo: { content: { @@ -250,20 +244,22 @@ Meta.test({ "--create-migration", ]); - await t.should( - "succeed have replaced and terminated the previous engine", - async () => { - await gql` - query { - findManyRecords { - id - } - } - ` - .expectErrorContains("Could not find engine") - .on(e); - }, - ); + // TODO: MET-500 + // Does not work with the new version of t.e engine + // await t.should( + // "have replaced and terminated the previous engine", + // async () => { + // await gql` + // query { + // findManyRecords { + // id + // } + // } + // ` + // .expectErrorContains("Could not find engine") + // .on(e); + // }, + // ); const e2 = t.getTypegraphEngine("prisma")!; @@ -285,8 +281,6 @@ Meta.test({ Meta.test({ name: "cli:deploy - with prefix", - systemTypegraphs: true, - port: true, gitRepo: { content: { @@ -337,20 +331,22 @@ Meta.test({ "--create-migration", ]); - await t.should( - "succeed have replaced and terminated the previous engine", - async () => { - await gql` - query { - findManyRecords { - id - } - } - ` - .expectErrorContains("Could not find engine") - .on(e); - }, - ); + // TODO: MET-500 + // Does not work with the new version of t.e engine + // await t.should( + // "succeed have replaced and terminated the previous engine", + // async () => { + // await gql` + // query { + // findManyRecords { + // id + // } + // } + // ` + // .expectErrorContains("Could not find engine") + // .on(e); + // }, + // ); const e2 = t.getTypegraphEngine("pref-prisma")!; diff --git a/typegate/tests/e2e/cli/dev_test.ts b/typegate/tests/e2e/cli/dev_test.ts index defd27dd4f..6ff238706c 100644 --- a/typegate/tests/e2e/cli/dev_test.ts +++ b/typegate/tests/e2e/cli/dev_test.ts @@ -33,8 +33,6 @@ async function writeTypegraph(version: number | null, target = "migration.py") { Meta.test({ name: "meta dev: choose to reset the database", - port: true, - systemTypegraphs: true, gitRepo: { content: { @@ -139,8 +137,6 @@ async function listSubdirs(path: string): Promise { Meta.test({ name: "meta dev: remove latest migration", - port: true, - systemTypegraphs: true, gitRepo: { content: { diff --git a/typegate/tests/e2e/cli/undeploy_test.ts b/typegate/tests/e2e/cli/undeploy_test.ts index 33063db1f5..950aa2ec3e 100644 --- a/typegate/tests/e2e/cli/undeploy_test.ts +++ b/typegate/tests/e2e/cli/undeploy_test.ts @@ -9,8 +9,6 @@ const m = new TestModule(import.meta); Meta.test({ name: "meta undeploy", - port: true, - systemTypegraphs: true, }, async (t) => { const schema = randomSchema(); // prepare diff --git a/typegate/tests/e2e/nextjs/apollo_test.ts b/typegate/tests/e2e/nextjs/apollo_test.ts index f477f1a33b..d164709244 100644 --- a/typegate/tests/e2e/nextjs/apollo_test.ts +++ b/typegate/tests/e2e/nextjs/apollo_test.ts @@ -277,8 +277,6 @@ async function undeployTypegraph(port: number) { Meta.test({ name: "apollo client", - port: true, - systemTypegraphs: true, introspection: true, }, async (t) => { await initBucket(); diff --git a/typegate/tests/e2e/self_deploy/self_deploy_test.ts b/typegate/tests/e2e/self_deploy/self_deploy_test.ts index 78e217aef3..1c03d834e1 100644 --- a/typegate/tests/e2e/self_deploy/self_deploy_test.ts +++ b/typegate/tests/e2e/self_deploy/self_deploy_test.ts @@ -11,8 +11,6 @@ import * as path from "std/path/mod.ts"; Meta.test({ name: "deploy and undeploy typegraph without meta-cli", - port: true, - systemTypegraphs: true, }, async (t) => { const gate = `http://localhost:${t.port}`; const auth = new BasicAuth("admin", "password"); diff --git a/typegate/tests/e2e/templates/templates_test.ts b/typegate/tests/e2e/templates/templates_test.ts index 6d6a23b3ca..e6d2fa2b17 100644 --- a/typegate/tests/e2e/templates/templates_test.ts +++ b/typegate/tests/e2e/templates/templates_test.ts @@ -70,8 +70,6 @@ const modifiers: Record Promise | void> = { for (const template of ["python", "deno", "node"]) { Meta.test({ name: `${template} template`, - port: true, - systemTypegraphs: true, }, async (t) => { const dir = await newTempDir(); diff --git a/typegate/tests/internal/internal_test.ts b/typegate/tests/internal/internal_test.ts index 86d87ed4f6..ae368c4cea 100644 --- a/typegate/tests/internal/internal_test.ts +++ b/typegate/tests/internal/internal_test.ts @@ -9,8 +9,6 @@ const cwd = path.join(testDir, "internal"); Meta.test({ name: "Internal test", - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython("internal/internal.py", cwd); diff --git a/typegate/tests/metatype.yml b/typegate/tests/metatype.yml index 087b1c7ad8..47a94ac849 100644 --- a/typegate/tests/metatype.yml +++ b/typegate/tests/metatype.yml @@ -18,7 +18,7 @@ typegates: secrets: prisma: POSTGRES: "postgresql://postgres:password@localhost:5432/db?schema=e2e" - migration_failure_test: + migration-failure-test: POSTGRES: "postgresql://postgres:password@localhost:5432/db?schema=e2e2" typegraphs: diff --git a/typegate/tests/policies/policies_jwt_injection.py b/typegate/tests/policies/policies_jwt_injection.py index cc9b39c704..427b5d9724 100644 --- a/typegate/tests/policies/policies_jwt_injection.py +++ b/typegate/tests/policies/policies_jwt_injection.py @@ -18,6 +18,6 @@ def policies_jwt_injection(g: Graph): g.expose( sayHelloWorld=deno.func( - t.struct({}), t.string(), code="""() => "Hello World!""" + t.struct({}), t.string(), code="""() => 'Hello World!'""" ).with_policy(some_policy), ) diff --git a/typegate/tests/policies/policies_test.ts b/typegate/tests/policies/policies_test.ts index ae04923d4d..1e97687c79 100644 --- a/typegate/tests/policies/policies_test.ts +++ b/typegate/tests/policies/policies_test.ts @@ -20,7 +20,7 @@ async function genSecretKey(): Promise> { Meta.test("Policies", async (t) => { const e = await t.engine("policies/policies.py", { secrets: await genSecretKey(), - typegraph: "policies", + // typegraph: "policies", }); await t.should("have public access", async () => { diff --git a/typegate/tests/prisma_migrate/prisma_migrate_test.ts b/typegate/tests/prisma_migrate/prisma_migrate_test.ts index 3612cb17d8..7ba9d79c6c 100644 --- a/typegate/tests/prisma_migrate/prisma_migrate_test.ts +++ b/typegate/tests/prisma_migrate/prisma_migrate_test.ts @@ -16,7 +16,6 @@ import { testDir } from "../utils/dir.ts"; Meta.test({ name: "prisma migrations", - systemTypegraphs: true, }, async (t) => { const tgPath = "runtimes/prisma/prisma.py"; const migrations = t.getTypegraphEngine("typegate/prisma_migration")!; diff --git a/typegate/tests/runtimes/deno/deno.py b/typegate/tests/runtimes/deno/deno.py index c5130ab1aa..9d8370c1f0 100644 --- a/typegate/tests/runtimes/deno/deno.py +++ b/typegate/tests/runtimes/deno/deno.py @@ -1,13 +1,3 @@ -import os -import sys - -from typegraph.gen.exports.core import ( - ArtifactResolutionConfig, - MigrationAction, - MigrationConfig, -) -from typegraph.graph.shared_types import BasicAuth -from typegraph.graph.tg_deploy import TypegraphDeployParams, tg_deploy from typegraph.graph.typegraph import Graph from typegraph.policy import Policy from typegraph.runtimes.deno import DenoRuntime @@ -69,32 +59,3 @@ def deno(g: Graph): """, ), ) - - -cwd = sys.argv[1] -PORT = sys.argv[2] -gate = f"http://localhost:{PORT}" -auth = BasicAuth("admin", "password") - -deno_tg = deno() -deploy_result = tg_deploy( - deno_tg, - TypegraphDeployParams( - base_url=gate, - auth=auth, - typegraph_path=os.path.join(cwd, "deno.py"), - artifacts_config=ArtifactResolutionConfig( - dir=cwd, - prefix=None, - disable_artifact_resolution=None, - codegen=None, - prisma_migration=MigrationConfig( - migration_dir="prisma-migrations", - global_action=MigrationAction(reset=False, create=True), - runtime_actions=None, - ), - ), - ), -) - -print(deploy_result.serialized) diff --git a/typegate/tests/runtimes/deno/deno_partial.py b/typegate/tests/runtimes/deno/deno_partial.py new file mode 100644 index 0000000000..c4441993f8 --- /dev/null +++ b/typegate/tests/runtimes/deno/deno_partial.py @@ -0,0 +1,22 @@ +from typegraph.graph.typegraph import Graph +from typegraph.policy import Policy +from typegraph.runtimes.deno import DenoRuntime + +from typegraph import t, typegraph + + +@typegraph() +def deno_partial(g: Graph): + public = Policy.public() + + deno = DenoRuntime() + + g.expose( + public, + sum=deno.import_( + t.struct({"numbers": t.list(t.integer())}), + t.integer(), + module="ts/deno.ts", + name="sum", + ), + ) diff --git a/typegate/tests/runtimes/deno/deno_test.ts b/typegate/tests/runtimes/deno/deno_test.ts index 56f79ba0bf..35a40912d6 100644 --- a/typegate/tests/runtimes/deno/deno_test.ts +++ b/typegate/tests/runtimes/deno/deno_test.ts @@ -28,11 +28,12 @@ const reusableTgOutput = { Meta.test( { name: "Deno runtime", - port: true, - systemTypegraphs: true, }, async (t) => { - const e = await t.engineFromTgDeployPython("runtimes/deno/deno.py", cwd); + const e = await t.engineFromTgDeployPython( + "runtimes/deno/deploy_deno.py", + cwd, + ); await t.should("work on the default worker", async () => { await gql` @@ -113,11 +114,12 @@ Meta.test( Meta.test( { name: "Deno runtime: file name reloading", - port: true, - systemTypegraphs: true, }, async (t) => { - const e = await t.engineFromTgDeployPython("runtimes/deno/deno.py", cwd); + const e = await t.engineFromTgDeployPython( + "runtimes/deno/deploy_deno.py", + cwd, + ); await t.should("success for allowed network access", async () => { await gql` @@ -148,8 +150,6 @@ Meta.test( Meta.test( { name: "Deno runtime: use local imports", - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython( @@ -206,8 +206,6 @@ Meta.test("Deno runtime with typescript", async (t) => { Meta.test( { name: "DenoRuntime using TS SDK: artifacts and deps", - port: true, - systemTypegraphs: true, }, async (metaTest) => { const port = metaTest.port; @@ -257,8 +255,6 @@ Meta.test( Meta.test( { name: "Deno runtime: file name reloading", - port: true, - systemTypegraphs: true, }, async (t) => { const load = async (value: number) => { @@ -304,8 +300,6 @@ Meta.test( Meta.test( { name: "Deno runtime: script reloading", - port: true, - systemTypegraphs: true, }, async (t) => { const denoScript = path.join( @@ -353,11 +347,12 @@ Meta.test( { name: "Deno runtime: infinite loop or similar", sanitizeOps: false, - port: true, - systemTypegraphs: true, }, async (t) => { - const e = await t.engineFromTgDeployPython("runtimes/deno/deno.py", cwd); + const e = await t.engineFromTgDeployPython( + "runtimes/deno/deploy_deno.py", + cwd, + ); await t.should("safely fail upon stack overflow", async () => { await gql` diff --git a/typegate/tests/runtimes/deno/deploy_deno.py b/typegate/tests/runtimes/deno/deploy_deno.py new file mode 100644 index 0000000000..af0158f38b --- /dev/null +++ b/typegate/tests/runtimes/deno/deploy_deno.py @@ -0,0 +1,40 @@ +from deno import deno + +import os +import sys + +from typegraph.gen.exports.core import ( + ArtifactResolutionConfig, + MigrationAction, + MigrationConfig, +) +from typegraph.graph.shared_types import BasicAuth +from typegraph.graph.tg_deploy import TypegraphDeployParams, tg_deploy + +cwd = sys.argv[1] +PORT = sys.argv[2] +gate = f"http://localhost:{PORT}" +auth = BasicAuth("admin", "password") + +deno_tg = deno() +deploy_result = tg_deploy( + deno_tg, + TypegraphDeployParams( + base_url=gate, + auth=auth, + typegraph_path=os.path.join(cwd, "deno.py"), + artifacts_config=ArtifactResolutionConfig( + dir=cwd, + prefix=None, + disable_artifact_resolution=None, + codegen=None, + prisma_migration=MigrationConfig( + migration_dir="prisma-migrations", + global_action=MigrationAction(reset=False, create=True), + runtime_actions=None, + ), + ), + ), +) + +print(deploy_result.serialized) diff --git a/typegate/tests/runtimes/python/python_test.ts b/typegate/tests/runtimes/python/python_test.ts index a17105268e..3ae831bbb7 100644 --- a/typegate/tests/runtimes/python/python_test.ts +++ b/typegate/tests/runtimes/python/python_test.ts @@ -107,8 +107,6 @@ Meta.test("Python VM performance", async (t) => { Meta.test( { name: "Python runtime", - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython( @@ -202,8 +200,6 @@ Meta.test( Meta.test( { name: "Deno: def, lambda", - port: true, - systemTypegraphs: true, }, async (t) => { const port = t.port; @@ -273,8 +269,6 @@ Meta.test( Meta.test( { name: "Python: upload artifacts with deps", - port: true, - systemTypegraphs: true, }, async (metaTest) => { const port = metaTest.port; @@ -326,8 +320,6 @@ Meta.test( { name: "Python: infinite loop or similar", sanitizeOps: false, - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython( @@ -367,8 +359,6 @@ Meta.test( Meta.test( { name: "Python: typegate reloading", - port: true, - systemTypegraphs: true, }, async (metaTest) => { const port = metaTest.port; diff --git a/typegate/tests/runtimes/typegate/typegate_prisma_test.ts b/typegate/tests/runtimes/typegate/typegate_prisma_test.ts index fe8d3a9478..ecf67d26eb 100644 --- a/typegate/tests/runtimes/typegate/typegate_prisma_test.ts +++ b/typegate/tests/runtimes/typegate/typegate_prisma_test.ts @@ -10,7 +10,6 @@ const adminHeaders = { Meta.test({ name: "typegate: find available operations", - systemTypegraphs: true, }, async (t) => { const prismaEngine = await t.engine("runtimes/prisma/prisma.py", { secrets: { diff --git a/typegate/tests/runtimes/typegate/typegate_runtime_test.ts b/typegate/tests/runtimes/typegate/typegate_runtime_test.ts index b8995c12b7..64949f0b27 100644 --- a/typegate/tests/runtimes/typegate/typegate_runtime_test.ts +++ b/typegate/tests/runtimes/typegate/typegate_runtime_test.ts @@ -6,7 +6,6 @@ import { gql, Meta } from "../../utils/mod.ts"; Meta.test({ name: "typegate: find available operations", - systemTypegraphs: true, }, async (t) => { const prismaEngine = await t.engine("runtimes/prisma/prisma.py", { secrets: { diff --git a/typegate/tests/runtimes/wasm_reflected/wasm_reflected_test.ts b/typegate/tests/runtimes/wasm_reflected/wasm_reflected_test.ts index dfc8a64590..e260510779 100644 --- a/typegate/tests/runtimes/wasm_reflected/wasm_reflected_test.ts +++ b/typegate/tests/runtimes/wasm_reflected/wasm_reflected_test.ts @@ -13,8 +13,6 @@ const auth = new BasicAuth("admin", "password"); Meta.test( { name: "Wasm runtime: reflected", - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython( @@ -39,8 +37,6 @@ Meta.test( Meta.test( { name: "Wasm Runtime typescript sdk: reflected", - port: true, - systemTypegraphs: true, }, async (metaTest) => { const port = metaTest.port; @@ -64,7 +60,7 @@ Meta.test( secrets: {}, }); - const engine = await metaTest.engineFromDeployed(serialized); + await using engine = await metaTest.engineFromDeployed(serialized); await t.step("wit bindings", async () => { await gql` @@ -185,8 +181,6 @@ Meta.test( }) .on(engine); }); - - await engine.terminate(); }); }, ); diff --git a/typegate/tests/runtimes/wasm_reflected/wasm_sync_test.ts b/typegate/tests/runtimes/wasm_reflected/wasm_sync_test.ts index ea1b83c24d..7de3cc13a0 100644 --- a/typegate/tests/runtimes/wasm_reflected/wasm_sync_test.ts +++ b/typegate/tests/runtimes/wasm_reflected/wasm_sync_test.ts @@ -51,8 +51,6 @@ const auth = new BasicAuth("admin", "password"); Meta.test( { name: "Wasm Runtime typescript SDK: Sync Config", - port: true, - systemTypegraphs: true, syncConfig, async setup() { await cleanUp(); diff --git a/typegate/tests/runtimes/wasm_wire/wasm_sync_test.ts b/typegate/tests/runtimes/wasm_wire/wasm_sync_test.ts index 72bd25a9bc..fcd20066e6 100644 --- a/typegate/tests/runtimes/wasm_wire/wasm_sync_test.ts +++ b/typegate/tests/runtimes/wasm_wire/wasm_sync_test.ts @@ -51,8 +51,6 @@ const auth = new BasicAuth("admin", "password"); Meta.test( { name: "Wasm Runtime typescript SDK: Sync Config", - port: true, - systemTypegraphs: true, syncConfig, async setup() { await cleanUp(); diff --git a/typegate/tests/runtimes/wasm_wire/wasm_wire_test.ts b/typegate/tests/runtimes/wasm_wire/wasm_wire_test.ts index 11970ec741..99a3d9fe74 100644 --- a/typegate/tests/runtimes/wasm_wire/wasm_wire_test.ts +++ b/typegate/tests/runtimes/wasm_wire/wasm_wire_test.ts @@ -12,8 +12,6 @@ const auth = new BasicAuth("admin", "password"); Meta.test( { name: "Wasm runtime: wire", - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython( @@ -38,8 +36,6 @@ Meta.test( Meta.test( { name: "Wasm Runtime typescript sdk: wire", - port: true, - systemTypegraphs: true, }, async (metaTest) => { const port = metaTest.port; @@ -63,7 +59,7 @@ Meta.test( secrets: {}, }); - const engine = await metaTest.engineFromDeployed(serialized); + await using engine = await metaTest.engineFromDeployed(serialized); await t.step("wit bindings", async () => { await gql` @@ -182,8 +178,6 @@ Meta.test( }) .on(engine); }); - - await engine.terminate(); }); }, ); diff --git a/typegate/tests/schema_validation/circular_test.ts b/typegate/tests/schema_validation/circular_test.ts index 8c06497ba7..040fef9ee9 100644 --- a/typegate/tests/schema_validation/circular_test.ts +++ b/typegate/tests/schema_validation/circular_test.ts @@ -10,8 +10,6 @@ const cwd = path.join(testDir, "schema_validation"); Meta.test( { name: "circular test", - port: true, - systemTypegraphs: true, }, async (t) => { const tgPath = "schema_validation/circular.py"; diff --git a/typegate/tests/type_nodes/either_test.ts b/typegate/tests/type_nodes/either_test.ts index 8d548bdf1b..91569f90a8 100644 --- a/typegate/tests/type_nodes/either_test.ts +++ b/typegate/tests/type_nodes/either_test.ts @@ -11,8 +11,6 @@ Meta.test( { name: "Either type", introspection: true, - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython( diff --git a/typegate/tests/type_nodes/union_node_attr_test.ts b/typegate/tests/type_nodes/union_node_attr_test.ts index 1c70809188..1fcfca2714 100644 --- a/typegate/tests/type_nodes/union_node_attr_test.ts +++ b/typegate/tests/type_nodes/union_node_attr_test.ts @@ -10,8 +10,6 @@ const cwd = path.join(testDir, "type_nodes"); Meta.test({ name: "Union type", introspection: true, - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython( "type_nodes/union_node_attr.py", diff --git a/typegate/tests/type_nodes/union_node_quantifier_test.ts b/typegate/tests/type_nodes/union_node_quantifier_test.ts index 9bced5c491..656d1ada84 100644 --- a/typegate/tests/type_nodes/union_node_quantifier_test.ts +++ b/typegate/tests/type_nodes/union_node_quantifier_test.ts @@ -11,8 +11,6 @@ Meta.test( { name: "Union type", introspection: true, - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython( diff --git a/typegate/tests/type_nodes/union_test.ts b/typegate/tests/type_nodes/union_test.ts index ad345ab87b..9d4e9393a3 100644 --- a/typegate/tests/type_nodes/union_test.ts +++ b/typegate/tests/type_nodes/union_test.ts @@ -12,8 +12,6 @@ Meta.test( { name: "Union type", introspection: true, - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython("type_nodes/union_node.py", cwd); @@ -251,8 +249,6 @@ Meta.test( Meta.test( { name: "nested unions", - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython("type_nodes/union_node.py", cwd); @@ -305,8 +301,6 @@ Meta.test( Meta.test( { name: "multilevel unions", - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython("type_nodes/union_node.py", cwd); @@ -359,8 +353,6 @@ Meta.test( Meta.test( { name: "scalar unions", - port: true, - systemTypegraphs: true, }, async (t) => { const e = await t.engineFromTgDeployPython("type_nodes/union_node.py", cwd); diff --git a/typegate/tests/utils/memory_register.ts b/typegate/tests/utils/memory_register.ts index 9d841bd915..c52546892a 100644 --- a/typegate/tests/utils/memory_register.ts +++ b/typegate/tests/utils/memory_register.ts @@ -11,11 +11,21 @@ export class MemoryRegister extends Register { super(); } + [Symbol.asyncDispose](): Promise { + return Promise.all( + Array.from(this.map.values()).map((engine) => + engine[Symbol.asyncDispose]() + ), + ).then(() => { + this.map.clear(); + }); + } + async add(engine: QueryEngine): Promise { const old = this.map.get(engine.name); this.map.set(engine.name, engine); if (old) { - await old.terminate(); + await old[Symbol.asyncDispose](); } } @@ -23,7 +33,7 @@ export class MemoryRegister extends Register { const old = this.map.get(name); if (old) { this.map.delete(name); - await old.terminate(); + await old[Symbol.asyncDispose](); } } diff --git a/typegate/tests/utils/mod.ts b/typegate/tests/utils/mod.ts index 0ffd585794..a4ee242b14 100644 --- a/typegate/tests/utils/mod.ts +++ b/typegate/tests/utils/mod.ts @@ -1,12 +1,12 @@ // Copyright Metatype OÜ, licensed under the Elastic License 2.0. // SPDX-License-Identifier: Elastic-2.0 +// import { SingleRegister } from "test-utils/single_register.ts"; +// import { Typegate } from "@typegate/typegate/mod.ts"; import { QueryEngine } from "../../src/engine/query_engine.ts"; import { dirname, join } from "std/path/mod.ts"; -import { copy } from "std/streams/copy.ts"; +import { copy } from "std/fs/copy.ts"; import { init_native } from "native"; -import { SingleRegister } from "./single_register.ts"; -import { Typegate } from "../../src/typegate/mod.ts"; import { RestQuery } from "./query/rest_query.ts"; import { GraphQLQuery } from "./query/graphql_query.ts"; import { test } from "./test.ts"; @@ -14,6 +14,7 @@ import { metaCli } from "./meta.ts"; import { testDir } from "./dir.ts"; import { autoTest } from "./autotest.ts"; import { init_runtimes } from "../../src/runtimes/mod.ts"; +import { getCurrentTest } from "./test.ts"; // native must load first to avoid import race conditions and panic init_native(); @@ -41,33 +42,33 @@ export const Meta = { cli: metaCli, }; -// TODO use pre-existing Typegate for the test export async function execute( - engine: QueryEngine, + _engine: QueryEngine | null, request: Request, ): Promise { - const register = new SingleRegister(engine.name, engine); - const typegate = await Typegate.init(null, register); - try { - return await typegate.handle(request, { - remoteAddr: { hostname: "localhost" }, - } as Deno.ServeHandlerInfo); - } finally { - await typegate.deinit({ engines: false }); - } + // TODO: MET-500 + // This might only work in temp mode; using different temp dir for each typegate instance + // if (engine) { + // const register = new SingleRegister(engine.name, engine); + // const test = getCurrentTest(); + // await using typegate = await Typegate.init(null, register, test.tempDir); + // return await typegate.handle(request, { + // remoteAddr: { hostname: "localhost" }, + // } as Deno.ServeHandlerInfo); + // } else { + const typegate = getCurrentTest().typegates.next(); + return await typegate.handle(request, { + remoteAddr: { hostname: "localhost" }, + } as Deno.ServeHandlerInfo); + // } } export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export async function copyFile(src: string, dest: string) { - const srcFile = await Deno.open(join(testDir, src)); const destPath = join(testDir, dest); await Deno.mkdir(dirname(destPath), { recursive: true }); - const destFile = await Deno.create(destPath); - await copy(srcFile, destFile); - - srcFile.close(); - destFile.close(); + await copy(join(testDir, src), destPath); } diff --git a/typegate/tests/utils/s3.ts b/typegate/tests/utils/s3.ts index 5ac1019536..d2492a9a4c 100644 --- a/typegate/tests/utils/s3.ts +++ b/typegate/tests/utils/s3.ts @@ -5,6 +5,7 @@ import { CreateBucketCommand, DeleteBucketCommand, DeleteObjectCommand, + HeadObjectCommand, ListObjectsV2Command, S3Client, } from "aws-sdk/client-s3"; @@ -49,3 +50,16 @@ export async function createBucket(client: S3Client, bucket: string) { const createCommand = new CreateBucketCommand({ Bucket: bucket }); await client.send(createCommand); } + +export async function hasObject(client: S3Client, bucket: string, key: string) { + try { + const headCommand = new HeadObjectCommand({ Bucket: bucket, Key: key }); + await client.send(headCommand); + return true; + } catch (e) { + if (e.name === "NotFound") { + return false; + } + throw e; + } +} diff --git a/typegate/tests/utils/single_register.ts b/typegate/tests/utils/single_register.ts index 6e22b249aa..84e2a42b63 100644 --- a/typegate/tests/utils/single_register.ts +++ b/typegate/tests/utils/single_register.ts @@ -9,6 +9,11 @@ export class SingleRegister extends Register { super(); } + [Symbol.asyncDispose](): Promise { + // not disposing engine because it's shared + return Promise.resolve(); + } + add(_engine: QueryEngine): Promise { return Promise.resolve(); } diff --git a/typegate/tests/utils/test.ts b/typegate/tests/utils/test.ts index 16a43ae5a2..c0478e5c21 100644 --- a/typegate/tests/utils/test.ts +++ b/typegate/tests/utils/test.ts @@ -12,6 +12,9 @@ import { Typegate } from "../../src/typegate/mod.ts"; import { createMetaCli } from "./meta.ts"; import { TypeGraph } from "../../src/typegraph/mod.ts"; import { SyncConfig } from "../../src/sync/config.ts"; +// until deno supports it... +import { AsyncDisposableStack } from "dispose"; +import config from "../../src/config.ts"; type AssertSnapshotParams = typeof assertSnapshot extends ( ctx: Deno.TestContext, @@ -34,7 +37,7 @@ export enum SDKLangugage { } // with a round-robin load balancer emulation -class TypegateManager { +class TypegateManager implements AsyncDisposable { private index = 0; constructor(private typegates: Typegate[]) {} @@ -49,14 +52,13 @@ class TypegateManager { return typegate; } - async terminate() { - await Promise.all(this.typegates.map((tg) => tg.deinit())); + async [Symbol.asyncDispose]() { + await Promise.all(this.typegates.map((tg) => tg[Symbol.asyncDispose]())); } } -interface ServeResult { +interface ServeResult extends AsyncDisposable { port: number; - cleanup: () => Promise; } function serve(typegates: TypegateManager): Promise { @@ -66,7 +68,7 @@ function serve(typegates: TypegateManager): Promise { onListen: ({ port }) => { resolve({ port, - cleanup: async () => { + async [Symbol.asyncDispose]() { await server.shutdown(); }, }); @@ -84,25 +86,33 @@ type MetaTestCleanupFn = () => void | Promise; const defaultCli = await createMetaCli(shell); export class MetaTest { - private cleanups: MetaTestCleanupFn[] = []; shell = shell; meta = defaultCli; workingDir = testDir; - port: number | null = null; currentTypegateIndex = 0; + #disposed = false; static async init( t: Deno.TestContext, typegates: TypegateManager, introspection: boolean, - port = false, + tempDir: string, ): Promise { - const mt = new MetaTest(t, typegates, introspection); - if (port) { - const { port: p, cleanup } = await serve(typegates); - mt.port = p; - mt.addCleanup(cleanup); - } + await using stack = new AsyncDisposableStack(); + stack.use(typegates); + + const server = await serve(typegates); + const portNumber = server.port; + stack.use(server); + + const mt = new MetaTest( + t, + typegates, + introspection, + portNumber, + tempDir, + stack.move(), + ); return mt; } @@ -111,10 +121,20 @@ export class MetaTest { public t: Deno.TestContext, public typegates: TypegateManager, private introspection: boolean, - ) {} + public port: number, + public tempDir: string, + public disposables: AsyncDisposableStack, + ) { + } + + async [Symbol.asyncDispose]() { + if (this.#disposed) return; + this.#disposed = true; + await this.disposables[Symbol.asyncDispose](); + } addCleanup(fn: MetaTestCleanupFn) { - this.cleanups.push(fn); + this.disposables.defer(fn); } get typegate() { @@ -161,35 +181,77 @@ export class MetaTest { } async undeploy(tgName: string) { - await this.typegates.next().register.remove(tgName); + await this.typegates.next().removeTypegraph(tgName); } async engine(path: string, opts: ParseOptions = {}): Promise { - const tgString = await this.serialize(path, opts); - const tgJson = await TypeGraph.parseJson(tgString); - - // for convience, automatically prefix secrets - const secrets = opts.secrets ?? {}; - const { engine, response } = await this.typegate.pushTypegraph( - tgJson, - secrets, - this.introspection, + const oldTypegraphList = await this.typegates.next().register.list(); + + const cmd = ["deploy", "-f", path, "--target", "dev", "--allow-dirty"]; + + cmd.push("--gate", `http://localhost:${this.port}`); + + if (opts.prefix != null) { + cmd.push("--prefix", opts.prefix); + } + + // if (opts.typegraph != null) { + // cmd.push("--typegraph", opts.typegraph); + // } + + for (const [key, value] of Object.entries(opts.secrets ?? {})) { + cmd.push("--secret", `${key}=${value}`); + } + + const { stdout, stderr } = await this.meta(cmd); + + console.log("STDOUT>"); + console.log(stdout); + console.log("STDERR>"); + console.log(stderr); + + const newTypegraphList = await this.typegates.next().register.list(); + + const newTypegraph = newTypegraphList.find((e) => + !oldTypegraphList.includes(e) ); + // what for redeploy? + if (newTypegraph == null) { + throw new Error("No new typegraph"); + } - if (engine == null) { - throw response.failure!; + if (opts.typegraph != null && opts.typegraph != newTypegraph.name) { + throw new Error( + `Expected typegraph ${opts.typegraph}, got ${newTypegraph.name}`, + ); } - return engine; + return newTypegraph; + + // TODO: MET-500 + // const tgString = await this.serialize(path, opts); + // const tgJson = await TypeGraph.parseJson(tgString); + // + // // for convience, automatically prefix secrets + // const secrets = opts.secrets ?? {}; + // const { engine, response } = await this.typegates.next().pushTypegraph( + // tgJson, + // secrets, + // this.introspection, + // ); + // + // if (engine == null) { + // throw response.failure!; + // } + // + // return engine; } async engineFromDeployed(tgString: string): Promise { const tg = await TypeGraph.parseJson(tgString); - const { engine, response } = await this.typegates.next().pushTypegraph( - tg, - {}, - this.introspection, - ); + const { engine, response } = await this.typegates + .next() + .pushTypegraph(tg, {}, this.introspection); if (engine == null) { throw response.failure!; @@ -225,12 +287,6 @@ export class MetaTest { ): Promise { // run self deployed typegraph - if (!this.port) { - throw new Error( - "Error: port option in MetaTest config should be set to 'true'", - ); - } - const { stderr, stdout } = await this.shell([ lang.toString(), path, @@ -262,16 +318,11 @@ export class MetaTest { .filter((e) => e == engine) .map((e) => { typegate.register.remove(e.name); - return e.terminate(); + return e[Symbol.asyncDispose](); }), ); } - async terminate() { - await Promise.all(this.cleanups.map((c) => c())); - await this.typegates.terminate(); - } - async should( fact: string, fn: (t: Deno.TestContext) => void | Promise, @@ -350,10 +401,7 @@ interface TempGitRepo { } interface TestConfig { - systemTypegraphs?: boolean; introspection?: boolean; - // port on which the typegate instance will be exposed on expose the typegate instance - port?: boolean; // number of typegate instances to create replicas?: number; // create a temporary clean git repo for the tests @@ -365,7 +413,7 @@ interface TestConfig { interface Test { ( - opts: string | Omit & TestConfig, + opts: string | (Omit & TestConfig), fn: (t: MetaTest) => void | Promise, ): void; } @@ -375,6 +423,14 @@ interface TestExt extends Test { ignore: Test; } +let currentTest: MetaTest | null = null; +export function getCurrentTest(): MetaTest { + if (currentTest == null) { + throw new Error("No current test"); + } + return currentTest; +} + export const test = ((o, fn): void => { const opts = typeof o === "string" ? { name: o } : o; return Deno.test({ @@ -392,30 +448,48 @@ export const test = ((o, fn): void => { ); } - const typegates = await Promise.all( + const tempDir = await Deno.makeTempDir({ + prefix: "typegate-test-", + dir: config.tmp_dir, + }); + + // TODO different tempDir for each typegate instance + const result = await Promise.allSettled( Array.from({ length: replicas }).map((_) => - Typegate.init(opts.syncConfig ?? null) + Typegate.init(opts.syncConfig ?? null, null, tempDir) ), ); + const typegates = result.map((r) => { + if (r.status === "fulfilled") { + return r.value; + } else { + throw r.reason; + } + }); const { - systemTypegraphs = false, gitRepo = null, introspection = false, } = opts; - if (systemTypegraphs) { - await Promise.all( - typegates.map((typegate) => SystemTypegraph.loadAll(typegate)), - ); - } + await Promise.all( + typegates.map((typegate) => SystemTypegraph.loadAll(typegate)), + ); - const mt = await MetaTest.init( + await using mt = await MetaTest.init( t, new TypegateManager(typegates), introspection, - opts.port != null, + tempDir, ); + mt.disposables.defer(async () => { + await Deno.remove(tempDir, { recursive: true }); + }); + + if (opts.teardown != null) { + mt.disposables.defer(opts.teardown); + } + try { if (gitRepo != null) { const dir = await newTempDir(); @@ -426,7 +500,6 @@ export const test = ((o, fn): void => { await Deno.mkdir(dirname(destPath), { recursive: true }); await Deno.copyFile(join(testDir, srcPath), destPath); } - console.log(dir); const sh = (args: string[], options?: ShellOptions) => { return shell(args, { currentDir: dir!, ...options }); @@ -441,16 +514,11 @@ export const test = ((o, fn): void => { await sh(["git", "commit", "-m", "Initial commit"]); } + currentTest = mt; await fn(mt); + currentTest = null; } catch (error) { - console.error(error); throw error; - } finally { - await mt.terminate(); - } - - if (opts.teardown != null) { - await opts.teardown(); } }, ...opts, diff --git a/typegraph/python/typegraph/graph/tg_artifact_upload.py b/typegraph/python/typegraph/graph/tg_artifact_upload.py index 48a5f71f95..331bd0f567 100644 --- a/typegraph/python/typegraph/graph/tg_artifact_upload.py +++ b/typegraph/python/typegraph/graph/tg_artifact_upload.py @@ -3,9 +3,11 @@ import json import os +import sys from dataclasses import dataclass from typing import Any, Dict, List, Optional, Union from urllib import request +from urllib.error import HTTPError from typegraph.gen.exports.core import Artifact from typegraph.gen.types import Err, Ok, Result @@ -90,7 +92,15 @@ def __upload( data=content, headers=upload_headers, ) - response = request.urlopen(upload_req) + try: + response = request.urlopen(upload_req) + except HTTPError as e: + errmsg = json.load(e.fp).get("error", None) + + print(f"Failed to upload artifact {path}: {e}", file=sys.stderr) + print(f" - {errmsg}", file=sys.stderr) + print(f" - url={url}", file=sys.stderr) + raise e if response.status != 201: raise Exception(f"Failed to upload artifact {path} {response.status}") diff --git a/website/docs/concepts/architecture/index.mdx b/website/docs/concepts/architecture/index.mdx index d6694305c3..2ee49f6ff5 100644 --- a/website/docs/concepts/architecture/index.mdx +++ b/website/docs/concepts/architecture/index.mdx @@ -5,16 +5,67 @@ sidebar_position: 3 # Architecture -## Artifact Tracking Protocol +## Artifacts -While developing and building an application using `Metatype`, different types of artifacts can be used in different parts of the application. An Artifact is an external file that your application uses as a dependency to do some task. For instance, while working with [PythonRuntime](/docs/reference/runtimes/python), we import an external `python` script `artifacts/hasher.py` which has some hashing functionality. The script which we imported will be marked as an `Artifact` and run inside the `PythonRuntime`. There can be numerous artifacts which can be used by different [Runtimes](/docs/reference/runtimes) inside Metatype. +While developing and building an application using _Metatype_, different types of artifacts can be used in different parts of the application. +An artifact is an external file that your application uses as a dependency to execute tasks. -`Metatype` tracks these artifacts into two modes. -- `Single Replica` mode: which stores the artifacts inside the [Typegate](/docs/reference/typegate) node to which the [Typegraph](/docs/reference/typegraph) was deployed to. The artifacts will be persisited to the local file system of the typegate. -- `Sync` mode: which stores the artifacts in a cloud store. `S3` storage and `Redis` are a dependency in this mode. This will be useful when there are multiple typgate instances are running and they need to synchornize and share artifacts. +For instance, while working with [PythonRuntime](/docs/reference/runtimes/python), we import an external Python script `artifacts/hasher.py` which has some hashing functionality. +The script which we imported will be marked as an artifact and run inside the `PythonRuntime`. +There can be numerous artifacts which can be used by different [Runtimes](/docs/reference/runtimes) inside Metatype. + +The way _Metatype_ tracks depends on the mode in which the typegate is running. +- In the _single-replica_ mode _(default)_, the artifacts are stored in only in the filesystem of the [Typegate](/docs/reference/typegate) node. +- In the [_Synchronized_ mode](/docs/reference/typegate/synchronization), they are stored in a shared S3-compatible object store.
-![](../../../../docs/workflows/artifact_upload_protocol.drawio.svg) +![Artifact upload protocol](../../../../docs/workflows/artifact_upload_protocol.drawio.svg "Artifact upload protocol")
+ + + +### Reference counting + +Artifacts are tracked using reference counts. After a successful upload, each artifact is assigned a zero reference count. + +When a typegraph is deployed for the first time, the reference counts associated +to each of its artifacts are incremented. + +When a typegraph is updated, reference counts are updated according to the diff +of the artifact list between new and old versions of the typegraph (list +of artifact hashes): removal → decrementation, addition → incrementation, no +change → no change. + +When a typegraph is undeployed, the reference count of each artifact associated +to the typegraph is decremented. + +#### Rationale + +Why do we use reference counts instead of removing unused artifacts after each +typegraph update/undeployment? + +- Artifacts are shared between typegraphs: multiple typegraphs from a single + projects might share the same artifacts. +- We might have orphaned artifacts due to uncompleted typegraph process. + +### Artifact removal + +#### GC: Garbage collection + +Artifacts that have a zero reference count are removed. + +**Triggers:** The GC is triggerred at the end of each typegraph +deployment/undeployment process. + +#### Full GC + +**_Not implemented_** + + +A full GC can be triggered manually with the _Meta CLI_ or on the console. +- Remove orphaned artifacts from uncompleted typegraph deployment process or uncompleted GC. +- Recompute all the reference counts, then run the GC. +It might be necessary as the reference count might get wrong due to lack of a +proper transaction support for the typegraph deployment/undeployment process.