Compare commits

...

307 Commits

Author SHA1 Message Date
2ade5aff91 update dependencies 2022-08-23 12:57:16 +06:00
29bf6e80ec start 0.12.4 2022-08-23 12:55:52 +06:00
027e927e1b Merge pull request #181 from InsanusMokrassar/0.12.3
0.12.3
2022-08-22 01:44:17 +06:00
fa061f88e2 refactor 2022-08-22 01:42:04 +06:00
1d01b65b5f fix 2022-08-22 01:26:28 +06:00
2c2b364167 add default unoptimized realization of selectByIds 2022-08-22 01:25:07 +06:00
7c5fc9bf7c selectPaginated and abstract exposed keyvalue(s) repos 2022-08-22 01:11:09 +06:00
193d22ff20 start 0.12.3 2022-08-22 00:02:34 +06:00
0f3b553ba2 Merge pull request #180 from InsanusMokrassar/0.12.2
0.12.2
2022-08-19 16:25:24 +06:00
4eb1013446 Update CHANGELOG.md 2022-08-19 15:54:46 +06:00
be1b2563ec Update libs.versions.toml 2022-08-19 15:54:30 +06:00
1c9c2f1e70 Update libs.versions.toml 2022-08-19 02:00:29 +06:00
52dfded741 start 0.12.2 2022-08-19 01:59:09 +06:00
0a115e5cf4 Merge pull request #179 from InsanusMokrassar/0.12.1
0.12.1
2022-08-11 23:50:12 +06:00
c349af999b Update CHANGELOG.md 2022-08-11 23:36:01 +06:00
9a5709a34d Update libs.versions.toml 2022-08-11 22:52:34 +06:00
28d311160b start 0.12.1 2022-08-11 22:47:56 +06:00
1ca7081a40 Merge pull request #176 from InsanusMokrassar/0.12.0
0.12.0
2022-08-04 13:55:30 +06:00
5ce8ebe82c Merge branch 'master' into 0.12.0 2022-08-04 13:51:12 +06:00
a7a88b29b9 Merge pull request #178 from InsanusMokrassar/0.11.14
0.11.14
2022-08-04 13:48:18 +06:00
cd7b982385 update android version 2022-08-04 12:50:36 +06:00
ee59100075 fix of build 2022-08-04 12:36:23 +06:00
f808ac58ef Delete HmacSHA256.kt 2022-08-04 11:30:15 +06:00
f33ada5396 deprecations handling 2022-08-04 10:59:04 +06:00
984d781f2f fill changelog 2022-08-04 10:14:52 +06:00
dd33e1e8bc change version of kotlin 2022-08-04 10:05:29 +06:00
2950de29e5 fixes 2022-08-04 09:59:05 +06:00
50a8799f9d update serialization 2022-08-04 09:47:29 +06:00
bdb0ce6fc7 Update libs.versions.toml 2022-08-04 09:19:57 +06:00
18ec2bca96 Update libs.versions.toml 2022-08-03 10:09:38 +06:00
937ef48794 Update libs.versions.toml 2022-08-01 21:47:05 +06:00
6331f13e9a actualize changelog 2022-07-26 14:58:28 +06:00
5213a2ff8e add kdocs for pagination 2022-07-26 14:58:14 +06:00
087d7452fd encode by default pages number 2022-07-26 14:54:52 +06:00
703094c924 add several additional constructors for the pagination result 2022-07-26 13:56:47 +06:00
eea645c865 PaginationResult#objectsCount 2022-07-25 13:31:15 +06:00
324832a189 start 0.11.14 2022-07-25 13:14:55 +06:00
d55d735c51 Merge pull request #177 from InsanusMokrassar/0.11.13
0.11.13
2022-07-22 16:34:58 +06:00
e3ff1b9609 fill changelog 2022-07-22 16:31:33 +06:00
70c31966ca Update gradle-wrapper.properties 2022-07-18 14:33:04 +06:00
0e4188882f Update libs.versions.toml 2022-07-18 14:08:23 +06:00
6bf0ce92ba Update libs.versions.toml 2022-07-18 13:51:00 +06:00
d51bdc5086 Update gradle.properties 2022-07-18 13:48:09 +06:00
f04f262cee Update libs.versions.toml 2022-07-18 13:16:56 +06:00
0f172055ef Update gradle.properties 2022-07-18 13:11:02 +06:00
e5dd4363f1 Merge pull request #175 from InsanusMokrassar/0.11.12
0.11.12
2022-07-02 02:50:47 +06:00
a3a48bbaac fixes in file key value repo 2022-07-02 02:28:43 +06:00
5e716fb9a8 start 0.11.12 2022-07-02 02:26:58 +06:00
11a36153cc Merge pull request #174 from InsanusMokrassar/0.11.11
0.11.11: improvements of cache repos
2022-06-30 14:03:29 +06:00
8bee354f04 improvements of cache repos 2022-06-30 13:59:32 +06:00
f7dd2b5ce7 Merge pull request #173 from InsanusMokrassar/0.11.11
0.11.11
2022-06-30 13:41:43 +06:00
8ca10c00bb update dokka workflow 2022-06-30 13:41:12 +06:00
905c7e8eda deprecate hmacSha256 2022-06-30 13:19:37 +06:00
d4c5e849bf deprecate BodyPair 2022-06-30 13:15:19 +06:00
8250a2a021 start 0.11.11 2022-06-30 13:14:06 +06:00
01b3df7b8c Update github_release.gradle 2022-06-30 10:12:54 +06:00
daa6e4aff5 Merge pull request #172 from InsanusMokrassar/0.11.10
0.11.10
2022-06-30 02:56:14 +06:00
23bcb26a58 complete improvements in caches 2022-06-30 02:44:44 +06:00
e55f60c30b rename unlimited kv cache 2022-06-30 00:22:07 +06:00
0d0c16e16d add full cache repos 2022-06-29 23:53:49 +06:00
540d5cce7c start add full repos caches 2022-06-29 19:43:58 +06:00
a548b00979 update repos cache 2022-06-29 19:31:57 +06:00
4b7ca6d565 start 0.11.10 2022-06-29 19:29:38 +06:00
0473fa238c Merge pull request #171 from InsanusMokrassar/0.11.9
0.11.9
2022-06-29 01:56:03 +06:00
cfc7119697 update dependencies 2022-06-28 23:06:09 +06:00
22a6520d3e start 0.11.9 2022-06-28 23:01:32 +06:00
fb25e91191 start 0.12.0 2022-06-28 22:57:21 +06:00
c116b270b6 Merge pull request #170 from InsanusMokrassar/0.11.8
0.11.8
2022-06-28 14:18:11 +06:00
aa2d598689 fixes in FileKeyValueRepo 2022-06-28 14:13:04 +06:00
5ef3bb746b start 0.11.8 2022-06-28 14:12:12 +06:00
037616e271 Merge pull request #169 from InsanusMokrassar/0.11.7
0.11.7
2022-06-28 03:07:54 +06:00
abbea906f1 fixes 2022-06-28 01:46:24 +06:00
9132e216c9 add several extensions for MapperRepo 2022-06-28 01:43:57 +06:00
12a7e3c4af fixes 2022-06-28 01:06:46 +06:00
b40c093917 SimpleMapper, SimpleSuspendableMapper, mappers for CRUDRepo 2022-06-28 00:56:51 +06:00
7ac12455c8 add ReadCRUDRepo mappers 2022-06-28 00:19:25 +06:00
5043eec7a2 start 0.11.7 2022-06-27 23:35:10 +06:00
cf31f53e01 Merge pull request #168 from InsanusMokrassar/0.11.6
0.11.6
2022-06-24 00:19:34 +06:00
cd22d76fa7 fsms fixes 2022-06-23 18:47:59 +06:00
c8759843f7 start 0.11.6 2022-06-23 17:19:07 +06:00
781bbcc012 Merge pull request #167 from InsanusMokrassar/0.11.5
0.11.5
2022-06-23 12:50:31 +06:00
6da29c0686 add note about 0.11.4 2022-06-23 12:49:09 +06:00
fcdb6fc45a start and finish 0.11.5 -.- 2022-06-23 12:48:24 +06:00
e785a99bd7 Merge pull request #166 from InsanusMokrassar/0.11.4
0.11.4
2022-06-22 22:37:30 +06:00
121e513fdd fixes in tempUpload of js part 2022-06-22 22:36:06 +06:00
9cf01ab54f improve compose list state creation 2022-06-22 16:56:51 +06:00
c655107681 renames in compose states creation 2022-06-22 16:52:19 +06:00
91a5af6a9a fix of changelog 2022-06-22 16:10:58 +06:00
86e74c0a6f add extension toMutableListState 2022-06-22 16:09:52 +06:00
d8f01f21a0 start 0.11.4 2022-06-22 16:09:35 +06:00
9fb8626d8c Rewrite crud.yml 2022-06-16 15:51:22 +06:00
1c52e04cdb Merge pull request #164 from InsanusMokrassar/0.11.3
0.11.3
2022-06-14 23:59:49 +06:00
798128256e fixes in websockets 2022-06-14 23:48:10 +06:00
72c2df47fd start 0.11.3 2022-06-14 19:35:17 +06:00
c9c6d4c0c1 Merge pull request #163 from InsanusMokrassar/0.11.2
0.11.2
2022-06-13 23:21:14 +06:00
2f4f9f3003 fixes 2022-06-13 23:20:25 +06:00
22e8f8e5d6 start 0.11.2 2022-06-13 22:19:33 +06:00
04cf8c3d9a Merge pull request #162 from InsanusMokrassar/0.11.1
0.11.1
2022-06-12 16:32:05 +06:00
52198be543 fixes in tempupload 2022-06-12 14:26:41 +06:00
80fd5a489b add pagination type info in crud read repo client 2022-06-11 22:37:32 +06:00
b187043ee1 fixes in kv and kvs of ktor repos 2022-06-10 22:25:16 +06:00
8965752055 start 0.11.1 2022-06-10 22:23:45 +06:00
b796620267 Merge pull request #161 from InsanusMokrassar/0.11.0
0.11.0
2022-06-05 02:54:05 +06:00
62df81bb4e update uuid and partially fill changelog 2022-06-05 00:34:45 +06:00
0a8e0f6178 complete swagger templates 2022-06-04 17:12:05 +06:00
f705020aaa fixes in packages 2022-06-04 16:55:23 +06:00
78bd3b9853 replaces of new API of ktor 2022-06-04 16:52:01 +06:00
992b283597 add kv swagger template 2022-06-04 16:47:22 +06:00
8fc1ff1d59 fixes and filling of cru.yml for swagger 2022-06-04 16:18:48 +06:00
3bbde61f39 renames in crud repos and some little refactoring 2022-06-04 15:15:17 +06:00
8d955c4b9d renames in kevalues repos 2022-06-04 15:00:23 +06:00
18593c530b renames in key value repos 2022-06-04 14:42:16 +06:00
78903cd4eb complete improvements in repos ktor parts 2022-06-04 00:22:36 +06:00
eaa143f7d7 kv rework, fixes in map keyvalue repo, tests 2022-06-03 22:33:26 +06:00
bcb0e42fa2 add new started clients for crud 2022-06-03 17:52:58 +06:00
8eed435302 start improvements in ktor parts 2022-06-03 12:22:51 +06:00
0e4a63057f start 0.11.0 2022-06-02 23:50:06 +06:00
1af5faa440 Merge pull request #159 from InsanusMokrassar/0.10.8
0.10.8
2022-05-29 20:06:14 +06:00
7412217b0c add Element.isOverflow 2022-05-29 10:22:57 +06:00
3e749d75b7 start 0.10.8 2022-05-29 10:19:39 +06:00
846b5c87c9 Merge pull request #158 from InsanusMokrassar/0.10.7
0.10.7
2022-05-28 23:59:54 +06:00
7b5e84f80b doForAll and getForAll not suspend 2022-05-28 22:47:06 +06:00
27822a8a66 start 0.10.7 2022-05-28 18:48:51 +06:00
36f4e7ec37 Merge pull request #157 from InsanusMokrassar/0.10.6
0.10.6
2022-05-28 09:47:10 +06:00
187e84ad65 fill changelog and downgrade coroutines 2022-05-28 09:45:18 +06:00
195fe221c4 Update libs.versions.toml 2022-05-28 00:25:59 +06:00
6f9c19bbf6 fixes in ResizeObserver 2022-05-27 11:39:09 +06:00
65bdab4f7e ResizeObserver 2022-05-27 02:37:58 +06:00
c4d5fcfc22 start 0.10.6 2022-05-27 02:37:14 +06:00
43232afa62 Merge pull request #156 from InsanusMokrassar/0.10.5
0.10.5
2022-05-19 10:44:00 -04:00
18908a01d7 rewrite the way to use onStateHandlingErrorHandler inside of DefaultStatesMachine 2022-05-19 20:42:22 +06:00
b22fbfb3bc StateHandlingErrorHandler 2022-05-19 20:22:34 +06:00
57aaea88b6 Update gradle.properties 2022-05-17 12:38:15 +06:00
140949c5ea Update CHANGELOG.md 2022-05-17 12:32:59 +06:00
639241e0d3 Update libs.versions.toml 2022-05-17 12:31:50 +06:00
e0eb42bc2d update dexcount 2022-05-16 17:38:49 +06:00
7990b21cc5 add SharedPreferencesKeyValueRepo 2022-05-16 17:34:34 +06:00
2ba5c97709 start 0.10.5 2022-05-16 17:33:49 +06:00
33b7c85fc2 Merge pull request #154 from InsanusMokrassar/0.10.4
0.10.4
2022-05-12 17:24:13 +06:00
7f6c02ffdf update serialization 2022-05-12 17:04:53 +06:00
f368616e6f start 0.10.4 2022-05-12 17:03:59 +06:00
dd632f4203 Merge pull request #153 from InsanusMokrassar/0.10.3
0.10.3
2022-05-11 08:42:24 +06:00
a8f3ae501b Update CHANGELOG.md 2022-05-11 08:34:44 +06:00
ea76963ac2 Update libs.versions.toml 2022-05-11 08:33:55 +06:00
99dfa97958 Merge pull request #152 from InsanusMokrassar/0.10.3
0.10.3
2022-05-11 02:49:35 +06:00
f68270a5b3 fixes in AccumulatorFlow 2022-05-11 02:38:48 +06:00
542ed81034 start 0.10.3 2022-05-11 01:18:48 +06:00
404a11f5e7 Merge pull request #151 from InsanusMokrassar/0.10.2
0.10.2
2022-05-10 23:44:39 +06:00
411221070e Update CHANGELOG.md 2022-05-10 23:44:13 +06:00
25d35d0c76 Update compose 2022-05-10 15:03:04 +06:00
c72904d61c start 0.10.2 2022-05-10 15:00:56 +06:00
b94c9acd26 Merge pull request #150 from InsanusMokrassar/0.10.1
0.10.1
2022-04-29 23:20:27 +06:00
bdb4988569 fixes 2022-04-29 19:46:15 +06:00
1dafd1352a hotfix 2022-04-29 18:47:23 +06:00
31fbd3cad7 hmacSha256 and hex 2022-04-29 18:42:59 +06:00
848bc5ec10 Update gradle.properties 2022-04-28 23:45:57 +06:00
4d0ad826a0 Update libs.versions.toml 2022-04-28 23:32:35 +06:00
6955820fcc Merge pull request #149 from InsanusMokrassar/0.10.0
0.10.0
2022-04-28 20:30:52 +06:00
ef530624b9 remove redundant pluginManagement 2022-04-28 20:30:08 +06:00
9b9e7dd88f fixes in websockets 2022-04-28 19:43:52 +06:00
a13cc9e961 add error propagation for websockets when websockets has not been installed 2022-04-28 18:40:18 +06:00
0d2b923378 fix signature of createStandardWebsocketFlow 2022-04-28 18:16:14 +06:00
fba84c8ac8 optimize imports in ktor server 2022-04-27 18:30:28 +06:00
db10fe1b2c several fixes 2022-04-27 18:24:51 +06:00
175dd980f8 several warnings fixes 2022-04-27 15:15:03 +06:00
8364020671 migration onto new ktor and other dependencies 2022-04-27 14:39:21 +06:00
eba44cd394 update dependencies 2022-04-26 13:36:55 +06:00
b3bac8015a start 0.10.0 2022-04-26 13:35:00 +06:00
0b48afd251 Merge pull request #147 from InsanusMokrassar/0.9.24
0.9.24
2022-04-21 17:20:45 +06:00
19857930a4 small refactor in actual MPPFileInput 2022-04-21 16:45:44 +06:00
d0dbe3ed2f MPPFile#input 2022-04-21 16:12:58 +06:00
8b7e78b63a start 0.9.24 2022-04-21 16:12:21 +06:00
92a4ecb523 Merge pull request #146 from InsanusMokrassar/0.9.23
0.9.23
2022-04-20 23:46:06 +06:00
6a5ad4d728 Update CHANGELOG.md 2022-04-20 23:41:59 +06:00
be4aa8daac fixes in inheritance of interfaces 2022-04-20 23:14:06 +06:00
b5eac37782 improvements in ExposedCRUDRepo 2022-04-20 18:41:42 +06:00
b1ad3c5a39 start 0.9.23 2022-04-20 18:22:21 +06:00
ba16bad029 Merge pull request #145 from InsanusMokrassar/0.9.22
0.9.22
2022-04-19 13:55:13 +06:00
ca8ae4cd72 improve ktor server part 2022-04-19 11:16:04 +06:00
53d35d74b3 start 0.9.22 2022-04-19 11:14:50 +06:00
49c139e235 Merge pull request #144 from InsanusMokrassar/0.9.21
0.9.21
2022-04-17 08:56:59 +06:00
caf9c821f3 fixes in AbstractExposedWriteCRUDRepo 2022-04-15 00:49:32 +06:00
ca4c6db96f start 0.9.21 2022-04-15 00:44:28 +06:00
6b2298c752 Merge pull request #143 from InsanusMokrassar/0.9.20
0.9.20
2022-04-13 16:02:12 +06:00
a1bf43def9 upfixes 2022-04-13 16:01:13 +06:00
15e9254e00 fixes in OneToManyAndroidRepo and adding of CursorIterator 2022-04-13 15:54:48 +06:00
afe5a72c6f small fix of docs 2022-04-12 15:32:25 +06:00
750a8b9ecf start 0.9.20 2022-04-12 15:13:25 +06:00
27fc3f93e0 Merge pull request #142 from InsanusMokrassar/0.9.19
0.9.19
2022-04-08 01:16:13 +06:00
8166d4b99b Update CHANGELOG.md 2022-04-08 01:16:01 +06:00
b61d2ae2eb fixes in versions repo 2022-04-07 15:41:08 +06:00
4790fe0aea get back either serializer fun 2022-04-07 09:59:58 +06:00
bc37b11cee downgrade kotlin 2022-04-07 09:56:23 +06:00
223fed910f fix for column name of table version in exposed standard versions repo proxy 2022-04-07 00:43:55 +06:00
b85ab7b061 update coroutines 2022-04-06 11:47:06 +06:00
888dc299c9 update kotlin 2022-04-04 10:06:57 +06:00
e113dc28ed start 0.9.19 2022-04-04 10:05:56 +06:00
31e55d2307 Merge pull request #141 from InsanusMokrassar/0.9.18
0.9.18
2022-03-31 20:38:43 +06:00
e90645f248 Element#onActionOutside 2022-03-31 14:48:15 +06:00
4bb7ba2571 start 0.9.18 2022-03-31 14:31:04 +06:00
8d31c25bf8 Merge pull request #140 from InsanusMokrassar/0.9.17
0.9.17
2022-03-27 17:47:11 +06:00
c7ee1c28b2 fix of docs for DefaultStatesManager 2022-03-27 17:43:01 +06:00
99b09c8b28 add conflicts resolver for default states managers 2022-03-27 17:15:23 +06:00
a328c4425a Update CHANGELOG.md 2022-03-27 13:57:08 +06:00
c0f61ca896 Update libs.versions.toml 2022-03-26 19:11:44 +06:00
86e70c0961 js visibility of objects extensions 2022-03-26 11:01:40 +06:00
d87a3a039f start 0.9.17 2022-03-26 10:57:50 +06:00
6279a2c40a Merge pull request #139 from InsanusMokrassar/0.9.16
0.9.16
2022-03-18 00:10:25 +06:00
f377ebea88 add renderComposableAndLinkToRoot 2022-03-17 19:16:31 +06:00
7373fef964 add js onRemoved for nodes and different extensions for Composition 2022-03-17 14:56:14 +06:00
41ef86dbda start 0.9.16 2022-03-17 14:56:14 +06:00
7bc2b2336d Merge pull request #138 from InsanusMokrassar/0.9.15
0.9.15
2022-03-16 19:47:28 +06:00
0a615e6d78 cancel jobs of completed states in DefaultStatesMachine 2022-03-16 19:35:14 +06:00
8f928e16e1 FSM updates 2022-03-16 18:40:21 +06:00
72bc8da1e7 start 0.9.15 2022-03-16 17:59:29 +06:00
51e349a5db Merge pull request #137 from InsanusMokrassar/0.9.14
0.9.14 (upfixes)
2022-03-15 16:32:24 +06:00
3cbb19ba2c fixes and versions updates 2022-03-15 16:30:32 +06:00
ae3a5bf45d Merge pull request #136 from InsanusMokrassar/0.9.14
0.9.14
2022-03-15 16:22:35 +06:00
9c667f4b78 several fixes in actual temporal upload of js client 2022-03-15 15:37:41 +06:00
21195e1bcb temporal files uploading functionality 2022-03-15 15:33:45 +06:00
03117ac565 start 0.9.14 2022-03-15 15:31:58 +06:00
d13fbdf176 Merge pull request #135 from InsanusMokrassar/0.9.13
0.9.13
2022-03-13 17:14:23 +06:00
7cecc0e0b6 Update CHANGELOG.md 2022-03-13 14:08:01 +06:00
203e781f5d Update libs.versions.toml 2022-03-13 14:06:16 +06:00
3eb6cd77cd start 0.9.13 2022-03-13 14:05:24 +06:00
51855b2405 Merge pull request #134 from InsanusMokrassar/0.9.12
0.9.12
2022-03-12 01:46:23 +06:00
00cc26d874 fix in publication scripts 2022-03-12 01:38:13 +06:00
02520636ad hotfix in selectFile 2022-03-12 01:28:53 +06:00
76813fae8e hotfix in selectFile 2022-03-12 01:28:41 +06:00
4693483c2b rename selectFile with result MPPFile to selectFileOrThrow 2022-03-12 01:12:17 +06:00
6dbd12df59 fill changelog and rename selectOptionalFile to selectFileOrNull 2022-03-12 01:03:41 +06:00
9e84dc5031 add a set of tools for JS and Web Compose 2022-03-12 00:55:03 +06:00
8f790360bc start 0.9.12 2022-03-12 00:34:22 +06:00
808375cea6 Merge pull request #133 from InsanusMokrassar/0.9.11
0.9.11
2022-03-11 13:33:49 +06:00
d9c15db9de add compose submodule and add several functions in top of composition 2022-03-10 17:47:47 +06:00
cf2be8ed43 migration onto toml 2022-03-10 17:04:05 +06:00
97339c9b1d update wrapper version 2022-03-10 15:55:45 +06:00
eb35903de9 Update CHANGELOG.md 2022-03-10 15:20:42 +06:00
43e88e00da Update klock 2022-03-08 10:01:53 +06:00
1e51f9d77b Update CHANGELOG.md 2022-03-08 10:01:02 +06:00
dad738357c start 0.9.11 2022-03-08 10:00:19 +06:00
cb828ab3f2 Merge pull request #132 from InsanusMokrassar/0.9.10
0.9.10
2022-03-06 16:24:51 +06:00
aefaf4a8cc update klock (again) 2022-03-06 16:08:02 +06:00
b29f37a251 update klock 2022-03-04 20:20:17 +06:00
88854020ac extend functionality of websockets 2022-03-04 20:16:59 +06:00
14ebe01fc6 improvements in includeWebsocketHandling 2022-03-04 15:31:32 +06:00
771aed0f0f UnifiedRequester#createStandardWebsocketFlow(String,DeserializationStrategy) 2022-03-04 15:20:16 +06:00
16c57fcd6a start 0.9.10 2022-03-04 15:17:36 +06:00
75397a7ccb Merge pull request #131 from InsanusMokrassar/0.9.9
0.9.9
2022-02-24 14:44:13 +06:00
b05404f828 IntersectionObserver 2022-02-24 00:26:07 +06:00
edea942874 small refactor of operatons in applyDiff 2022-02-23 23:02:31 +06:00
8a8f568b9a add applyDiff in Diff Utils and update korlibs klock version 2022-02-23 22:55:56 +06:00
4dc8d30c52 start 0.9.9 2022-02-23 22:32:05 +06:00
cb4e08e823 Merge pull request #130 from InsanusMokrassar/0.9.8
0.9.8
2022-02-19 21:44:56 +06:00
c443bf4fa0 update dependencies 2022-02-19 16:15:36 +06:00
a6982de822 start 0.9.8 2022-02-19 16:06:45 +06:00
4f1a663e75 fixes in ExposedOneToManyKeyValueRepo and includeWebsocketHandling 2022-02-19 16:06:33 +06:00
dab262d626 start 0.9.7 2022-02-19 16:06:33 +06:00
87a3f61ca6 fix in ExposedOneToManyKeyValueRepo 2022-02-07 20:43:43 +06:00
506e937a68 start 0.9.6 2022-02-07 20:39:44 +06:00
5a037c76dd Merge pull request #126 from InsanusMokrassar/0.9.5
0.9.5
2022-02-01 23:30:26 +06:00
313f622f7e Update CHANGELOG.md 2022-02-01 14:27:07 +06:00
6cba1fe1a2 Update klock 2022-02-01 14:25:24 +06:00
fd2d0e80b7 start 0.9.5 2022-02-01 14:24:12 +06:00
96ab2e8aca Merge pull request #125 from InsanusMokrassar/0.9.4
0.9.4
2022-01-18 18:58:49 +06:00
0202988cae several fixes in optionallyReverse 2022-01-17 16:11:55 +06:00
d619d59947 Either updates 2022-01-17 15:56:48 +06:00
85b3e48d18 add optionallyReverse extensions 2022-01-17 15:38:23 +06:00
7a9b7d98a1 start 0.9.4 2022-01-14 13:22:04 +06:00
b212acfcaf Merge pull request #124 from InsanusMokrassar/0.9.3
0.9.3
2022-01-14 00:46:20 +06:00
3a45e5dc70 Update CHANGELOG.md 2022-01-14 00:37:03 +06:00
73190518d5 Update gradle.properties 2022-01-14 00:35:51 +06:00
03f78180dc Merge pull request #123 from InsanusMokrassar/0.9.2
0.9.2
2022-01-13 11:22:54 +06:00
1c0b8cf842 Update CHANGELOG.md 2022-01-13 10:20:35 +06:00
a1624ea2a9 Update gradle.properties 2022-01-13 10:19:08 +06:00
23a050cf1e Merge pull request #122 from InsanusMokrassar/0.9.1
0.9.1
2022-01-08 16:12:04 +06:00
916f2f96f4 typealiases for exposed one to many 2022-01-08 14:35:28 +06:00
00cc214754 repo exposed updates 2022-01-08 14:14:44 +06:00
b2e38f72b9 start 0.9.1 2022-01-08 14:12:57 +06:00
e7107d238d Update dokka_push.yml 2021-12-30 02:55:19 +06:00
ed9ebdbd1a Merge pull request #121 from InsanusMokrassar/0.9.0
0.9.0
2021-12-30 02:52:34 +06:00
e80676d3d2 Update packages_push.yml 2021-12-29 19:43:19 +06:00
02d02fa8f2 update android tools build 2021-12-29 19:24:25 +06:00
bd783fb74f update dokka 2021-12-29 17:44:45 +06:00
50386adf70 ignore kotlin-js-store folder 2021-12-28 21:19:47 +06:00
f4ee6c2890 update exposed and adapt to new version of kotlin serialization 2021-12-28 21:17:17 +06:00
d45aef9fe5 start 0.9.0 2021-12-28 09:58:59 +06:00
a56cd3dddd Merge pull request #120 from InsanusMokrassar/0.8.9
0.8.9
2021-12-27 17:02:16 +06:00
419e7070ee more fixes to god of fixes 2021-12-27 17:02:00 +06:00
612cf40b5f small hotfix 2021-12-27 16:00:07 +06:00
8b39882e83 fixes in DefaultUpdatableStatesMachine 2021-12-27 15:57:55 +06:00
e639ae172b Fixes in uniloadMultipart 2021-12-27 15:55:05 +06:00
d0446850ae start 0.8.9 2021-12-27 15:45:44 +06:00
c48465b90b Merge pull request #119 from InsanusMokrassar/0.8.8
0.8.8
2021-12-26 22:11:04 +06:00
f419fd03d2 small fix of performing state update for UpdatableStatesMachine 2021-12-26 22:00:26 +06:00
494812a660 one more fix 2021-12-26 21:25:12 +06:00
eb78f21eec fix FSMBuilder 2021-12-26 21:21:40 +06:00
4bda70268b small fix of style in DefaultUpdatableStatesMachine 2021-12-26 21:07:40 +06:00
f037ce4371 updates in FSM 2021-12-26 21:06:26 +06:00
3d2196e35d update dependencies (which possible) 2021-12-26 20:07:13 +06:00
a74f061b02 start 0.8.8 2021-12-26 20:02:18 +06:00
11ade14676 Merge pull request #118 from InsanusMokrassar/0.8.7
0.8.7
2021-12-15 01:22:42 +06:00
eb562d8784 add stream using for multipart 2021-12-14 22:03:29 +06:00
1ee5b4bfd4 hotfix for multipart 2021-12-14 21:13:11 +06:00
d97892080b add preview work with multipart 2021-12-14 18:01:41 +06:00
6f37125724 start 0.8.7 and make UnifiedRequester/UnifiedRouter fields are public 2021-12-14 13:25:58 +06:00
ed1baaade7 Merge pull request #117 from InsanusMokrassar/0.8.6
0.8.6
2021-12-08 15:57:43 +06:00
212 changed files with 6931 additions and 1436 deletions

28
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 11
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
rm gradle.properties
mv gradle.properties.tmp gradle.properties
- name: Build
run: ./gradlew build
# - name: Publish
# continue-on-error: true
# run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository
# env:
# GITHUBPACKAGES_USER: ${{ github.actor }}
# GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -10,12 +10,12 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Fix android 31.0.0 dx
java-version: 11
- name: Fix android 32.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
run: cd /usr/local/lib/android/sdk/build-tools/32.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Build
run: ./gradlew dokkaHtml
run: ./gradlew build && ./gradlew dokkaHtml
- name: Publish KDocs
uses: peaceiris/actions-gh-pages@v3
with:

View File

@@ -1,28 +0,0 @@
name: Publish package to GitHub Packages
on: [push]
jobs:
publishing:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Fix android 31.0.0 dx
continue-on-error: true
run: cd /usr/local/lib/android/sdk/build-tools/31.0.0/ && mv d8 dx && cd lib && mv d8.jar dx.jar
- name: Rewrite version
run: |
branch="`echo "${{ github.ref }}" | grep -o "[^/]*$"`"
cat gradle.properties | sed -e "s/^version=\([0-9\.]*\)/version=\1-branch_$branch-build${{ github.run_number }}/" > gradle.properties.tmp
rm gradle.properties
mv gradle.properties.tmp gradle.properties
- name: Build
run: ./gradlew build
- name: Publish
continue-on-error: true
run: ./gradlew --no-parallel publishAllPublicationsToGithubPackagesRepository -x signJsPublication -x signJvmPublication -x signKotlinMultiplatformPublication -x signAndroidDebugPublication -x signAndroidReleasePublication -x signKotlinMultiplatformPublication
env:
GITHUBPACKAGES_USER: ${{ github.actor }}
GITHUBPACKAGES_PASSWORD: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -11,5 +11,6 @@ out/
secret.gradle
local.properties
kotlin-js-store
publishing.sh

View File

@@ -1,5 +1,459 @@
# Changelog
## 0.12.4
* `Versions`:
* `Kotlin`: `1.7.0` -> `1.7.10`
* `Compose`: `1.2.0-alpha01-dev755` -> `1.2.0-alpha01-dev764`
## 0.12.3
* `Repos`:
* `Exposed`:
* Add abstract exposed variants of `KeyValue` and `KeyValues` repos
* Add new extension `Query#selectPaginated`
## 0.12.2
* `Versions`:
* `Serialization`: `1.4.0-RC` -> `1.4.0`
* `Compose`: `1.2.0-alpha01-dev753` -> `1.2.0-alpha01-dev755`
## 0.12.1
* `Versions`:
* `Ktor`: `2.0.3` -> `2.1.0`
## 0.12.0
**OLD DEPRECATIONS HAVE BEEN REMOVED**
**MINIMAL ANDROID API HAS BEEN ENLARGED UP TO API 21 (Android 5.0)**
* `Versions`
* `Kotlin`: `1.6.21` -> `1.7.0`
* `Coroutines`: `1.6.3` -> `1.6.4`
* `Exposed`: `0.38.2` -> `0.39.2`
* `Compose`: `1.2.0-alpha01-dev729` -> `1.2.0-alpha01-dev753`
* `Klock`: `2.7.0` -> `3.0.0`
* `uuid`: `0.4.1` -> `0.5.0`
* `Android Core KTX`: `1.7.0` -> `1.8.0`
* `Android AppCompat`: `1.4.1` -> `1.4.2`
* `Ktor`:
* All previously standard functions related to work with binary data by default have been deprecated
## 0.11.14
* `Pagination`:
* `PaginationResult` got new field `objectsNumber` which by default is a times between `pagesNumber` and `size`
## 0.11.13
* `Versions`:
* `Coroutines`: `1.6.3` -> `1.6.4`
* `Compose`: `1.2.0-alpha01-dev629` -> `1.2.0-alpha01-dev731`
## 0.11.12
* `Repos`:
* `Common`:
* `JVM`:
* Fixes in `ReadFileKeyValueRepo` methods (`values`/`keys`)
## 0.11.11
* `Crypto`:
* `hmacSha256` has been deprecated
* `Ktor`:
* `Client`:
* `BodyPair` has been deprecated
* `Repos`:
* `Cache`:
* New interface `CacheRepo`
* New interface `FullCacheRepo`
* `actualize*` methods inside of full cache repos now open for overriding
## 0.11.10
* `Repos`:
* `Cache`:
* `KVCache` has been replaced to the package `dev.inmo.micro_utils.repos.cache`
* `SimpleKVCache` has been replaced to the package `dev.inmo.micro_utils.repos.cache`
* New `KVCache` subtype - `FullKVCache`
* Add `Full*` variants of standard repos
* Add `cached`/`caching` (for write repos) extensions for all standard types of repos
## 0.11.9
* `Versions`
* `Coroutines`: `1.6.1` -> `1.6.3`
* `Ktor`: `2.0.2` -> `2.0.3`
* `Compose`: `1.2.0-alpha01-dev686` -> `1.2.0-alpha01-dev729`
## 0.11.8
* `Repos`:
* `Common`:
* Fixes in `FileKeyValueRepo`
## 0.11.7
* `Common`:
* New abstractions `SimpleMapper` and `SimpleSuspendableMapper`
* `Repos`:
* `Common`:
* Add mappers for `CRUDRepo`
## 0.11.6
* `FSM`:
* `Common`
* Several fixes related to the jobs handling
## 0.11.5
* `Coroutines`:
* `Compose`:
* Add extension `StateFlow#asMutableComposeListState` and `StateFlow#asComposeList`
* Add extension `StateFlow#asMutableComposeState`/`StateFlow#asComposeState`
## 0.11.4
**THIS VERSION HAS BEEN BROKEN, DO NOT USE IT**
## 0.11.3
* `Ktor`:
* Support of `WebSockets` has been improved
* `Client`:
* New extensions: `HttpClient#openBaseWebSocketFlow`, `HttpClient#openWebSocketFlow`, `HttpClient#openSecureWebSocketFlow`
## 0.11.2
* `Ktor`:
* Support of `WebSockets` has been improved and added fixes inside of clients
## 0.11.1
* `Repos`
* `Ktor`
* In `configureReadKeyValueRepoRoutes` and `configureReadKeyValuesRepoRoutes` configurators fixed requiring of `reversed` property
## 0.11.0
* `Versions`
* `UUID`: `0.4.0` -> `0.4.1`
* `Ktor`
* `Client`:
* New extension fun `HttpResponse#throwOnUnsuccess`
* All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind
* `Server`:
* All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind
* `Repos`
* `Ktor`:
* Fully rewritten work with all declared repositories
* All old functions, classes and extensions has been rewritten with new ktor-way with types info and keeping `ContentNegotiation` in mind
## 0.10.8
* `Common`
* Add `Element.isOverflow*` extension properties
## 0.10.7
* `Pagination`:
* Now it is possible to use `doForAll*` and `getForAll` functions in non suspend places
## 0.10.6
* `Versions`
* `Ktor`: `2.0.1` -> `2.0.2`
* `Common`
* `JS`:
* Add `ResizeObserver` functionality
## 0.10.5
* `Versions`
* `Compose`: `1.2.0-alpha01-dev683` -> `1.2.0-alpha01-dev686`
* `Repos`
* `Android`:
* New function `SharedPreferencesKeyValueRepo`
* `FSM`
* Add `StateHandlingErrorHandler` and opportunity to handle states handling errors
## 0.10.4
* `Versions`:
* `Serialization`: `1.3.2` -> `1.3.3`
## 0.10.3
* `Versions`:
* `Compose`: `1.2.0-alpha01-dev682` -> `1.2.0-alpha01-dev683`
* `Coroutines`:
* Fixes in `AccumulatorFlow`
## 0.10.2
* `Versions`:
* `Compose`: `1.2.0-alpha01-dev675` -> `1.2.0-alpha01-dev682`
## 0.10.1
* `Versions`:
* `Ktor`: `2.0.0` -> `2.0.1`
* `Crypto`:
* Add `hmacSha256`
* Add `hex`
## 0.10.0
* `Versions`:
* `Kotlin`: `1.6.10` -> `1.6.21`
* `Compose`: `1.1.1` -> `1.2.0-alpha01-dev675`
* `Exposed`: `0.37.3` -> `0.38.2`
* `Ktor`: `1.6.8` -> `2.0.0`
* `Dokka`: `1.6.10` -> `1.6.21`
## 0.9.24
* `Ktor`:
* `Common`:
* New extension fun `MPPFile#input`
## 0.9.23
* `Repos`:
* `Exposed`:
* New property `ExposedRepo#selectAll` to retrieve all the rows in the table
## 0.9.22
* `Ktor`:
* `Server`:
* Now `createKtorServer` fun is fully customizable
## 0.9.21
* `Repos`:
* `Exposed`:
* fixes in `AbstractExposedWriteCRUDRepo`
## 0.9.20
* `Repos`:
* `Common`:
* Fixes in `OneToManyAndroidRepo`
* New `CursorIterator`
## 0.9.19
* `Versions`:
* `Coroutines`: `1.6.0` -> `1.6.1`
* `Repos`:
* `Exposed`:
* Fixes in `ExposedStandardVersionsRepoProxy`
## 0.9.18
* `Common`
* New extensions for `Element`: `Element#onActionOutside` and `Element#onClickOutside`
## 0.9.17
* `Common`:
* New extensions `Element#onVisibilityChanged`, `Element#onVisible` and `Element#onInvisible`
* `Coroutines`:
* New extension `Element.visibilityFlow()`
* `FSM`:
* Now it is possible to resolve conflicts on `startChain`
## 0.9.16
* `Versions`:
* `Klock`: `2.6.3` -> `2.7.0`
* `Common`:
* New extension `Node#onRemoved`
* `Compose`:
* New extension `Composition#linkWithRoot` for removing of composition with root element
* `Coroutines`:
* `Compose`:
* New function `renderComposableAndLinkToContextAndRoot` with linking of composition to root element
## 0.9.15
* `FSM`:
* Rename `DefaultUpdatableStatesMachine#compare` to `DefaultUpdatableStatesMachine#shouldReplaceJob`
* `DefaultStatesManager` now is extendable
* `DefaultStatesMachine` will stop all jobs of states which was removed from `statesManager`
## 0.9.14
* `Versions`:
* `Klock`: `2.6.2` -> `2.6.3`
* `Ktor`: `1.6.7` -> `1.6.8`
* `Ktor`:
* Add temporal files uploading functionality (for clients to upload and for server to receive)
## 0.9.13
* `Versions`:
* `Compose`: `1.1.0` -> `1.1.1`
## 0.9.12
* `Common`:
* `JS`:
* New function `openLink`
* New function `selectFile`
* New function `triggerDownloadFile`
* `Compose`:
* Created :)
* `Common`:
* `DefaultDisposableEffectResult` as a default realization of `DisposableEffectResult`
* `JS`:
* `openLink` on top of `openLink` with `String` target from common
* `Coroutines`:
* `Compose`:
* `Common`:
* New extension `Flow.toMutableState`
* New extension `StateFlow.toMutableState`
* `JS`:
* New function `selectFileOrThrow` on top of `selectFile` from `common`
* New function `selectFileOrNull` on top of `selectFile` from `common`
## 0.9.11
* `Versions`:
* `Klock`: `2.6.1` -> `2.6.2`
* `Coroutines`:
* `Compose`:
* Created :)
* New extensions and function:
* `Composition#linkWithJob`
* `Composition#linkWithContext`
* `renderComposableAndLinkToContext`
## 0.9.10
* `Versions`:
* `Klock`: `2.5.2` -> `2.6.1`
* Ktor:
* Client:
* New function `UnifiedRequester#createStandardWebsocketFlow` without `checkReconnection` arg
* Server:
* Now it is possible to filter data in `Route#includeWebsocketHandling`
* Callback in `Route#includeWebsocketHandling` and dependent methods is `suspend` since now
* Add `URLProtocol` support in `Route#includeWebsocketHandling` and dependent methods
## 0.9.9
* `Versions`:
* `Klock`: `2.5.1` -> `2.5.2`
* `Common`:
* Add new diff tool - `applyDiff`
* Implementation of `IntersectionObserver` in JS part (copypaste of [this](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0) comment)
## 0.9.8
* `Versions`:
* `Exposed`: `0.37.2` -> `0.37.3`
* `Klock`: `2.4.13` -> `2.5.1`
* `AppCompat`: `1.4.0` -> `1.4.1`
## 0.9.7
* `Repos`:
* `Exposed`:
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `insertIgnore`
* `Ktor`:
* `Server`:
* `Route#includeWebsocketHandling` now will check that `WebSockets` feature and install it if not
## 0.9.6
* `Repos`:
* `Exposed`:
* Fix in `ExposedOneToManyKeyValueRepo` - now it will not use `deleteIgnoreWhere`
## 0.9.5
* `Versions`:
* `Klock`: `2.4.12` -> `2.4.13`
## 0.9.4
* `Pagination`:
* `Common`:
* Add several `optionallyReverse` functions
* `Common`:
* Changes in `Either`:
* Now `Either` uses `optionalT1` and `optionalT2` as main properties
* `Either#t1` and `Either#t2` are deprecated
* New extensions `Either#mapOnFirst` and `Either#mapOnSecond`
## 0.9.3
* `Versions`:
* `UUID`: `0.3.1` -> `0.4.0`
## 0.9.2
* `Versions`:
* `Klock`: `2.4.10` -> `2.4.12`
## 0.9.1
* `Repos`:
* `Exposed`:
* Default realizations of standard interfaces for exposed DB are using public fields for now:
* `ExposedReadKeyValueRepo`
* `ExposedReadOneToManyKeyValueRepo`
* `ExposedStandardVersionsRepoProxy`
* New typealiases for one to many exposed realizations:
* `ExposedReadKeyValuesRepo`
* `ExposedKeyValuesRepo`
## 0.9.0
* `Versions`:
* `Kotlin`: `1.5.31` -> `1.6.10`
* `Coroutines`: `1.5.2` -> `1.6.0`
* `Serialization`: `1.3.1` -> `1.3.2`
* `Exposed`: `0.36.2` -> `0.37.2`
* `Ktor`: `1.6.5` -> `1.6.7`
* `Klock`: `2.4.8` -> `2.4.10`
## 0.8.9
* `Ktor`:
* `Server`:
* Fixes in `uniloadMultipart`
* `Client`:
* Fixes in `unimultipart`
* `FSM`:
* Fixes in `DefaultUpdatableStatesMachine`
## 0.8.8
* `Versions`:
* `AppCompat`: `1.3.1` -> `1.4.0`
* Android Compile SDK: `31.0.0` -> `32.0.0`
* `FSM`:
* `DefaultStatesMachine` now is extendable
* New type `UpdatableStatesMachine` with default realization`DefaultUpdatableStatesMachine`
## 0.8.7
* `Ktor`:
* `Client`:
* `UnifiedRequester` now have no private fields
* Add preview work with multipart
* `Server`
* `UnifiedRouter` now have no private fields
* Add preview work with multipart
## 0.8.6
* `Common`:

View File

@@ -10,7 +10,7 @@ kotlin {
sourceSets {
androidMain {
dependencies {
api "androidx.appcompat:appcompat-resources:$appcompat_version"
api libs.android.appCompat.resources
}
}
}

View File

@@ -10,13 +10,13 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
api libs.kt.coroutines
api project(":micro_utils.common")
}
}
androidMain {
dependencies {
api "androidx.recyclerview:recyclerview:$androidx_recycler_version"
api libs.android.recyclerView
}
}
}

View File

@@ -7,12 +7,12 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.3'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath "com.getkeepsafe.dexcount:dexcount-gradle-plugin:$dexcount_version"
classpath "com.github.breadmoirai:github-release:$github_release_plugin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokka_version"
classpath libs.buildscript.kt.gradle
classpath libs.buildscript.kt.serialization
classpath libs.buildscript.jb.dokka
classpath libs.buildscript.gh.release
classpath libs.buildscript.android.gradle
classpath libs.buildscript.android.dexcount
}
}
@@ -21,6 +21,7 @@ allprojects {
mavenLocal()
mavenCentral()
google()
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
}
// temporal crutch until legacy tests will be stabled or legacy target will be removed

View File

@@ -0,0 +1,18 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
}
}
}
}

View File

@@ -0,0 +1,16 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.DisposableEffectResult
class DefaultDisposableEffectResult(
private val onDispose: () -> Unit
) : DisposableEffectResult {
override fun dispose() {
onDispose()
}
companion object {
val DoNothing = DefaultDisposableEffectResult {}
}
}

View File

@@ -0,0 +1,9 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.Composition
import dev.inmo.micro_utils.common.onRemoved
import org.w3c.dom.Element
fun Composition.linkWithElement(element: Element) {
element.onRemoved { dispose() }
}

View File

@@ -0,0 +1,10 @@
package dev.inmo.micro_utils.common.compose
import org.jetbrains.compose.web.attributes.ATarget
fun openLink(link: String, mode: ATarget = ATarget.Blank, features: String = "") = dev.inmo.micro_utils.common.openLink(
link,
mode.targetStr,
features
)

View File

@@ -0,0 +1,13 @@
package dev.inmo.micro_utils.common.compose
import androidx.compose.runtime.*
import org.jetbrains.compose.web.dom.DOMScope
import org.w3c.dom.Element
fun <TElement : Element> renderComposableAndLinkToRoot(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithElement(root)
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.common.compose"/>

View File

@@ -1,3 +1,5 @@
@file:Suppress("OPT_IN_IS_NOT_ENABLED")
package dev.inmo.micro_utils.common
@RequiresOptIn(

View File

@@ -43,6 +43,7 @@ private inline fun <T> performChanges(
if (oldOneEqualToNewObject || newOneEqualToOldObject) {
changedList.addAll(
potentialChanges.take(i).mapNotNull {
@Suppress("UNCHECKED_CAST")
if (it.first != null && it.second != null) it as Pair<IndexedValue<T>, IndexedValue<T>> else null
}
)
@@ -121,7 +122,10 @@ fun <T> Iterable<T>.calculateDiff(
when {
oldObject === newObject || (oldObject == newObject && !strictComparison) -> {
changedObjects.addAll(potentiallyChangedObjects.map { it as Pair<IndexedValue<T>, IndexedValue<T>> })
changedObjects.addAll(potentiallyChangedObjects.map {
@Suppress("UNCHECKED_CAST")
it as Pair<IndexedValue<T>, IndexedValue<T>>
})
potentiallyChangedObjects.clear()
}
else -> {
@@ -153,3 +157,22 @@ inline fun <T> StrictDiff(old: Iterable<T>, new: Iterable<T>) = old.calculateDif
inline fun <T> Iterable<T>.calculateStrictDiff(
other: Iterable<T>
) = calculateDiff(other, strictComparison = true)
/**
* This method call [calculateDiff] with strict mode [strictComparison] and then apply differences to [this]
* mutable list
*/
fun <T> MutableList<T>.applyDiff(
source: Iterable<T>,
strictComparison: Boolean = false
) = calculateDiff(source, strictComparison).let {
for (i in it.removed.indices.sortedDescending()) {
removeAt(it.removed[i].index)
}
it.added.forEach { (i, t) ->
add(i, t)
}
it.replaced.forEach { (_, new) ->
set(new.index, new.value)
}
}

View File

@@ -1,3 +1,5 @@
@file:Suppress("unused", "NOTHING_TO_INLINE")
package dev.inmo.micro_utils.common
import kotlinx.serialization.*
@@ -6,7 +8,7 @@ import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
/**
* Realization of this interface will contains at least one not null - [t1] or [t2]
* Realization of this interface will contains at least one not null - [optionalT1] or [optionalT2]
*
* @see EitherFirst
* @see EitherSecond
@@ -14,26 +16,25 @@ import kotlinx.serialization.encoding.*
* @see Either.Companion.second
* @see Either.onFirst
* @see Either.onSecond
* @see Either.mapOnFirst
* @see Either.mapOnSecond
*/
@Serializable(EitherSerializer::class)
sealed interface Either<T1, T2> {
val t1: T1?
val t2: T2?
val optionalT1: Optional<T1>
val optionalT2: Optional<T2>
companion object {
fun <T1, T2> serializer(
t1Serializer: KSerializer<T1>,
t2Serializer: KSerializer<T2>,
): KSerializer<Either<T1, T2>> = EitherSerializer(t1Serializer, t2Serializer)
}
val t1OrNull: T1?
get() = optionalT1.dataOrNull()
val t2OrNull: T2?
get() = optionalT2.dataOrNull()
}
class EitherSerializer<T1, T2>(
t1Serializer: KSerializer<T1>,
t2Serializer: KSerializer<T2>,
) : KSerializer<Either<T1, T2>> {
@ExperimentalSerializationApi
@InternalSerializationApi
@OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class)
override val descriptor: SerialDescriptor = buildSerialDescriptor(
"TypedSerializer",
SerialKind.CONTEXTUAL
@@ -44,8 +45,6 @@ class EitherSerializer<T1, T2>(
private val t1EitherSerializer = EitherFirst.serializer(t1Serializer, t2Serializer)
private val t2EitherSerializer = EitherSecond.serializer(t1Serializer, t2Serializer)
@ExperimentalSerializationApi
@InternalSerializationApi
override fun deserialize(decoder: Decoder): Either<T1, T2> {
return decoder.decodeStructure(descriptor) {
var type: String? = null
@@ -77,8 +76,6 @@ class EitherSerializer<T1, T2>(
}
@ExperimentalSerializationApi
@InternalSerializationApi
override fun serialize(encoder: Encoder, value: Either<T1, T2>) {
encoder.encodeStructure(descriptor) {
when (value) {
@@ -96,25 +93,25 @@ class EitherSerializer<T1, T2>(
}
/**
* This type [Either] will always have not nullable [t1]
* This type [Either] will always have not nullable [optionalT1]
*/
@Serializable
data class EitherFirst<T1, T2>(
override val t1: T1
val t1: T1
) : Either<T1, T2> {
override val t2: T2?
get() = null
override val optionalT1: Optional<T1> = t1.optional
override val optionalT2: Optional<T2> = Optional.absent()
}
/**
* This type [Either] will always have not nullable [t2]
* This type [Either] will always have not nullable [optionalT2]
*/
@Serializable
data class EitherSecond<T1, T2>(
override val t2: T2
val t2: T2
) : Either<T1, T2> {
override val t1: T1?
get() = null
override val optionalT1: Optional<T1> = Optional.absent()
override val optionalT2: Optional<T2> = t2.optional
}
/**
@@ -127,23 +124,35 @@ inline fun <T1, T2> Either.Companion.first(t1: T1): Either<T1, T2> = EitherFirst
inline fun <T1, T2> Either.Companion.second(t2: T2): Either<T1, T2> = EitherSecond(t2)
/**
* Will call [block] in case when [Either.t1] of [this] is not null
* Will call [block] in case when [this] is [EitherFirst]
*/
inline fun <T1, T2, E : Either<T1, T2>> E.onFirst(block: (T1) -> Unit): E {
val t1 = t1
t1 ?.let(block)
optionalT1.onPresented(block)
return this
}
/**
* Will call [block] in case when [Either.t2] of [this] is not null
* Will call [block] in case when [this] is [EitherSecond]
*/
inline fun <T1, T2, E : Either<T1, T2>> E.onSecond(block: (T2) -> Unit): E {
val t2 = t2
t2 ?.let(block)
optionalT2.onPresented(block)
return this
}
/**
* @return Result of [block] if [this] is [EitherFirst]
*/
inline fun <T1, R> Either<T1, *>.mapOnFirst(block: (T1) -> R): R? {
return optionalT1.mapOnPresented(block)
}
/**
* @return Result of [block] if [this] is [EitherSecond]
*/
inline fun <T2, R> Either<*, T2>.mapOnSecond(block: (T2) -> R): R? {
return optionalT2.mapOnPresented(block)
}
inline fun <reified T1, reified T2> Any.either() = when (this) {
is T1 -> Either.first<T1, T2>(this)
is T2 -> Either.second<T1, T2>(this)

View File

@@ -23,11 +23,12 @@ value class FileName(val string: String) {
}
@PreviewFeature
expect class MPPFile
expect val MPPFile.filename: FileName
expect val MPPFile.filesize: Long
expect val MPPFile.bytesAllocatorSync: ByteArrayAllocator
expect val MPPFile.bytesAllocator: SuspendByteArrayAllocator
fun MPPFile.bytesSync() = bytesAllocatorSync()
suspend fun MPPFile.bytes() = bytesAllocator()

View File

@@ -0,0 +1,53 @@
package dev.inmo.micro_utils.common
import kotlin.jvm.JvmName
interface SimpleMapper<T1, T2> {
fun convertToT1(from: T2): T1
fun convertToT2(from: T1): T2
}
@JvmName("convertFromT2")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T2) = convertToT1(from)
@JvmName("convertFromT1")
fun <T1, T2> SimpleMapper<T1, T2>.convert(from: T1) = convertToT2(from)
class SimpleMapperImpl<T1, T2>(
private val t1: (T2) -> T1,
private val t2: (T1) -> T2,
) : SimpleMapper<T1, T2> {
override fun convertToT1(from: T2): T1 = t1.invoke(from)
override fun convertToT2(from: T1): T2 = t2.invoke(from)
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleMapper(
noinline t1: (T2) -> T1,
noinline t2: (T1) -> T2,
) = SimpleMapperImpl(t1, t2)
interface SimpleSuspendableMapper<T1, T2> {
suspend fun convertToT1(from: T2): T1
suspend fun convertToT2(from: T1): T2
}
@JvmName("convertFromT2")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T2) = convertToT1(from)
@JvmName("convertFromT1")
suspend fun <T1, T2> SimpleSuspendableMapper<T1, T2>.convert(from: T1) = convertToT2(from)
class SimpleSuspendableMapperImpl<T1, T2>(
private val t1: suspend (T2) -> T1,
private val t2: suspend (T1) -> T2,
) : SimpleSuspendableMapper<T1, T2> {
override suspend fun convertToT1(from: T2): T1 = t1.invoke(from)
override suspend fun convertToT2(from: T1): T2 = t2.invoke(from)
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T1, T2> simpleSuspendableMapper(
noinline t1: suspend (T2) -> T1,
noinline t2: suspend (T1) -> T2,
) = SimpleSuspendableMapperImpl(t1, t2)

View File

@@ -21,8 +21,10 @@ import kotlinx.serialization.Serializable
*/
@Serializable
data class Optional<T> internal constructor(
internal val data: T?,
internal val dataPresented: Boolean
@Warning("It is unsafe to use this data directly")
val data: T?,
@Warning("It is unsafe to use this data directly")
val dataPresented: Boolean
) {
companion object {
/**
@@ -39,37 +41,57 @@ data class Optional<T> internal constructor(
inline val <T> T.optional
get() = Optional.presented(this)
inline val <T : Any> T?.optionalOrAbsentIfNull
get() = if (this == null) {
Optional.absent<T>()
} else {
Optional.presented(this)
}
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
inline fun <T> Optional<T>.onPresented(block: (T) -> Unit): Optional<T> = apply {
@OptIn(Warning::class)
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) }
}
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
inline fun <T, R> Optional<T>.mapOnPresented(block: (T) -> R): R? = run {
@OptIn(Warning::class)
if (dataPresented) { @Suppress("UNCHECKED_CAST") block(data as T) } else null
}
/**
* Will call [block] when data absent ([Optional.dataPresented] == false)
*/
fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
inline fun <T> Optional<T>.onAbsent(block: () -> Unit): Optional<T> = apply {
@OptIn(Warning::class)
if (!dataPresented) { block() }
}
/**
* Will call [block] when data presented ([Optional.dataPresented] == true)
*/
inline fun <T, R> Optional<T>.mapOnAbsent(block: () -> R): R? = run {
@OptIn(Warning::class)
if (!dataPresented) { block() } else null
}
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or null otherwise
*/
fun <T> Optional<T>.dataOrNull() = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
fun <T> Optional<T>.dataOrNull() = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else null
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or throw [throwable] otherwise
*/
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
fun <T> Optional<T>.dataOrThrow(throwable: Throwable) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else throw throwable
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
*/
fun <T> Optional<T>.dataOrElse(block: () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
/**
* Returns [Optional.data] if [Optional.dataPresented] of [this] is true, or call [block] and returns the result of it
*/
suspend fun <T> Optional<T>.dataOrElseSuspendable(block: suspend () -> T) = if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()
inline fun <T> Optional<T>.dataOrElse(block: () -> T) = @OptIn(Warning::class) if (dataPresented) @Suppress("UNCHECKED_CAST") (data as T) else block()

View File

@@ -32,7 +32,7 @@ class DiffUtilsTests {
val withIndex = oldList.withIndex()
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
for ((i, v) in withIndex) {
for ((i, _) in withIndex) {
if (i + count > oldList.lastIndex) {
continue
}
@@ -54,8 +54,8 @@ class DiffUtilsTests {
val oldList = (0 until 10).map { it.toString() }
val withIndex = oldList.withIndex()
for (step in 0 until oldList.size) {
for ((i, v) in withIndex) {
for (step in oldList.indices) {
for ((i, _) in withIndex) {
val mutable = oldList.toMutableList()
val changes = (
if (step == 0) i until oldList.size else (i until oldList.size step step)
@@ -73,4 +73,83 @@ class DiffUtilsTests {
}
}
}
@Test
fun testThatSimpleRemoveApplyWorks() {
val oldList = (0 until 10).toList()
val withIndex = oldList.withIndex()
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
for ((i, _) in withIndex) {
if (i + count > oldList.lastIndex) {
continue
}
val removedSublist = oldList.subList(i, i + count)
val mutableOldList = oldList.toMutableList()
val targetList = oldList - removedSublist
mutableOldList.applyDiff(targetList)
assertEquals(
targetList,
mutableOldList
)
}
}
}
@Test
fun testThatSimpleAddApplyWorks() {
val oldList = (0 until 10).map { it.toString() }
val withIndex = oldList.withIndex()
for (count in 1 .. (floor(oldList.size.toFloat() / 2).toInt())) {
for ((i, _) in withIndex) {
if (i + count > oldList.lastIndex) {
continue
}
val addedSublist = oldList.subList(i, i + count).map { "added$it" }
val mutable = oldList.toMutableList()
mutable.addAll(i, addedSublist)
val mutableOldList = oldList.toMutableList()
mutableOldList.applyDiff(mutable)
assertEquals(
mutable,
mutableOldList
)
}
}
}
@Test
fun testThatSimpleChangesApplyWorks() {
val oldList = (0 until 10).map { it.toString() }
val withIndex = oldList.withIndex()
for (step in oldList.indices) {
for ((i, _) in withIndex) {
val mutable = oldList.toMutableList()
val newList = if (step == 0) {
i until oldList.size
} else {
i until oldList.size step step
}
newList.forEach { index ->
IndexedValue(index, mutable[index]) to IndexedValue(index, "changed$index").also {
mutable[index] = it.value
}
}
val mutableOldList = oldList.toMutableList()
mutableOldList.applyDiff(mutable)
assertEquals(
mutable,
mutableOldList
)
}
}
}
}

View File

@@ -0,0 +1,61 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
fun Node.onRemoved(block: () -> Unit): MutationObserver {
lateinit var observer: MutationObserver
observer = MutationObserver { _, _ ->
fun checkIfRemoved(node: Node): Boolean {
return node.parentNode != document && (node.parentNode ?.let { checkIfRemoved(it) } ?: true)
}
if (checkIfRemoved(this)) {
observer.disconnect()
block()
}
}
observer.observe(document, MutationObserverInit(childList = true, subtree = true))
return observer
}
fun Element.onVisibilityChanged(block: IntersectionObserverEntry.(Float, IntersectionObserver) -> Unit): IntersectionObserver {
var previousIntersectionRatio = -1f
val observer = IntersectionObserver { entries, observer ->
entries.forEach {
if (previousIntersectionRatio != it.intersectionRatio) {
previousIntersectionRatio = it.intersectionRatio.toFloat()
it.block(previousIntersectionRatio, observer)
}
}
}
observer.observe(this)
return observer
}
fun Element.onVisible(block: Element.(IntersectionObserver) -> Unit) {
var previous = -1f
onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio > 0 && previous == 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}
fun Element.onInvisible(block: Element.(IntersectionObserver) -> Unit): IntersectionObserver {
var previous = -1f
return onVisibilityChanged { intersectionRatio, observer ->
if (previous != intersectionRatio) {
if (intersectionRatio == 0f && previous != 0f) {
block(observer)
}
previous = intersectionRatio
}
}
}

View File

@@ -0,0 +1,124 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.DOMRectReadOnly
import org.w3c.dom.Element
external interface IntersectionObserverOptions {
/**
* An Element or Document object which is an ancestor of the intended target, whose bounding rectangle will be
* considered the viewport. Any part of the target not visible in the visible area of the root is not considered
* visible.
*/
var root: Element?
/**
* A string which specifies a set of offsets to add to the root's bounding_box when calculating intersections,
* effectively shrinking or growing the root for calculation purposes. The syntax is approximately the same as that
* for the CSS margin property; see The root element and root margin in Intersection Observer API for more
* information on how the margin works and the syntax. The default is "0px 0px 0px 0px".
*/
var rootMargin: String?
/**
* Either a single number or an array of numbers between 0.0 and 1.0, specifying a ratio of intersection area to
* total bounding box area for the observed target. A value of 0.0 means that even a single visible pixel counts as
* the target being visible. 1.0 means that the entire target element is visible. See Thresholds in Intersection
* Observer API for a more in-depth description of how thresholds are used. The default is a threshold of 0.0.
*/
var threshold: Array<Number>?
}
fun IntersectionObserverOptions(
block: IntersectionObserverOptions.() -> Unit = {}
): IntersectionObserverOptions = js("{}").unsafeCast<IntersectionObserverOptions>().apply(block)
external interface IntersectionObserverEntry {
/**
* Returns the bounds rectangle of the target element as a DOMRectReadOnly. The bounds are computed as described in
* the documentation for Element.getBoundingClientRect().
*/
val boundingClientRect: DOMRectReadOnly
/**
* Returns the ratio of the intersectionRect to the boundingClientRect.
*/
val intersectionRatio: Number
/**
* Returns a DOMRectReadOnly representing the target's visible area.
*/
val intersectionRect: DOMRectReadOnly
/**
* A Boolean value which is true if the target element intersects with the intersection observer's root. If this is
* true, then, the IntersectionObserverEntry describes a transition into a state of intersection; if it's false,
* then you know the transition is from intersecting to not-intersecting.
*/
val isIntersecting: Boolean
/**
* Returns a DOMRectReadOnly for the intersection observer's root.
*/
val rootBounds: DOMRectReadOnly
/**
* The Element whose intersection with the root changed.
*/
val target: Element
/**
* A DOMHighResTimeStamp indicating the time at which the intersection was recorded, relative to the
* IntersectionObserver's time origin.
*/
val time: Double
}
typealias IntersectionObserverCallback = (entries: Array<IntersectionObserverEntry>, observer: IntersectionObserver) -> Unit
/**
* This is just an implementation from [this commentary](https://youtrack.jetbrains.com/issue/KT-43157#focus=Comments-27-4498582.0-0)
* of Kotlin JS issue related to the absence of [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver)
*/
external class IntersectionObserver(callback: IntersectionObserverCallback) {
constructor(callback: IntersectionObserverCallback, options: IntersectionObserverOptions)
/**
* The Element or Document whose bounds are used as the bounding box when testing for intersection. If no root value
* was passed to the constructor or its value is null, the top-level document's viewport is used.
*/
val root: Element
/**
* An offset rectangle applied to the root's bounding box when calculating intersections, effectively shrinking or
* growing the root for calculation purposes. The value returned by this property may not be the same as the one
* specified when calling the constructor as it may be changed to match internal requirements. Each offset can be
* expressed in pixels (px) or as a percentage (%). The default is "0px 0px 0px 0px".
*/
val rootMargin: String
/**
* A list of thresholds, sorted in increasing numeric order, where each threshold is a ratio of intersection area to
* bounding box area of an observed target. Notifications for a target are generated when any of the thresholds are
* crossed for that target. If no value was passed to the constructor, 0 is used.
*/
val thresholds: Array<Number>
/**
* Stops the IntersectionObserver object from observing any target.
*/
fun disconnect()
/**
* Tells the IntersectionObserver a target element to observe.
*/
fun observe(targetElement: Element)
/**
* Returns an array of IntersectionObserverEntry objects for all observed targets.
*/
fun takeRecords(): Array<IntersectionObserverEntry>
/**
* Tells the IntersectionObserver to stop observing a particular target element.
*/
fun unobserve(targetElement: Element)
}

View File

@@ -0,0 +1,12 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.Element
inline val Element.isOverflowWidth
get() = scrollWidth > clientWidth
inline val Element.isOverflowHeight
get() = scrollHeight > clientHeight
inline val Element.isOverflow
get() = isOverflowHeight || isOverflowWidth

View File

@@ -2,8 +2,7 @@ package dev.inmo.micro_utils.common
import org.khronos.webgl.ArrayBuffer
import org.w3c.dom.ErrorEvent
import org.w3c.files.File
import org.w3c.files.FileReader
import org.w3c.files.*
import kotlin.js.Promise
/**
@@ -24,6 +23,11 @@ fun MPPFile.readBytesPromise() = Promise<ByteArray> { success, failure ->
reader.readAsArrayBuffer(this)
}
fun MPPFile.readBytes(): ByteArray {
val reader = FileReaderSync()
return reader.readAsArrayBuffer(this).toByteArray()
}
private suspend fun MPPFile.dirtyReadBytes(): ByteArray = readBytesPromise().await()
/**
@@ -40,5 +44,11 @@ actual val MPPFile.filesize: Long
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
@Warning("That is not optimized version of bytes allocator. Use asyncBytesAllocator everywhere you can")
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = ::dirtyReadBytes

View File

@@ -0,0 +1,38 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.*
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventListener
fun Element.onActionOutside(type: String, options: dynamic = null, callback: (Event) -> Unit): EventListener {
lateinit var observer: MutationObserver
val listener = EventListener {
val elementsToCheck = mutableListOf<Element>(this@onActionOutside)
while (it.target != this@onActionOutside && elementsToCheck.isNotEmpty()) {
val childrenGettingElement = elementsToCheck.removeFirst()
for (i in 0 until childrenGettingElement.childElementCount) {
elementsToCheck.add(childrenGettingElement.children[i] ?: continue)
}
}
if (elementsToCheck.isEmpty()) {
callback(it)
}
}
if (options == null) {
document.addEventListener(type, listener)
} else {
document.addEventListener(type, listener, options)
}
observer = onRemoved {
if (options == null) {
document.removeEventListener(type, listener)
} else {
document.removeEventListener(type, listener, options)
}
observer.disconnect()
}
return listener
}
fun Element.onClickOutside(options: dynamic = null, callback: (Event) -> Unit) = onActionOutside("click", options, callback)

View File

@@ -0,0 +1,8 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.window
fun openLink(link: String, target: String = "_blank", features: String = "") {
window.open(link, target, features) ?.focus()
}

View File

@@ -0,0 +1,58 @@
package dev.inmo.micro_utils.common
import org.w3c.dom.*
import kotlin.js.Json
import kotlin.js.json
external class ResizeObserver(
callback: (Array<ResizeObserverEntry>, ResizeObserver) -> Unit
) {
fun observe(target: Element, options: Json = definedExternally)
fun unobserve(target: Element)
fun disconnect()
}
external interface ResizeObserverSize {
val blockSize: Float
val inlineSize: Float
}
external interface ResizeObserverEntry {
val borderBoxSize: Array<ResizeObserverSize>
val contentBoxSize: Array<ResizeObserverSize>
val devicePixelContentBoxSize: Array<ResizeObserverSize>
val contentRect: DOMRectReadOnly
val target: Element
}
fun ResizeObserver.observe(target: Element, options: ResizeObserverObserveOptions) = observe(
target,
json(
"box" to options.box ?.name
)
)
class ResizeObserverObserveOptions(
val box: Box? = null
) {
sealed interface Box {
val name: String
object Content : Box {
override val name: String
get() = "content-box"
}
object Border : Box {
override val name: String
get() = "border-box"
}
object DevicePixelContent : Box {
override val name: String
get() = "device-pixel-content-box"
}
}
}

View File

@@ -0,0 +1,30 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import kotlinx.dom.createElement
import org.w3c.dom.HTMLElement
import org.w3c.dom.HTMLInputElement
import org.w3c.files.get
fun selectFile(
inputSetup: (HTMLInputElement) -> Unit = {},
onFailure: (Throwable) -> Unit = {},
onFile: (MPPFile) -> Unit
) {
(document.createElement("input") {
(this as HTMLInputElement).apply {
type = "file"
onchange = {
runCatching {
files ?.get(0) ?: error("File must not be null")
}.onSuccess {
onFile(it)
}.onFailure {
onFailure(it)
}
}
inputSetup(this)
}
} as HTMLElement).click()
}

View File

@@ -0,0 +1,14 @@
package dev.inmo.micro_utils.common
import kotlinx.browser.document
import org.w3c.dom.HTMLAnchorElement
fun triggerDownloadFile(filename: String, fileLink: String) {
val hiddenElement = document.createElement("a") as HTMLAnchorElement
hiddenElement.href = fileLink
hiddenElement.target = "_blank"
hiddenElement.download = filename
hiddenElement.click()
}

View File

@@ -22,6 +22,11 @@ actual val MPPFile.filesize: Long
/**
* @suppress
*/
actual val MPPFile.bytesAllocatorSync: ByteArrayAllocator
get() = ::readBytes
/**
* @suppress
*/
actual val MPPFile.bytesAllocator: SuspendByteArrayAllocator
get() = {
doInIO {

View File

@@ -10,12 +10,17 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
api libs.kt.coroutines
}
}
jsMain {
dependencies {
api project(":micro_utils.common")
}
}
androidMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
api libs.kt.coroutines.android
}
}
}

View File

@@ -0,0 +1,20 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.library"
alias(libs.plugins.jb.compose)
}
apply from: "$mppProjectWithSerializationAndComposePresetPath"
kotlin {
sourceSets {
commonMain {
dependencies {
api libs.kt.coroutines
api project(":micro_utils.coroutines")
api project(":micro_utils.common.compose")
}
}
}
}

View File

@@ -0,0 +1,26 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import dev.inmo.micro_utils.common.applyDiff
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
@Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asMutableComposeListState(
scope: CoroutineScope
): SnapshotStateList<T> {
val state = mutableStateListOf<T>()
subscribeSafelyWithoutExceptions(scope) {
state.applyDiff(it)
}
return state
}
@Suppress("NOTHING_TO_INLINE")
inline fun <reified T> Flow<List<T>>.asComposeList(
scope: CoroutineScope
): List<T> = asMutableComposeListState(scope)

View File

@@ -0,0 +1,35 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
fun <T> Flow<T>.asMutableComposeState(
initial: T,
scope: CoroutineScope
): MutableState<T> {
val state = mutableStateOf(initial)
subscribeSafelyWithoutExceptions(scope) { state.value = it }
return state
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asMutableComposeState(
scope: CoroutineScope
): MutableState<T> = asMutableComposeState(value, scope)
fun <T> Flow<T>.asComposeState(
initial: T,
scope: CoroutineScope
): State<T> {
val state = asMutableComposeState(initial, scope)
return derivedStateOf { state.value }
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.asComposeState(
scope: CoroutineScope
): State<T> = asComposeState(value, scope)

View File

@@ -0,0 +1,23 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
fun <T> Flow<T>.toMutableState(
initial: T,
scope: CoroutineScope
): MutableState<T> {
val state = mutableStateOf(initial)
subscribeSafelyWithoutExceptions(scope) { state.value = it }
return state
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> StateFlow<T>.toMutableState(
scope: CoroutineScope
): MutableState<T> = toMutableState(value, scope)

View File

@@ -0,0 +1,14 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.job
import kotlin.coroutines.CoroutineContext
fun Composition.linkWithJob(job: Job) {
job.invokeOnCompletion {
this@linkWithJob.dispose()
}
}
fun Composition.linkWithContext(coroutineContext: CoroutineContext) = linkWithJob(coroutineContext.job)

View File

@@ -0,0 +1,26 @@
package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.*
import dev.inmo.micro_utils.common.compose.linkWithElement
import kotlinx.coroutines.*
import org.jetbrains.compose.web.dom.DOMScope
import org.w3c.dom.Element
suspend fun <TElement : Element> renderComposableAndLinkToContext(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithContext(
currentCoroutineContext()
)
}
suspend fun <TElement : Element> renderComposableAndLinkToContextAndRoot(
root: TElement,
monotonicFrameClock: MonotonicFrameClock = DefaultMonotonicFrameClock,
content: @Composable DOMScope<TElement>.() -> Unit
): Composition = org.jetbrains.compose.web.renderComposable(root, monotonicFrameClock, content).apply {
linkWithContext(currentCoroutineContext())
linkWithElement(root)
}

View File

@@ -0,0 +1 @@
<manifest package="dev.inmo.micro_utils.coroutines.compose"/>

View File

@@ -6,11 +6,12 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.coroutines.cancellation.CancellationException
private sealed interface AccumulatorFlowStep
private data class DataRetrievedAccumulatorFlowStep(val data: Any) : AccumulatorFlowStep
private data class SubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
private data class UnsubscribeAccumulatorFlowStep(val channel: Channel<Any>) : AccumulatorFlowStep
private sealed interface AccumulatorFlowStep<T>
private data class DataRetrievedAccumulatorFlowStep<T>(val data: T) : AccumulatorFlowStep<T>
private data class SubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
private data class UnsubscribeAccumulatorFlowStep<T>(val channel: Channel<T>) : AccumulatorFlowStep<T>
/**
* This [Flow] will have behaviour very similar to [SharedFlow], but there are several differences:
@@ -26,12 +27,12 @@ class AccumulatorFlow<T>(
private val subscope = scope.LinkedSupervisorScope()
private val activeData = ArrayDeque<T>()
private val dataMutex = Mutex()
private val channelsForBroadcast = mutableListOf<Channel<Any>>()
private val channelsForBroadcast = mutableListOf<Channel<T>>()
private val channelsMutex = Mutex()
private val steps = subscope.actor<AccumulatorFlowStep> { step ->
private val steps = subscope.actor<AccumulatorFlowStep<T>> { step ->
when (step) {
is DataRetrievedAccumulatorFlowStep -> {
if (activeData.first() === step.data) {
if (activeData.firstOrNull() === step.data) {
dataMutex.withLock {
activeData.removeFirst()
}
@@ -42,7 +43,7 @@ class AccumulatorFlow<T>(
dataMutex.withLock {
val dataToSend = activeData.toList()
safelyWithoutExceptions {
dataToSend.forEach { step.channel.send(it as Any) }
dataToSend.forEach { step.channel.send(it) }
}
}
}
@@ -58,24 +59,29 @@ class AccumulatorFlow<T>(
channelsMutex.withLock {
channelsForBroadcast.forEach { channel ->
safelyWithResult {
channel.send(it as Any)
channel.send(it)
}
}
}
}
override suspend fun collectSafely(collector: FlowCollector<T>) {
val channel = Channel<Any>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
val channel = Channel<T>(Channel.UNLIMITED, BufferOverflow.SUSPEND)
steps.send(SubscribeAccumulatorFlowStep(channel))
for (data in channel) {
try {
collector.emit(data as T)
steps.send(DataRetrievedAccumulatorFlowStep(data))
} finally {
channel.cancel()
steps.send(UnsubscribeAccumulatorFlowStep(channel))
val result = runCatchingSafely {
for (data in channel) {
val emitResult = runCatchingSafely {
collector.emit(data)
}
if (emitResult.isSuccess || emitResult.exceptionOrNull() is CancellationException) {
steps.send(DataRetrievedAccumulatorFlowStep(data))
}
emitResult.getOrThrow()
}
}
channel.cancel()
steps.send(UnsubscribeAccumulatorFlowStep(channel))
result.getOrThrow()
}
}

View File

@@ -0,0 +1,28 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.micro_utils.common.onRemoved
import dev.inmo.micro_utils.common.onVisibilityChanged
import kotlinx.coroutines.flow.*
import org.w3c.dom.Element
fun Element.visibilityFlow(): Flow<Boolean> = channelFlow {
var previousData: Boolean? = null
val observer = onVisibilityChanged { intersectionRatio, _ ->
val currentData = intersectionRatio > 0
if (currentData != previousData) {
trySend(currentData)
}
previousData = currentData
}
val removeObserver = onRemoved {
observer.disconnect()
close()
}
invokeOnClose {
observer.disconnect()
removeObserver.disconnect()
}
}

View File

@@ -0,0 +1,42 @@
package dev.inmo.micro_utils.coroutines
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.selectFile
import kotlinx.coroutines.CompletableDeferred
import org.w3c.dom.HTMLInputElement
suspend fun selectFileOrThrow(
inputSetup: (HTMLInputElement) -> Unit = {}
): MPPFile {
val result = CompletableDeferred<MPPFile>()
selectFile(
inputSetup,
{
result.completeExceptionally(it)
}
) {
result.complete(it)
}
return result.await()
}
suspend fun selectFileOrNull(
inputSetup: (HTMLInputElement) -> Unit = {},
onFailure: (Throwable) -> Unit = {}
): MPPFile? {
val result = CompletableDeferred<MPPFile?>()
selectFile(
inputSetup,
{
result.complete(null)
onFailure(it)
}
) {
result.complete(it)
}
return result.await()
}

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.crypto
val HEX_ARRAY = "0123456789abcdef".toCharArray()
fun SourceBytes.hex(): String {
val hexChars = CharArray(size * 2)
for (j in indices) {
val v: Int = this[j].toInt() and 0xFF
hexChars[j * 2] = HEX_ARRAY[v ushr 4]
hexChars[j * 2 + 1] = HEX_ARRAY[v and 0x0F]
}
return hexChars.concatToString()
}
fun SourceString.hex(): String = encodeToByteArray().hex()

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.crypto
import kotlin.test.*
class Hex {
@Test
fun testSimpleHmacSHA256Message() {
val text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
val resultHex = text.hex()
assertEquals(
"4c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742c2073656420646f20656975736d6f642074656d706f7220696e6369646964756e74207574206c61626f726520657420646f6c6f7265206d61676e6120616c697175612e20557420656e696d206164206d696e696d2076656e69616d2c2071756973206e6f737472756420657865726369746174696f6e20756c6c616d636f206c61626f726973206e69736920757420616c697175697020657820656120636f6d6d6f646f20636f6e7365717561742e2044756973206175746520697275726520646f6c6f7220696e20726570726568656e646572697420696e20766f6c7570746174652076656c697420657373652063696c6c756d20646f6c6f726520657520667567696174206e756c6c612070617269617475722e204578636570746575722073696e74206f6363616563617420637570696461746174206e6f6e2070726f6964656e742c2073756e7420696e2063756c706120717569206f666669636961206465736572756e74206d6f6c6c697420616e696d20696420657374206c61626f72756d2e",
resultHex
)
}
}

View File

@@ -26,12 +26,12 @@ ext {
}
android {
compileSdkVersion "$android_compileSdkVersion".toInteger()
buildToolsVersion "$android_buildToolsVersion"
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
buildToolsVersion libs.versions.android.props.buildTools.get()
defaultConfig {
minSdkVersion "$android_minSdkVersion".toInteger()
targetSdkVersion "$android_compileSdkVersion".toInteger()
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
versionCode "${android_code_version}".toInteger()
versionName "$version"
}

View File

@@ -21,6 +21,7 @@ allprojects {
releaseMode = (project.hasProperty('RELEASE_MODE') && project.property('RELEASE_MODE') == "true") || System.getenv('RELEASE_MODE') == "true"
mppProjectWithSerializationPresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerialization.gradle"
mppProjectWithSerializationAndComposePresetPath = "${rootProject.projectDir.absolutePath}/mppProjectWithSerializationAndCompose.gradle"
mppJavaProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppJavaProject.gradle"
mppAndroidProjectPresetPath = "${rootProject.projectDir.absolutePath}/mppAndroidProject.gradle"

View File

@@ -10,6 +10,7 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api project(":micro_utils.common")
api project(":micro_utils.coroutines")
}
}

View File

@@ -47,24 +47,11 @@ fun <I : O, O : State> CheckableHandlerHolder(
}
)
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
fun <I : O, O : State> StateHandlerHolder(
inputKlass: KClass<I>,
strict: Boolean = false,
delegateTo: StatesHandler<I, O>
) = CheckableHandlerHolder(inputKlass, strict, delegateTo)
inline fun <reified I : O, O : State> CheckableHandlerHolder(
strict: Boolean = false,
delegateTo: StatesHandler<I, O>
) = CheckableHandlerHolder(I::class, strict, delegateTo)
@Deprecated("Renamed", ReplaceWith("CheckableHandlerHolder"))
inline fun <reified I : O, O : State> StateHandlerHolder(
strict: Boolean = false,
delegateTo: StatesHandler<I, O>
) = CheckableHandlerHolder(strict, delegateTo)
inline fun <reified I : O, O: State> StatesHandler<I, O>.holder(
strict: Boolean = true
) = CheckableHandlerHolder<I, O>(

View File

@@ -1,8 +1,14 @@
package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions
import dev.inmo.micro_utils.common.Optional
import dev.inmo.micro_utils.common.onPresented
import dev.inmo.micro_utils.coroutines.*
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
/**
* Default [StatesMachine] may [startChain] and use inside logic for handling [State]s. By default you may use
@@ -10,13 +16,24 @@ import kotlinx.coroutines.*
* handling until [start] method will be called
*/
interface StatesMachine<T : State> : StatesHandler<T, T> {
suspend fun launchStateHandling(
state: T,
handlers: List<CheckableHandlerHolder<in T, T>>,
onStateHandlingErrorHandler: StateHandlingErrorHandler<T>
): T? {
return runCatchingSafely {
handlers.firstOrNull { it.checkHandleable(state) } ?.run {
handleState(state)
}
}.getOrElse {
onStateHandlingErrorHandler(state, it)
}
}
suspend fun launchStateHandling(
state: T,
handlers: List<CheckableHandlerHolder<in T, T>>
): T? {
return handlers.firstOrNull { it.checkHandleable(state) } ?.run {
handleState(state)
}
return launchStateHandling(state, handlers, defaultStateHandlingErrorHandler())
}
/**
@@ -35,24 +52,66 @@ interface StatesMachine<T : State> : StatesHandler<T, T> {
*/
operator fun <T: State> invoke(
statesManager: StatesManager<T>,
handlers: List<CheckableHandlerHolder<in T, T>>
) = DefaultStatesMachine(statesManager, handlers)
handlers: List<CheckableHandlerHolder<in T, T>>,
onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
) = DefaultStatesMachine(statesManager, handlers, onStateHandlingErrorHandler)
}
}
/**
* Default realization of [StatesMachine]. It uses [statesManager] for incapsulation of [State]s storing and contexts
* resolving, and uses [launchStateHandling] for [State] handling
* resolving, and uses [launchStateHandling] for [State] handling.
*
* This class suppose to be extended in case you wish some custom behaviour inside of [launchStateHandling], for example
*/
class DefaultStatesMachine <T: State>(
private val statesManager: StatesManager<T>,
private val handlers: List<CheckableHandlerHolder<in T, T>>
open class DefaultStatesMachine <T: State>(
protected val statesManager: StatesManager<T>,
protected val handlers: List<CheckableHandlerHolder<in T, T>>,
protected val onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
) : StatesMachine<T> {
/**
* Will call [launchStateHandling] for state handling
*/
override suspend fun StatesMachine<in T>.handleState(state: T): T? = launchStateHandling(state, handlers)
override suspend fun launchStateHandling(state: T, handlers: List<CheckableHandlerHolder<in T, T>>): T? {
return launchStateHandling(state, handlers, onStateHandlingErrorHandler)
}
/**
* This
*/
protected val statesJobs = mutableMapOf<T, Job>()
protected val statesJobsMutex = Mutex()
protected open suspend fun performUpdate(state: T) {
val newState = launchStateHandling(state, handlers)
if (newState == null) {
statesManager.endChain(state)
} else {
statesManager.update(state, newState)
}
}
open suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
statesJobsMutex.withLock {
statesJobs[actualState] ?.cancel()
statesJobs[actualState] = scope.launch {
performUpdate(actualState)
}.also { job ->
job.invokeOnCompletion { _ ->
scope.launch {
statesJobsMutex.withLock {
if (statesJobs[actualState] == job) {
statesJobs.remove(actualState)
}
}
}
}
}
}
}
/**
* Launch handling of states. On [statesManager] [StatesManager.onStartChain],
* [statesManager] [StatesManager.onChainStateUpdated] will be called lambda with performing of state. If
@@ -60,23 +119,21 @@ class DefaultStatesMachine <T: State>(
* [StatesManager.endChain].
*/
override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions {
val statePerformer: suspend (T) -> Unit = { state: T ->
val newState = launchStateHandling(state, handlers)
if (newState != null) {
statesManager.update(state, newState)
} else {
statesManager.endChain(state)
}
}
statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it) }
(statesManager.getActiveStates().asFlow() + statesManager.onStartChain).subscribeSafelyWithoutExceptions(this) {
launch { performStateUpdate(Optional.absent(), it, scope.LinkedSupervisorScope()) }
}
statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) {
launch { statePerformer(it.second) }
launch { performStateUpdate(Optional.presented(it.first), it.second, scope.LinkedSupervisorScope()) }
}
statesManager.getActiveStates().forEach {
launch { statePerformer(it) }
statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { removedState ->
launch {
statesJobsMutex.withLock {
val stateInMap = statesJobs.keys.firstOrNull { stateInMap -> stateInMap == removedState }
if (stateInMap === removedState) {
statesJobs[stateInMap] ?.cancel()
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
package dev.inmo.micro_utils.fsm.common
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler
import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.withLock
/**
* This extender of [StatesMachine] interface declare one new function [updateChain]. Realizations of this interface
* must be able to perform update of chain in internal [StatesManager]
*/
interface UpdatableStatesMachine<T : State> : StatesMachine<T> {
/**
* Update chain with current state equal to [currentState] with [newState]. Behaviour of this update preforming
* in cases when [currentState] does not exist in [StatesManager] must be declared inside of realization of
* [StatesManager.update] function
*/
suspend fun updateChain(currentState: T, newState: T)
}
open class DefaultUpdatableStatesMachine<T : State>(
statesManager: StatesManager<T>,
handlers: List<CheckableHandlerHolder<in T, T>>,
onStateHandlingErrorHandler: StateHandlingErrorHandler<T> = defaultStateHandlingErrorHandler()
) : DefaultStatesMachine<T>(
statesManager,
handlers,
onStateHandlingErrorHandler
), UpdatableStatesMachine<T> {
protected val jobsStates = mutableMapOf<Job, T>()
/**
* Realization of this update will use the [Job] of [previousState] in [statesJobs] and [jobsStates] if
* [previousState] is [Optional.presented] and [shouldReplaceJob] has returned true for [previousState] and [actualState]. In
* other words, [Job] of [previousState] WILL NOT be replaced with the new one if they are "equal". Equality of
* states is solved in [shouldReplaceJob] and can be rewritten in subclasses
*/
override suspend fun performStateUpdate(previousState: Optional<T>, actualState: T, scope: CoroutineScope) {
statesJobsMutex.withLock {
if (shouldReplaceJob(previousState, actualState)) {
statesJobs[actualState] ?.cancel()
}
val job = previousState.mapOnPresented {
statesJobs.remove(it)
} ?.takeIf { it.isActive } ?: scope.launch {
performUpdate(actualState)
}.also { job ->
job.invokeOnCompletion { _ ->
scope.launch {
statesJobsMutex.withLock {
statesJobs.remove(
jobsStates[job] ?: return@withLock
)
jobsStates.remove(job)
}
}
}
}
jobsStates.remove(job)
statesJobs[actualState] = job
jobsStates[job] = actualState
}
}
/**
* Compare if [previous] potentially lead to the same behaviour with [new]
*/
protected open suspend fun shouldReplaceJob(previous: Optional<T>, new: T): Boolean = previous.dataOrNull() != new
override suspend fun updateChain(currentState: T, newState: T) {
statesManager.update(currentState, newState)
}
}

View File

@@ -7,6 +7,12 @@ import kotlin.reflect.KClass
class FSMBuilder<T : State>(
var statesManager: StatesManager<T> = DefaultStatesManager(InMemoryDefaultStatesManagerRepo()),
val fsmBuilder: (statesManager: StatesManager<T>, states: List<CheckableHandlerHolder<T, T>>) -> StatesMachine<T> = { statesManager, states ->
StatesMachine(
statesManager,
states
)
},
var defaultStateHandler: StatesHandler<T, T>? = StatesHandler { null }
) {
private var states = mutableListOf<CheckableHandlerHolder<T, T>>()
@@ -42,7 +48,7 @@ class FSMBuilder<T : State>(
add(filter, handler)
}
fun build() = StatesMachine(
fun build() = fsmBuilder(
statesManager,
states.toList().let { list ->
defaultStateHandler ?.let { list + it.holder { true } } ?: list

View File

@@ -37,36 +37,49 @@ interface DefaultStatesManagerRepo<T : State> {
/**
* @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE
* any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default will be used
* any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default, will be used
* [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when
* this callback will return true, currently placed on the [State.context] [State] will be replaced by new state
* with [endChain] with current state
* @param onUpdateContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
* new state by using [endChain] with that state
*/
class DefaultStatesManager<T : State>(
private val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
private val onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
open class DefaultStatesManager<T : State>(
protected val repo: DefaultStatesManagerRepo<T> = InMemoryDefaultStatesManagerRepo(),
protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> true },
protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) : StatesManager<T> {
private val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
protected val _onChainStateUpdated = MutableSharedFlow<Pair<T, T>>(0)
override val onChainStateUpdated: Flow<Pair<T, T>> = _onChainStateUpdated.asSharedFlow()
private val _onStartChain = MutableSharedFlow<T>(0)
protected val _onStartChain = MutableSharedFlow<T>(0)
override val onStartChain: Flow<T> = _onStartChain.asSharedFlow()
private val _onEndChain = MutableSharedFlow<T>(0)
protected val _onEndChain = MutableSharedFlow<T>(0)
override val onEndChain: Flow<T> = _onEndChain.asSharedFlow()
private val mapMutex = Mutex()
protected val mapMutex = Mutex()
constructor(
repo: DefaultStatesManagerRepo<T>,
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean
) : this (
repo,
onUpdateContextsConflictResolver = onContextsConflictResolver
)
override suspend fun update(old: T, new: T) = mapMutex.withLock {
val stateByOldContext: T? = repo.getContextState(old.context)
when {
stateByOldContext != old -> return@withLock
stateByOldContext == null || old.context == new.context -> {
repo.removeState(old)
repo.set(new)
_onChainStateUpdated.emit(old to new)
}
else -> {
val stateOnNewOneContext = repo.getContextState(new.context)
if (stateOnNewOneContext == null || onContextsConflictResolver(old, new, stateOnNewOneContext)) {
if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) {
stateOnNewOneContext ?.let { endChainWithoutLock(it) }
repo.removeState(old)
repo.set(new)
@@ -77,13 +90,17 @@ class DefaultStatesManager<T : State>(
}
override suspend fun startChain(state: T) = mapMutex.withLock {
if (!repo.contains(state.context)) {
val stateOnContext = repo.getContextState(state.context)
if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) {
stateOnContext ?.let {
endChainWithoutLock(it)
}
repo.set(state)
_onStartChain.emit(state)
}
}
private suspend fun endChainWithoutLock(state: T) {
protected open suspend fun endChainWithoutLock(state: T) {
if (repo.getContextState(state.context) == state) {
repo.removeState(state)
_onEndChain.emit(state)

View File

@@ -1,16 +0,0 @@
package dev.inmo.micro_utils.fsm.common.managers
import dev.inmo.micro_utils.fsm.common.State
import kotlinx.coroutines.flow.*
/**
* Creates [DefaultStatesManager] with [InMemoryDefaultStatesManagerRepo]
*
* @param onContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context]
* key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by
* new state by using [endChain] with that state
*/
@Deprecated("Use DefaultStatesManager instead", ReplaceWith("DefaultStatesManager"))
fun <T: State> InMemoryStatesManager(
onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> true }
) = DefaultStatesManager(onContextsConflictResolver = onContextsConflictResolver)

View File

@@ -0,0 +1,6 @@
package dev.inmo.micro_utils.fsm.common.utils
typealias StateHandlingErrorHandler<T> = suspend (T, Throwable) -> T?
val DefaultStateHandlingErrorHandler: StateHandlingErrorHandler<*> = { _, _ -> null }
inline fun <T> defaultStateHandlingErrorHandler(): StateHandlingErrorHandler<T> = DefaultStateHandlingErrorHandler as StateHandlingErrorHandler<T>

View File

@@ -1,7 +1,6 @@
import dev.inmo.micro_utils.fsm.common.*
import dev.inmo.micro_utils.fsm.common.dsl.buildFSM
import dev.inmo.micro_utils.fsm.common.managers.DefaultStatesManager
import dev.inmo.micro_utils.fsm.common.managers.InMemoryStatesManager
import kotlinx.coroutines.*
sealed interface TrafficLightState : State {

View File

@@ -21,7 +21,7 @@ if (new File(projectDir, "secret.gradle").exists()) {
owner "InsanusMokrassar"
repo "MicroUtils"
tagName "${project.version}"
tagName "v${project.version}"
releaseName "${project.version}"
targetCommitish "${project.version}"

View File

@@ -7,43 +7,12 @@ android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g
kotlin_version=1.5.31
kotlin_coroutines_version=1.5.2
kotlin_serialisation_core_version=1.3.1
kotlin_exposed_version=0.36.2
ktor_version=1.6.5
klockVersion=2.4.8
github_release_plugin_version=2.2.12
uuidVersion=0.3.1
# ANDROID
core_ktx_version=1.7.0
androidx_recycler_version=1.2.1
appcompat_version=1.3.1
android_minSdkVersion=19
android_compileSdkVersion=31
android_buildToolsVersion=31.0.0
dexcount_version=3.0.0
junit_version=4.12
test_ext_junit_version=1.1.2
espresso_core=3.3.0
# JS NPM
crypto_js_version=4.1.1
# Dokka
dokka_version=1.5.31
# Project data
group=dev.inmo
version=0.8.6
android_code_version=86
version=0.12.4
android_code_version=143

87
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,87 @@
[versions]
kt = "1.7.10"
kt-serialization = "1.4.0"
kt-coroutines = "1.6.4"
jb-compose = "1.2.0-alpha01-dev764"
jb-exposed = "0.39.2"
jb-dokka = "1.7.10"
klock = "3.0.0"
uuid = "0.5.0"
ktor = "2.1.0"
gh-release = "2.4.1"
android-gradle = "7.2.2"
dexcount = "3.1.0"
android-coreKtx = "1.8.0"
android-recyclerView = "1.2.1"
android-appCompat = "1.4.2"
android-espresso = "3.4.0"
android-test = "1.1.3"
android-props-minSdk = "21"
android-props-compileSdk = "32"
android-props-buildTools = "32.0.0"
[libraries]
kt-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kt" }
kt-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kt-serialization" }
kt-serialization-cbor = { module = "org.jetbrains.kotlinx:kotlinx-serialization-cbor", version.ref = "kt-serialization" }
kt-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kt-coroutines" }
kt-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kt-coroutines" }
kt-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kt-coroutines" }
ktor-io = { module = "io.ktor:ktor-io", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-java = { module = "io.ktor:ktor-client-java", version.ref = "ktor" }
ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktor" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-server = { module = "io.ktor:ktor-server", version.ref = "ktor" }
ktor-server-cio = { module = "io.ktor:ktor-server-cio", version.ref = "ktor" }
ktor-server-host-common = { module = "io.ktor:ktor-server-host-common", version.ref = "ktor" }
ktor-websockets = { module = "io.ktor:ktor-websockets", version.ref = "ktor" }
ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktor" }
ktor-server-statusPages = { module = "io.ktor:ktor-server-status-pages", version.ref = "ktor" }
ktor-server-content-negotiation = { module = "io.ktor:ktor-server-content-negotiation", version.ref = "ktor" }
klock = { module = "com.soywiz.korlibs.klock:klock", version.ref = "klock" }
uuid = { module = "com.benasher44:uuid", version.ref = "uuid" }
jb-exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "jb-exposed" }
android-coreKtx = { module = "androidx.core:core-ktx", version.ref = "android-coreKtx" }
android-recyclerView = { module = "androidx.recyclerview:recyclerview", version.ref = "android-recyclerView" }
android-appCompat-resources = { module = "androidx.appcompat:appcompat-resources", version.ref = "android-appCompat" }
android-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "android-espresso" }
android-test-junit = { module = "androidx.test.ext:junit", version.ref = "android-test" }
kt-test-js = { module = "org.jetbrains.kotlin:kotlin-test-js", version.ref = "kt" }
kt-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kt" }
buildscript-kt-gradle = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kt" }
buildscript-kt-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kt" }
buildscript-jb-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "jb-dokka" }
buildscript-gh-release = { module = "com.github.breadmoirai:github-release", version.ref = "gh-release" }
buildscript-android-gradle = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gradle-plugin", version.ref = "dexcount" }
[plugins]
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -12,7 +12,7 @@ kotlin {
dependencies {
api internalProject("micro_utils.ktor.common")
api internalProject("micro_utils.coroutines")
api "io.ktor:ktor-client-core:$ktor_version"
api libs.ktor.client
}
}
}

View File

@@ -1,57 +1,62 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
import io.ktor.client.features.websocket.ws
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.readBytes
import io.ktor.client.plugins.pluginOrNull
import io.ktor.client.plugins.websocket.WebSockets
import io.ktor.client.plugins.websocket.ws
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.websocket.Frame
import io.ktor.websocket.readBytes
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.isActive
import kotlinx.serialization.DeserializationStrategy
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
crossinline conversation: suspend (StandardKtorSerialInputData) -> T
): Flow<T> {
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
val correctedUrl = url.asCorrectWebSocketUrl
return channelFlow {
val producerScope = this@channelFlow
do {
val reconnect = try {
safely {
ws(correctedUrl) {
for (received in incoming) {
when (received) {
is Frame.Binary -> producerScope.send(conversation(received.readBytes()))
else -> {
producerScope.close()
return@ws
}
val reconnect = runCatchingSafely {
ws(correctedUrl, requestBuilder) {
for (received in incoming) {
when (received) {
is Frame.Binary -> send(conversation(received.data))
else -> {
close()
return@ws
}
}
}
}
checkReconnection(null)
} catch (e: Throwable) {
}.getOrElse { e ->
checkReconnection(e).also {
if (!it) {
producerScope.close(e)
close(e)
}
}
}
} while (reconnect)
if (!producerScope.isClosedForSend) {
safely(
{ it.printStackTrace() }
) {
producerScope.close()
} while (reconnect && isActive)
if (isActive) {
safely {
close()
}
}
}
@@ -61,14 +66,17 @@ inline fun <T> HttpClient.createStandardWebsocketFlow(
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
inline fun <T> HttpClient.createStandardWebsocketFlow(
url: String,
crossinline checkReconnection: (Throwable?) -> Boolean = { true },
deserializer: DeserializationStrategy<T>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
crossinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = createStandardWebsocketFlow(
url,
checkReconnection
checkReconnection,
requestBuilder
) {
serialFormat.decodeDefault(deserializer, it)
}

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.ktor.client
import io.ktor.client.plugins.ClientRequestException
import io.ktor.client.statement.HttpResponse
import io.ktor.http.isSuccess
inline fun HttpResponse.throwOnUnsuccess(
unsuccessMessage: () -> String
) {
if (status.isSuccess()) {
return
}
throw ClientRequestException(this, unsuccessMessage())
}

View File

@@ -0,0 +1,6 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.client.request.forms.InputProvider
expect suspend fun MPPFile.inputProvider(): InputProvider

View File

@@ -0,0 +1,103 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.Warning
import dev.inmo.micro_utils.coroutines.runCatchingSafely
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
import io.ktor.client.plugins.pluginOrNull
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.http.URLProtocol
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.isActive
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
@Warning("This feature is internal and should not be used directly. It is can be changed without any notification and warranty on compile-time or other guaranties")
inline fun <reified T : Any> openBaseWebSocketFlow(
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline webSocketSessionRequest: suspend SendChannel<T>.() -> Unit
): Flow<T> {
return channelFlow {
do {
val reconnect = runCatchingSafely {
webSocketSessionRequest()
checkReconnection(null)
}.getOrElse { e ->
checkReconnection(e).also {
if (!it) {
close(e)
}
}
}
} while (reconnect && isActive)
if (isActive) {
safely {
close()
}
}
}
}
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <reified T : Any> HttpClient.openWebSocketFlow(
url: String,
useSecureConnection: Boolean,
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Flow<T> {
pluginOrNull(WebSockets) ?: error("Plugin $WebSockets must be installed for using createStandardWebsocketFlow")
return openBaseWebSocketFlow<T>(checkReconnection) {
val block: suspend DefaultClientWebSocketSession.() -> Unit = {
while (isActive) {
send(receiveDeserialized<T>())
}
}
if (useSecureConnection) {
wss(url, requestBuilder, block)
} else {
ws(url, requestBuilder, block)
}
}
}
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <reified T : Any> HttpClient.openWebSocketFlow(
url: String,
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Flow<T> = openWebSocketFlow(url, false, checkReconnection, requestBuilder)
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <reified T : Any> HttpClient.openSecureWebSocketFlow(
url: String,
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Flow<T> = openWebSocketFlow(url, true, checkReconnection, requestBuilder)
/**
* @param checkReconnection This lambda will be called when it is required to reconnect to websocket to establish
* connection. Must return true in case if must be reconnected. By default always reconnecting
*/
inline fun <reified T : Any> HttpClient.createStandardWebsocketFlow(
url: String,
noinline checkReconnection: suspend (Throwable?) -> Boolean = { true },
noinline requestBuilder: HttpRequestBuilder.() -> Unit = {}
): Flow<T> = openWebSocketFlow(url, checkReconnection, requestBuilder)

View File

@@ -1,16 +1,21 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.call.body
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.client.statement.readBytes
import io.ktor.http.*
import io.ktor.utils.io.core.ByteReadPacket
import kotlinx.serialization.*
typealias BodyPair<T> = Pair<SerializationStrategy<T>, T>
@Deprecated("This class will be removed soon. It is now recommended to use built-in ktor features instead")
class UnifiedRequester(
private val client: HttpClient = HttpClient(),
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
val client: HttpClient = HttpClient(),
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
) {
suspend fun <ResultType> uniget(
url: String,
@@ -27,30 +32,86 @@ class UnifiedRequester(
suspend fun <BodyType, ResultType> unipost(
url: String,
bodyInfo: BodyPair<BodyType>,
bodyInfo: Pair<SerializationStrategy<BodyType>, BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>
) = client.unipost(url, bodyInfo, resultDeserializer, serialFormat)
suspend fun <ResultType> unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
): ResultType = client.unimultipart(url, filename, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
suspend fun <BodyType, ResultType> unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
): ResultType = client.unimultipart(url, filename, otherData, inputProvider, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat)
suspend fun <ResultType> unimultipart(
url: String,
mppFile: MPPFile,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {}
): ResultType = client.unimultipart(
url, mppFile, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
)
suspend fun <BodyType, ResultType> unimultipart(
url: String,
mppFile: MPPFile,
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {}
): ResultType = client.unimultipart(
url, mppFile, otherData, resultDeserializer, mimetype, additionalParametersBuilder, dataHeadersBuilder, requestBuilder, serialFormat
)
fun <T> createStandardWebsocketFlow(
url: String,
checkReconnection: (Throwable?) -> Boolean = { true },
deserializer: DeserializationStrategy<T>
) = client.createStandardWebsocketFlow(url, checkReconnection, deserializer, serialFormat)
checkReconnection: suspend (Throwable?) -> Boolean,
deserializer: DeserializationStrategy<T>,
requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = client.createStandardWebsocketFlow(url, deserializer, checkReconnection, serialFormat, requestBuilder)
fun <T> createStandardWebsocketFlow(
url: String,
deserializer: DeserializationStrategy<T>,
requestBuilder: HttpRequestBuilder.() -> Unit = {},
) = createStandardWebsocketFlow(url, { true }, deserializer, requestBuilder)
}
@Deprecated("This property will be removed soon. It is now recommended to use built-in ktor features instead")
val defaultRequester = UnifiedRequester()
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <ResultType> HttpClient.uniget(
url: String,
resultDeserializer: DeserializationStrategy<ResultType>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
) = get<StandardKtorSerialInputData>(
url
).let {
serialFormat.decodeDefault(resultDeserializer, it)
) = get(url).let {
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
}
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
fun <T> SerializationStrategy<T>.encodeUrlQueryValue(
value: T,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
@@ -59,13 +120,141 @@ fun <T> SerializationStrategy<T>.encodeUrlQueryValue(
value
)
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <BodyType, ResultType> HttpClient.unipost(
url: String,
bodyInfo: BodyPair<BodyType>,
bodyInfo: Pair<SerializationStrategy<BodyType>, BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
) = post<StandardKtorSerialInputData>(url) {
body = serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
) = post(url) {
setBody(
serialFormat.encodeDefault(bodyInfo.first, bodyInfo.second)
)
}.let {
serialFormat.decodeDefault(resultDeserializer, it)
serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>())
}
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <ResultType> HttpClient.unimultipart(
url: String,
filename: String,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = submitFormWithBinaryData(
url,
formData = formData {
append(
"bytes",
inputProvider,
Headers.build {
append(HttpHeaders.ContentType, mimetype)
append(HttpHeaders.ContentDisposition, "filename=\"$filename\"")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
}
) {
requestBuilder()
}.let { serialFormat.decodeDefault(resultDeserializer, it.body<StandardKtorSerialInputData>()) }
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
url: String,
filename: String,
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
inputProvider: InputProvider,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
filename,
inputProvider,
resultDeserializer,
mimetype,
additionalParametersBuilder = {
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
append(
"data",
InputProvider(serialized.size.toLong()) {
ByteReadPacket(serialized)
},
Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
},
dataHeadersBuilder,
requestBuilder,
serialFormat
)
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <ResultType> HttpClient.unimultipart(
url: String,
mppFile: MPPFile,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
mppFile.filename.string,
mppFile.inputProvider(),
resultDeserializer,
mimetype,
additionalParametersBuilder,
dataHeadersBuilder,
requestBuilder,
serialFormat
)
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <BodyType, ResultType> HttpClient.unimultipart(
url: String,
mppFile: MPPFile,
otherData: Pair<SerializationStrategy<BodyType>, BodyType>,
resultDeserializer: DeserializationStrategy<ResultType>,
mimetype: String = "*/*",
additionalParametersBuilder: FormBuilder.() -> Unit = {},
dataHeadersBuilder: HeadersBuilder.() -> Unit = {},
requestBuilder: HttpRequestBuilder.() -> Unit = {},
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
): ResultType = unimultipart(
url,
mppFile,
resultDeserializer,
mimetype,
additionalParametersBuilder = {
val serialized = serialFormat.encodeDefault(otherData.first, otherData.second)
append(
"data",
InputProvider(serialized.size.toLong()) {
ByteReadPacket(serialized)
},
Headers.build {
append(HttpHeaders.ContentType, ContentType.Application.Cbor.contentType)
append(HttpHeaders.ContentDisposition, "filename=data.bytes")
dataHeadersBuilder()
}
)
additionalParametersBuilder()
},
dataHeadersBuilder,
requestBuilder,
serialFormat
)

View File

@@ -0,0 +1,19 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.client.HttpClient
expect suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
): TemporalFileId
suspend fun UnifiedRequester.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (uploaded: Long, count: Long) -> Unit = { _, _ -> }
): TemporalFileId = client.tempUpload(
fullTempUploadDraftPath, file, onUpload
)

View File

@@ -0,0 +1,11 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.*
import io.ktor.client.request.forms.InputProvider
import io.ktor.utils.io.core.ByteReadPacket
actual suspend fun MPPFile.inputProvider(): InputProvider = bytes().let {
InputProvider(it.size.toLong()) {
ByteReadPacket(it)
}
}

View File

@@ -0,0 +1,59 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient
import kotlinx.coroutines.*
import org.w3c.dom.mediasource.ENDED
import org.w3c.dom.mediasource.ReadyState
import org.w3c.xhr.*
import org.w3c.xhr.XMLHttpRequest.Companion.DONE
suspend fun tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (Long, Long) -> Unit
): TemporalFileId {
val formData = FormData()
val answer = CompletableDeferred<TemporalFileId>(currentCoroutineContext().job)
formData.append(
"data",
file
)
val request = XMLHttpRequest()
request.responseType = XMLHttpRequestResponseType.TEXT
request.upload.onprogress = {
onUpload(it.loaded.toLong(), it.total.toLong())
}
request.onload = {
if (request.status == 200.toShort()) {
answer.complete(TemporalFileId(request.responseText))
} else {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
}
request.onerror = {
answer.completeExceptionally(Exception("Something went wrong: $it"))
}
request.open("POST", fullTempUploadDraftPath, true)
request.send(formData)
answer.invokeOnCompletion {
runCatching {
if (request.readyState != DONE) {
request.abort()
}
}
}
return answer.await()
}
actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (uploaded: Long, count: Long) -> Unit
): TemporalFileId = dev.inmo.micro_utils.ktor.client.tempUpload(fullTempUploadDraftPath, file, onUpload)

View File

@@ -0,0 +1,9 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.client.request.forms.InputProvider
import io.ktor.utils.io.streams.asInput
actual suspend fun MPPFile.inputProvider(): InputProvider = InputProvider(length()) {
inputStream().asInput()
}

View File

@@ -0,0 +1,40 @@
package dev.inmo.micro_utils.ktor.client
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.common.filename
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import io.ktor.client.HttpClient
import io.ktor.client.plugins.onUpload
import io.ktor.client.request.forms.formData
import io.ktor.client.request.forms.submitFormWithBinaryData
import io.ktor.client.statement.bodyAsText
import io.ktor.http.Headers
import io.ktor.http.HttpHeaders
import java.net.URLConnection
internal val MPPFile.mimeType: String
get() = URLConnection.getFileNameMap().getContentTypeFor(filename.name) ?: "*/*"
actual suspend fun HttpClient.tempUpload(
fullTempUploadDraftPath: String,
file: MPPFile,
onUpload: (Long, Long) -> Unit
): TemporalFileId {
val inputProvider = file.inputProvider()
val fileId = submitFormWithBinaryData(
fullTempUploadDraftPath,
formData = formData {
append(
"data",
inputProvider,
Headers.build {
append(HttpHeaders.ContentType, file.mimeType)
append(HttpHeaders.ContentDisposition, "filename=\"${file.filename.string}\"")
}
)
}
) {
onUpload(onUpload)
}.bodyAsText()
return TemporalFileId(fileId)
}

View File

@@ -10,8 +10,11 @@ kotlin {
sourceSets {
commonMain {
dependencies {
api "org.jetbrains.kotlinx:kotlinx-serialization-cbor:$kotlin_serialisation_core_version"
api "com.soywiz.korlibs.klock:klock:$klockVersion"
api internalProject("micro_utils.common")
api libs.kt.serialization.cbor
api libs.klock
api libs.uuid
api libs.ktor.io
}
}
}

View File

@@ -0,0 +1,6 @@
package dev.inmo.micro_utils.ktor.common
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.utils.io.core.Input
expect fun MPPFile.input(): Input

View File

@@ -1,3 +1,5 @@
@file:Suppress("NOTHING_TO_INLINE")
package dev.inmo.micro_utils.ktor.common
import kotlinx.serialization.*

View File

@@ -0,0 +1,10 @@
package dev.inmo.micro_utils.ktor.common
import kotlin.jvm.JvmInline
import kotlinx.serialization.Serializable
const val DefaultTemporalFilesSubPath = "temp_upload"
@Serializable
@JvmInline
value class TemporalFileId(val string: String)

View File

@@ -0,0 +1,7 @@
package dev.inmo.micro_utils.ktor.common
import dev.inmo.micro_utils.common.*
import io.ktor.utils.io.core.ByteReadPacket
import io.ktor.utils.io.core.Input
actual fun MPPFile.input(): Input = ByteReadPacket(readBytes())

View File

@@ -0,0 +1,7 @@
package dev.inmo.micro_utils.ktor.common
import dev.inmo.micro_utils.common.MPPFile
import io.ktor.utils.io.core.Input
import io.ktor.utils.io.streams.asInput
actual fun MPPFile.input(): Input = inputStream().asInput()

View File

@@ -16,10 +16,11 @@ kotlin {
jvmMain {
dependencies {
api "io.ktor:ktor-server:$ktor_version"
api "io.ktor:ktor-server-cio:$ktor_version"
api "io.ktor:ktor-server-host-common:$ktor_version"
api "io.ktor:ktor-websockets:$ktor_version"
api libs.ktor.server
api libs.ktor.server.cio
api libs.ktor.server.host.common
api libs.ktor.server.websockets
api libs.ktor.server.statusPages
}
}
}

View File

@@ -0,0 +1,15 @@
package dev.inmo.micro_utils.ktor.server
import io.ktor.server.application.ApplicationCall
import io.ktor.server.response.responseType
import io.ktor.util.InternalAPI
import io.ktor.util.reflect.TypeInfo
@InternalAPI
suspend fun <T : Any> ApplicationCall.respond(
message: T,
typeInfo: TypeInfo
) {
response.responseType = typeInfo
response.pipeline.execute(this, message as Any)
}

View File

@@ -2,42 +2,60 @@ package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.http.cio.websocket.*
import io.ktor.routing.Route
import io.ktor.websocket.webSocket
import io.ktor.http.URLProtocol
import io.ktor.server.application.install
import io.ktor.server.application.pluginOrNull
import io.ktor.server.routing.Route
import io.ktor.server.routing.application
import io.ktor.server.websocket.*
import io.ktor.websocket.send
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.serialization.SerializationStrategy
private suspend fun DefaultWebSocketSession.checkReceivedAndCloseIfExists() {
if (incoming.tryReceive() != null) {
close()
throw CorrectCloseException
}
}
@Deprecated("This method will be removed soon")
fun <T> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
converter: (T) -> StandardKtorSerialInputData
protocol: URLProtocol? = null,
converter: suspend WebSocketServerSession.(T) -> StandardKtorSerialInputData?
) {
webSocket(suburl) {
application.apply {
pluginOrNull(WebSockets) ?: install(WebSockets)
}
webSocket(suburl, protocol ?.name) {
safely {
flow.collect {
send(converter(it))
converter(it) ?.let { data ->
send(data)
}
}
}
}
}
@Deprecated("This method will be removed soon")
fun <T> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
serializer: SerializationStrategy<T>,
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat
serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
protocol: URLProtocol? = null,
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
) = includeWebsocketHandling(
suburl,
flow
) {
serialFormat.encodeDefault(serializer, it)
}
flow,
protocol,
converter = if (filter == null) {
{
serialFormat.encodeDefault(serializer, it)
}
} else {
{
if (filter(it)) {
serialFormat.encodeDefault(serializer, it)
} else {
null
}
}
}
)

View File

@@ -0,0 +1,31 @@
package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.http.URLProtocol
import io.ktor.server.application.install
import io.ktor.server.application.pluginOrNull
import io.ktor.server.routing.Route
import io.ktor.server.routing.application
import io.ktor.server.websocket.*
import io.ktor.websocket.send
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.SerializationStrategy
inline fun <reified T : Any> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
protocol: URLProtocol? = null,
noinline dataMapper: suspend WebSocketServerSession.(T) -> T? = { it }
) {
application.apply {
pluginOrNull(WebSockets) ?: install(WebSockets)
}
webSocket(suburl, protocol ?.name) {
safely {
flow.collect {
sendSerialized(dataMapper(it) ?: return@collect)
}
}
}
}

View File

@@ -1,28 +1,36 @@
package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.common.*
import dev.inmo.micro_utils.coroutines.safely
import dev.inmo.micro_utils.ktor.common.*
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.response.respondBytes
import io.ktor.routing.Route
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.request.receive
import io.ktor.server.request.receiveMultipart
import io.ktor.server.response.respond
import io.ktor.server.response.respondBytes
import io.ktor.server.routing.Route
import io.ktor.server.websocket.WebSocketServerSession
import io.ktor.util.pipeline.PipelineContext
import io.ktor.utils.io.core.*
import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.*
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
@Deprecated("This class method will be removed soon. It is now recommended to use built-in ktor features instead")
class UnifiedRouter(
private val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
private val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
val serialFormat: StandardKtorSerialFormat = standardKtorSerialFormat,
val serialFormatContentType: ContentType = standardKtorSerialFormatContentType
) {
fun <T> Route.includeWebsocketHandling(
suburl: String,
flow: Flow<T>,
serializer: SerializationStrategy<T>
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat)
serializer: SerializationStrategy<T>,
protocol: URLProtocol? = null,
filter: (suspend WebSocketServerSession.(T) -> Boolean)? = null
) = includeWebsocketHandling(suburl, flow, serializer, serialFormat, protocol, filter)
suspend fun <T> PipelineContext<*, ApplicationCall>.unianswer(
answerSerializer: SerializationStrategy<T>,
@@ -81,10 +89,16 @@ class UnifiedRouter(
call.respond(HttpStatusCode.BadRequest, "Request query parameters must contains $field")
}
}
companion object {
val default
get() = defaultUnifiedRouter
}
}
val defaultUnifiedRouter = UnifiedRouter()
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <T> ApplicationCall.unianswer(
answerSerializer: SerializationStrategy<T>,
answer: T
@@ -95,6 +109,7 @@ suspend fun <T> ApplicationCall.unianswer(
)
}
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <T> ApplicationCall.uniload(
deserializer: DeserializationStrategy<T>
) = safely {
@@ -104,6 +119,153 @@ suspend fun <T> ApplicationCall.uniload(
)
}
suspend fun ApplicationCall.uniloadMultipart(
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryChannelItem: (PartData.BinaryChannelItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
) = safely {
val multipartData = receiveMultipart()
var resultInput: Input? = null
multipartData.forEachPart {
when (it) {
is PartData.FormItem -> onFormItem(it)
is PartData.FileItem -> {
when (it.name) {
"bytes" -> resultInput = it.provider()
else -> onCustomFileItem(it)
}
}
is PartData.BinaryItem -> onBinaryContent(it)
is PartData.BinaryChannelItem -> onBinaryChannelItem(it)
}
}
resultInput ?: error("Bytes has not been received")
}
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <T> ApplicationCall.uniloadMultipart(
deserializer: DeserializationStrategy<T>,
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryChannelItem: (PartData.BinaryChannelItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {}
): Pair<Input, T> {
var data: Optional<T>? = null
val resultInput = uniloadMultipart(
onFormItem,
{
if (it.name == "data") {
data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
} else {
onCustomFileItem(it)
}
},
onBinaryChannelItem,
onBinaryContent
)
val completeData = data ?: error("Data has not been received")
return resultInput to (completeData.dataOrNull().let { it as T })
}
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <T> ApplicationCall.uniloadMultipartFile(
deserializer: DeserializationStrategy<T>,
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryChannelItem: (PartData.BinaryChannelItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
) = safely {
val multipartData = receiveMultipart()
var resultInput: MPPFile? = null
var data: Optional<T>? = null
multipartData.forEachPart {
when (it) {
is PartData.FormItem -> onFormItem(it)
is PartData.FileItem -> {
when (it.name) {
"bytes" -> {
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
resultInput = MPPFile.createTempFile(
name.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${name.extension}"
).apply {
outputStream().use { fileStream ->
it.streamProvider().use {
it.copyTo(fileStream)
}
}
}
}
"data" -> data = standardKtorSerialFormat.decodeDefault(deserializer, it.provider().readBytes()).optional
else -> onCustomFileItem(it)
}
}
is PartData.BinaryItem -> onBinaryContent(it)
is PartData.BinaryChannelItem -> onBinaryChannelItem(it)
}
}
val completeData = data ?: error("Data has not been received")
(resultInput ?: error("Bytes has not been received")) to (completeData.dataOrNull().let { it as T })
}
suspend fun ApplicationCall.uniloadMultipartFile(
onFormItem: (PartData.FormItem) -> Unit = {},
onCustomFileItem: (PartData.FileItem) -> Unit = {},
onBinaryChannelItem: (PartData.BinaryChannelItem) -> Unit = {},
onBinaryContent: (PartData.BinaryItem) -> Unit = {},
) = safely {
val multipartData = receiveMultipart()
var resultInput: MPPFile? = null
multipartData.forEachPart {
when (it) {
is PartData.FormItem -> onFormItem(it)
is PartData.FileItem -> {
if (it.name == "bytes") {
val name = FileName(it.originalFileName ?: error("File name is unknown for default part"))
resultInput = MPPFile.createTempFile(
name.nameWithoutExtension.let {
var resultName = it
while (resultName.length < 3) {
resultName += "_"
}
resultName
},
".${name.extension}"
).apply {
outputStream().use { fileStream ->
it.streamProvider().use {
it.copyTo(fileStream)
}
}
}
} else {
onCustomFileItem(it)
}
}
is PartData.BinaryItem -> onBinaryContent(it)
is PartData.BinaryChannelItem -> onBinaryChannelItem(it)
}
}
resultInput ?: error("Bytes has not been received")
}
suspend fun ApplicationCall.getParameterOrSendError(
field: String
) = parameters[field].also {
@@ -124,6 +286,7 @@ suspend fun ApplicationCall.getQueryParameterOrSendError(
}
}
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
fun <T> ApplicationCall.decodeUrlQueryValue(
field: String,
deserializer: DeserializationStrategy<T>
@@ -134,6 +297,7 @@ fun <T> ApplicationCall.decodeUrlQueryValue(
)
}
@Deprecated("This method will be removed soon. It is now recommended to use built-in ktor features instead")
suspend fun <T> ApplicationCall.decodeUrlQueryValueOrSendError(
field: String,
deserializer: DeserializationStrategy<T>

View File

@@ -1,8 +1,9 @@
package dev.inmo.micro_utils.ktor.server
import dev.inmo.micro_utils.ktor.server.configurators.KtorApplicationConfigurator
import io.ktor.application.Application
import io.ktor.server.application.Application
import io.ktor.server.cio.CIO
import io.ktor.server.cio.CIOApplicationEngine
import io.ktor.server.engine.*
import kotlin.random.Random
@@ -10,17 +11,21 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
block: Application.() -> Unit
): TEngine {
val env = applicationEngineEnvironment {
): TEngine = embeddedServer(
engine,
applicationEngineEnvironment {
module(block)
connector {
this@connector.host = host
this@connector.port = port
this.host = host
this.port = port
}
}
return embeddedServer(engine, env)
}
additionalEngineEnvironmentConfigurator()
},
additionalConfigurationConfigurator
)
/**
* Create server with [CIO] server engine without starting of it
@@ -30,18 +35,31 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
fun createKtorServer(
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
block: Application.() -> Unit
): ApplicationEngine = createKtorServer(CIO, host, port, block)
): CIOApplicationEngine = createKtorServer(
CIO,
host,
port,
additionalEngineEnvironmentConfigurator,
additionalConfigurationConfigurator,
block
)
fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration> createKtorServer(
engine: ApplicationEngineFactory<TEngine, TConfiguration>,
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: TConfiguration.() -> Unit = {},
configurators: List<KtorApplicationConfigurator>
): TEngine = createKtorServer(
engine,
host,
port
port,
additionalEngineEnvironmentConfigurator,
additionalConfigurationConfigurator
) {
configurators.forEach { it.apply { configure() } }
}
@@ -54,5 +72,7 @@ fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configurati
fun createKtorServer(
host: String = "localhost",
port: Int = Random.nextInt(1024, 65535),
configurators: List<KtorApplicationConfigurator>
): ApplicationEngine = createKtorServer(CIO, host, port, configurators)
configurators: List<KtorApplicationConfigurator>,
additionalEngineEnvironmentConfigurator: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
additionalConfigurationConfigurator: CIOApplicationEngine.Configuration.() -> Unit = {},
): ApplicationEngine = createKtorServer(CIO, host, port, additionalEngineEnvironmentConfigurator, additionalConfigurationConfigurator, configurators)

View File

@@ -0,0 +1,133 @@
package dev.inmo.micro_utils.ktor.server
import com.benasher44.uuid.uuid4
import dev.inmo.micro_utils.common.FileName
import dev.inmo.micro_utils.common.MPPFile
import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions
import dev.inmo.micro_utils.ktor.common.DefaultTemporalFilesSubPath
import dev.inmo.micro_utils.ktor.common.TemporalFileId
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.PartData
import io.ktor.http.content.streamProvider
import io.ktor.server.application.call
import io.ktor.server.request.receiveMultipart
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.Route
import io.ktor.server.routing.post
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
import java.nio.file.Files
import java.nio.file.attribute.FileTime
class TemporalFilesRoutingConfigurator(
private val subpath: String = DefaultTemporalFilesSubPath,
private val unifiedRouter: UnifiedRouter = UnifiedRouter.default,
private val temporalFilesUtilizer: TemporalFilesUtilizer = TemporalFilesUtilizer
) : ApplicationRoutingConfigurator.Element {
interface TemporalFilesUtilizer {
fun start(filesMap: MutableMap<TemporalFileId, MPPFile>, filesMutex: Mutex, onNewFileFlow: Flow<TemporalFileId>): Job
companion object : TemporalFilesUtilizer {
class ByTimerUtilizer(
private val removeMillis: Long,
private val scope: CoroutineScope
) : TemporalFilesUtilizer {
override fun start(
filesMap: MutableMap<TemporalFileId, MPPFile>,
filesMutex: Mutex,
onNewFileFlow: Flow<TemporalFileId>
): Job = scope.launchSafelyWithoutExceptions {
while (isActive) {
val filesWithCreationInfo = filesMap.mapNotNull { (fileId, file) ->
fileId to ((Files.getAttribute(file.toPath(), "creationTime") as? FileTime) ?.toMillis() ?: return@mapNotNull null)
}
if (filesWithCreationInfo.isEmpty()) {
delay(removeMillis)
continue
}
var min = filesWithCreationInfo.first()
for (fileWithCreationInfo in filesWithCreationInfo) {
if (fileWithCreationInfo.second < min.second) {
min = fileWithCreationInfo
}
}
delay(System.currentTimeMillis() - (min.second + removeMillis))
filesMutex.withLock {
filesMap.remove(min.first)
} ?.delete()
}
}
}
override fun start(
filesMap: MutableMap<TemporalFileId, MPPFile>,
filesMutex: Mutex,
onNewFileFlow: Flow<TemporalFileId>
): Job = Job()
}
}
private val temporalFilesMap = mutableMapOf<TemporalFileId, MPPFile>()
private val temporalFilesMutex = Mutex()
private val filesFlow = MutableSharedFlow<TemporalFileId>()
val utilizerJob = temporalFilesUtilizer.start(temporalFilesMap, temporalFilesMutex, filesFlow.asSharedFlow())
override fun Route.invoke() {
post(subpath) {
unifiedRouter.apply {
val multipart = call.receiveMultipart()
var fileInfo: Pair<TemporalFileId, MPPFile>? = null
var part = multipart.readPart()
while (part != null) {
if (part is PartData.FileItem) {
break
}
part = multipart.readPart()
}
part ?.let {
if (it is PartData.FileItem) {
val fileId = TemporalFileId(uuid4().toString())
val fileName = it.originalFileName ?.let { FileName(it) } ?: return@let
fileInfo = fileId to File.createTempFile(fileId.string, ".${fileName.extension}").apply {
outputStream().use { outputStream ->
it.streamProvider().use {
it.copyTo(outputStream)
}
}
deleteOnExit()
}
}
}
fileInfo ?.also { (fileId, file) ->
temporalFilesMutex.withLock {
temporalFilesMap[fileId] = file
}
call.respondText(fileId.string)
launchSafelyWithoutExceptions { filesFlow.emit(fileId) }
} ?: call.respond(HttpStatusCode.BadRequest)
}
}
}
suspend fun removeTemporalFile(temporalFileId: TemporalFileId) {
temporalFilesMutex.withLock {
temporalFilesMap.remove(temporalFileId)
}
}
fun getTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMap[temporalFileId]
suspend fun getAndRemoveTemporalFile(temporalFileId: TemporalFileId) = temporalFilesMutex.withLock {
temporalFilesMap.remove(temporalFileId)
}
}

View File

@@ -1,14 +1,15 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.features.CachingHeaders
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.cachingheaders.CachingHeaders
import io.ktor.server.plugins.cachingheaders.CachingHeadersConfig
import kotlinx.serialization.Contextual
data class ApplicationCachingHeadersConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun CachingHeaders.Configuration.invoke() }
fun interface Element { operator fun CachingHeadersConfig.invoke() }
override fun Application.configure() {
install(CachingHeaders) {

View File

@@ -1,8 +1,9 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.*
import io.ktor.routing.Route
import io.ktor.routing.Routing
import dev.inmo.micro_utils.ktor.server.configurators.ApplicationRoutingConfigurator.Element
import io.ktor.server.application.*
import io.ktor.server.routing.Route
import io.ktor.server.routing.Routing
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@@ -18,7 +19,7 @@ class ApplicationRoutingConfigurator(
}
override fun Application.configure() {
featureOrNull(Routing) ?.apply {
pluginOrNull(Routing) ?.apply {
rootInstaller.apply { invoke() }
} ?: install(Routing) {
rootInstaller.apply { invoke() }

View File

@@ -1,14 +1,15 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.sessions.Sessions
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.sessions.Sessions
import io.ktor.server.sessions.SessionsConfig
import kotlinx.serialization.Contextual
class ApplicationSessionsConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun Sessions.Configuration.invoke() }
fun interface Element { operator fun SessionsConfig.invoke() }
override fun Application.configure() {
install(Sessions) {

View File

@@ -1,6 +1,6 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.Application
import io.ktor.server.application.Application
interface KtorApplicationConfigurator {
fun Application.configure()

View File

@@ -1,14 +1,15 @@
package dev.inmo.micro_utils.ktor.server.configurators
import io.ktor.application.Application
import io.ktor.application.install
import io.ktor.features.StatusPages
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.plugins.statuspages.StatusPagesConfig
import kotlinx.serialization.Contextual
class StatusPagesConfigurator(
private val elements: List<@Contextual Element>
) : KtorApplicationConfigurator {
fun interface Element { operator fun StatusPages.Configuration.invoke() }
fun interface Element { operator fun StatusPagesConfig.invoke() }
override fun Application.configure() {
install(StatusPages) {

View File

@@ -4,8 +4,8 @@ buildscript {
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
classpath libs.buildscript.kt.gradle
classpath libs.buildscript.kt.serialization
}
}
@@ -16,11 +16,11 @@ plugins {
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
implementation libs.kt.stdlib
implementation libs.kt.serialization
implementation "io.ktor:ktor-client-core:$ktor_version"
implementation "io.ktor:ktor-client-java:$ktor_version"
implementation libs.ktor.client
implementation libs.ktor.client.java
}
mainClassName="MainKt"

View File

@@ -1,5 +1,6 @@
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
@@ -164,7 +165,7 @@ suspend fun main(vararg args: String) {
val ietfLanguageCodes = json.decodeFromString(
ListSerializer(LanguageCode.serializer()),
client.get(ietfLanguageCodesLink)
client.get(ietfLanguageCodesLink).bodyAsText()
).map {
it.copy(
title = it.title
@@ -175,7 +176,7 @@ suspend fun main(vararg args: String) {
}
val ietfLanguageCodesWithTagsMap = json.decodeFromString(
ListSerializer(LanguageCodeWithTag.serializer()),
client.get(ietfLanguageCodesAdditionalTagsLink)
client.get(ietfLanguageCodesAdditionalTagsLink).bodyAsText()
).filter { it.withSubtag != it.tag }.groupBy { it.tag }
val tags = ietfLanguageCodes.map {

View File

@@ -1,7 +1,6 @@
package dev.inmo.micro_utils.mime_types
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@@ -16,6 +15,7 @@ fun mimeType(raw: String) = mimesCache.getOrPut(raw) {
internal fun parseMimeType(raw: String): MimeType = CustomMimeType(raw)
@Suppress("OPT_IN_USAGE")
@Serializer(MimeType::class)
object MimeTypeSerializer : KSerializer<MimeType> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("mimeType", PrimitiveKind.STRING)

View File

@@ -23,7 +23,7 @@ kotlin {
commonMain {
dependencies {
implementation kotlin('stdlib')
api "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialisation_core_version"
implementation libs.kt.serialization
}
}
commonTest {
@@ -46,8 +46,8 @@ kotlin {
androidTest {
dependencies {
implementation kotlin('test-junit')
implementation "androidx.test.ext:junit:$test_ext_junit_version"
implementation "androidx.test.espresso:espresso-core:$espresso_core"
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
}

View File

@@ -0,0 +1,72 @@
project.version = "$version"
project.group = "$group"
apply from: "$publishGradlePath"
kotlin {
jvm {
compilations.main {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
js (IR) {
browser()
nodejs()
}
android {
publishAllLibraryVariants()
}
sourceSets {
commonMain {
dependencies {
implementation kotlin('stdlib')
implementation libs.kt.serialization
implementation compose.runtime
}
}
commonTest {
dependencies {
implementation kotlin('test-common')
implementation kotlin('test-annotations-common')
}
}
jvmMain {
dependencies {
implementation compose.desktop.currentOs
}
}
jvmTest {
dependencies {
implementation kotlin('test-junit')
}
}
jsMain {
dependencies {
implementation compose.web.core
}
}
jsTest {
dependencies {
implementation kotlin('test-js')
implementation kotlin('test-junit')
}
}
androidTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
}
}
apply from: "$defaultAndroidSettingsPresetPath"
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

View File

@@ -1,23 +1,60 @@
package dev.inmo.micro_utils.pagination
import kotlinx.serialization.Serializable
import kotlinx.serialization.*
import kotlin.math.ceil
/**
* @param page Current page number
* @param size Current page size. It can be greater than size of [results]
* @param results Result objects
* @param objectsNumber Count of all objects across all pages
*/
@Serializable
data class PaginationResult<T>(
override val page: Int,
val pagesNumber: Int,
override val size: Int,
val results: List<T>,
override val size: Int
) : Pagination
val objectsNumber: Long
) : Pagination {
/**
* Amount of pages for current pagination
*/
@EncodeDefault
val pagesNumber: Int = ceil(objectsNumber / size.toFloat()).toInt()
fun <T> emptyPaginationResult() = PaginationResult<T>(0, 0, emptyList(), 0)
constructor(
page: Int,
results: List<T>,
pagesNumber: Int,
size: Int
) : this(
page,
size,
results,
(pagesNumber * size).toLong()
)
@Deprecated("Replace with The other order of incoming parameters or objectsCount parameter")
constructor(
page: Int,
pagesNumber: Int,
results: List<T>,
size: Int
) : this(
page,
results,
pagesNumber,
size
)
}
fun <T> emptyPaginationResult() = PaginationResult<T>(0, 0, emptyList(), 0L)
/**
* @return New [PaginationResult] with [data] without checking of data sizes equality
*/
fun <I, O> PaginationResult<I>.changeResultsUnchecked(
data: List<O>
): PaginationResult<O> = PaginationResult(page, pagesNumber, data, size)
): PaginationResult<O> = PaginationResult(page, size, data, objectsNumber)
/**
* @return New [PaginationResult] with [data] <b>with</b> checking of data sizes equality
*/
@@ -33,12 +70,9 @@ fun <T> List<T>.createPaginationResult(
commonObjectsNumber: Long
) = PaginationResult(
pagination.page,
calculatePagesNumber(
commonObjectsNumber,
pagination.size
),
pagination.size,
this,
pagination.size
commonObjectsNumber
)
fun <T> List<T>.createPaginationResult(
@@ -46,12 +80,9 @@ fun <T> List<T>.createPaginationResult(
commonObjectsNumber: Long
) = PaginationResult(
calculatePage(firstIndex, size),
calculatePagesNumber(
commonObjectsNumber,
size
),
size,
this,
size
commonObjectsNumber
)
fun <T> Pair<Long, List<T>>.createPaginationResult(

View File

@@ -26,6 +26,10 @@ inline fun Pagination.nextPage() =
size
)
/**
* @param page Current page number
* @param size Current page size
*/
@Serializable
data class SimplePagination(
override val page: Int,

View File

@@ -27,4 +27,5 @@ inline fun <T> PaginationResult<T>.thisPageIfNotEmpty(): PaginationResult<T>? =
null
}
@Suppress("NOTHING_TO_INLINE")
inline fun <T> PaginationResult<T>.currentPageIfNotEmpty() = thisPageIfNotEmpty()

View File

@@ -2,19 +2,19 @@ package dev.inmo.micro_utils.pagination.utils
import dev.inmo.micro_utils.pagination.*
suspend fun <T> doForAll(
inline fun <T> doForAll(
initialPagination: Pagination = FirstPagePagination(),
paginationMapper: (PaginationResult<T>) -> Pagination?,
block: suspend (Pagination) -> PaginationResult<T>
block: (Pagination) -> PaginationResult<T>
) {
doWithPagination(initialPagination) {
block(it).let(paginationMapper)
}
}
suspend fun <T> doForAllWithNextPaging(
inline fun <T> doForAllWithNextPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T>
block: (Pagination) -> PaginationResult<T>
) {
doForAll(
initialPagination,
@@ -23,9 +23,9 @@ suspend fun <T> doForAllWithNextPaging(
)
}
suspend fun <T> doAllWithCurrentPaging(
inline fun <T> doAllWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T>
block: (Pagination) -> PaginationResult<T>
) {
doForAll(
initialPagination,
@@ -34,7 +34,7 @@ suspend fun <T> doAllWithCurrentPaging(
)
}
suspend fun <T> doForAllWithCurrentPaging(
inline fun <T> doForAllWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T>
block: (Pagination) -> PaginationResult<T>
) = doAllWithCurrentPaging(initialPagination, block)

View File

@@ -2,10 +2,10 @@ package dev.inmo.micro_utils.pagination.utils
import dev.inmo.micro_utils.pagination.*
suspend fun <T> getAll(
inline fun <T> getAll(
initialPagination: Pagination = FirstPagePagination(),
paginationMapper: (PaginationResult<T>) -> Pagination?,
block: suspend (Pagination) -> PaginationResult<T>
block: (Pagination) -> PaginationResult<T>
): List<T> {
val results = mutableListOf<T>()
doForAll(initialPagination, paginationMapper) {
@@ -16,46 +16,45 @@ suspend fun <T> getAll(
return results.toList()
}
suspend fun <T, R> R.getAllBy(
inline fun <T, R> R.getAllBy(
initialPagination: Pagination = FirstPagePagination(),
paginationMapper: R.(PaginationResult<T>) -> Pagination?,
block: suspend R.(Pagination) -> PaginationResult<T>
block: R.(Pagination) -> PaginationResult<T>
): List<T> = getAll(
initialPagination,
{ paginationMapper(it) },
{ block(it) }
)
suspend fun <T> getAllWithNextPaging(
inline fun <T> getAllWithNextPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T>
block: (Pagination) -> PaginationResult<T>
): List<T> = getAll(
initialPagination,
{ it.nextPageIfNotEmpty() },
block
)
suspend fun <T, R> R.getAllByWithNextPaging(
inline fun <T, R> R.getAllByWithNextPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend R.(Pagination) -> PaginationResult<T>
block: R.(Pagination) -> PaginationResult<T>
): List<T> = getAllWithNextPaging(
initialPagination,
{ block(it) }
)
suspend fun <T> getAllWithCurrentPaging(
inline fun <T> getAllWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend (Pagination) -> PaginationResult<T>
block: (Pagination) -> PaginationResult<T>
): List<T> = getAll(
initialPagination,
{ it.currentPageIfNotEmpty() },
block
)
suspend fun <T, R> R.getAllByWithCurrentPaging(
inline fun <T, R> R.getAllByWithCurrentPaging(
initialPagination: Pagination = FirstPagePagination(),
block: suspend R.(Pagination) -> PaginationResult<T>
block: R.(Pagination) -> PaginationResult<T>
): List<T> = getAllWithCurrentPaging(
initialPagination,
{ block(it) }
)
initialPagination
) { block(it) }

View File

@@ -38,3 +38,31 @@ fun <T> Set<T>.paginate(with: Pagination): PaginationResult<T> {
size.toLong()
)
}
fun <T> Iterable<T>.optionallyReverse(reverse: Boolean): Iterable<T> = when (this) {
is List<T> -> optionallyReverse(reverse)
is Set<T> -> optionallyReverse(reverse)
else -> if (reverse) {
reversed()
} else {
this
}
}
fun <T> List<T>.optionallyReverse(reverse: Boolean): List<T> = if (reverse) {
reversed()
} else {
this
}
fun <T> Set<T>.optionallyReverse(reverse: Boolean): Set<T> = if (reverse) {
reversed().toSet()
} else {
this
}
inline fun <reified T> Array<T>.optionallyReverse(reverse: Boolean) = if (reverse) {
Array(size) {
get(lastIndex - it)
}
} else {
this
}

View File

@@ -6,7 +6,7 @@ import dev.inmo.micro_utils.pagination.*
* Example:
*
* * `|__f__l_______________________|` will be transformed to `|_______________________f__l__|`
* * `|__f__l_|` will be transformed to `|__f__l_|`
* * `|__f__l_|` will be transformed to `|_f__l__|`
*
* @return Reversed version of this [Pagination]
*/
@@ -26,3 +26,15 @@ fun Pagination.reverse(datasetSize: Long): SimplePagination {
* Shortcut for [reverse]
*/
fun Pagination.reverse(objectsCount: Int) = reverse(objectsCount.toLong())
fun Pagination.optionallyReverse(objectsCount: Int, reverse: Boolean) = if (reverse) {
reverse(objectsCount)
} else {
this
}
fun Pagination.optionallyReverse(objectsCount: Long, reverse: Boolean) = if (reverse) {
reverse(objectsCount)
} else {
this
}

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