@_nazmiforreal/flutter-ota 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/bin/flutter-patcher.js +48 -39
  2. package/dart-src/packages/cli-tools/.gitkeep +0 -0
  3. package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
  4. package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
  5. package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
  6. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
  7. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
  8. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
  9. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
  10. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
  11. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
  13. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
  14. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
  15. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
  16. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
  17. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
  18. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
  19. package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
  20. package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
  21. package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
  22. package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
  23. package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
  24. package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  25. package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  26. package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  27. package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  28. package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  29. package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
  30. package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
  31. package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
  32. package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
  33. package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
  35. package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
  36. package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
  37. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
  38. package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
  39. package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
  40. package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
  41. package/dart-src/packages/core/lib/src/bundle.dart +191 -0
  42. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
  43. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
  44. package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
  45. package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
  46. package/dart-src/packages/core/lib/src/metadata.dart +31 -0
  47. package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
  48. package/dart-src/packages/core/lib/src/platform.dart +15 -0
  49. package/dart-src/packages/core/lib/src/rollout.dart +209 -0
  50. package/dart-src/packages/core/lib/src/semver.dart +70 -0
  51. package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
  52. package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
  53. package/dart-src/packages/core/lib/src/status.dart +24 -0
  54. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  55. package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
  56. package/dart-src/packages/core/lib/src/uuid.dart +45 -0
  57. package/dart-src/packages/core/pubspec.yaml +9 -0
  58. package/dart-src/packages/core/tool/dbg.dart +6 -0
  59. package/dart-src/plugins/aws/.gitkeep +0 -0
  60. package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
  61. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
  62. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
  63. package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
  64. package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
  65. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
  66. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
  67. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
  68. package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
  69. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
  70. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
  71. package/dart-src/plugins/aws/pubspec.yaml +22 -0
  72. package/dart-src/plugins/cloudflare/.gitkeep +0 -0
  73. package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
  74. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
  75. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
  76. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
  77. package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
  78. package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
  79. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
  80. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
  81. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
  82. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
  83. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
  84. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
  85. package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
  86. package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
  87. package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
  88. package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
  89. package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
  90. package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
  91. package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
  92. package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
  93. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
  94. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
  95. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
  96. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
  97. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
  98. package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
  99. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
  100. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
  101. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
  102. package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
  103. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
  104. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
  105. package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
  106. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
  107. package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
  108. package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
  109. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
  110. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
  111. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
  112. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
  113. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
  114. package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
  115. package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
  116. package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
  117. package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
  118. package/dart-src/plugins/postgres/.gitkeep +0 -0
  119. package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
  120. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
  121. package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
  122. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
  123. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
  124. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
  125. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
  126. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
  127. package/dart-src/plugins/postgres/pubspec.yaml +18 -0
  128. package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
  129. package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
  130. package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
  131. package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
  132. package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
  133. package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
  134. package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
  135. package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
  136. package/dart-src/plugins/standalone/.gitkeep +0 -0
  137. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
  138. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
  139. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
  140. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
  141. package/dart-src/plugins/standalone/pubspec.yaml +18 -0
  142. package/dart-src/plugins/supabase/lib/edge.dart +10 -0
  143. package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
  144. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
  145. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
  146. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
  147. package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
  148. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
  149. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
  150. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
  151. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
  152. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
  153. package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
  154. package/dart-src/plugins/supabase/pubspec.yaml +19 -0
  155. package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
  156. package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
  157. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
  158. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
  159. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
  160. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
  161. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
  162. package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
  163. package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
  164. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
  165. package/package.json +2 -1
  166. package/scripts/postinstall.js +53 -25
  167. /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
@@ -0,0 +1,241 @@
1
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart';
2
+
3
+ /// Build an [UpdateInfo] response from a [Bundle] and an [UpdateStatus].
4
+ UpdateInfo _makeResponse(Bundle bundle, UpdateStatus status) {
5
+ return UpdateInfo(
6
+ id: bundle.id,
7
+ message: bundle.message,
8
+ shouldForceUpdate: status == UpdateStatus.rollback
9
+ ? true
10
+ : bundle.shouldForceUpdate,
11
+ status: status,
12
+ storageUri: bundle.storageUri,
13
+ fileHash: bundle.fileHash,
14
+ );
15
+ }
16
+
17
+ /// Whether [bundle] is eligible for the user in [cohort].
18
+ bool _isEligibleUpdateCandidate(Bundle bundle, String? cohort) {
19
+ return isCohortEligibleForUpdate(
20
+ bundle.id,
21
+ cohort,
22
+ bundle.rolloutCohortCount,
23
+ bundle.targetCohorts,
24
+ );
25
+ }
26
+
27
+ /// Find the bundle with the largest ID that is newer than [bundleId]
28
+ /// and cohort-eligible.
29
+ Bundle? _findLatestEligibleUpdateCandidate(
30
+ List<Bundle> bundles,
31
+ String bundleId,
32
+ String? cohort,
33
+ ) {
34
+ Bundle? updateCandidate;
35
+
36
+ for (final bundle in bundles) {
37
+ if (bundle.id.compareTo(bundleId) > 0 &&
38
+ _isEligibleUpdateCandidate(bundle, cohort) &&
39
+ (updateCandidate == null ||
40
+ bundle.id.compareTo(updateCandidate.id) > 0)) {
41
+ updateCandidate = bundle;
42
+ }
43
+ }
44
+
45
+ return updateCandidate;
46
+ }
47
+
48
+ /// Default rollback response for init bundles (id = nilUuid).
49
+ const UpdateInfo _initBundleRollbackUpdateInfo = UpdateInfo(
50
+ message: null,
51
+ id: '00000000-0000-0000-0000-000000000000',
52
+ shouldForceUpdate: true,
53
+ status: UpdateStatus.rollback,
54
+ storageUri: null,
55
+ fileHash: null,
56
+ );
57
+
58
+ /// Resolve update info using the app-version strategy.
59
+ Future<UpdateInfo?> _appVersionStrategy(
60
+ List<Bundle> bundles,
61
+ AppVersionGetBundlesArgs args,
62
+ ) async {
63
+ final channel = args.channel;
64
+ final minBundleId = args.minBundleId;
65
+ final platform = args.platform;
66
+ final appVersion = args.appVersion;
67
+ final bundleId = args.bundleId;
68
+ final cohort = args.cohort;
69
+
70
+ final candidateBundles = <Bundle>[];
71
+
72
+ for (final b in bundles) {
73
+ if (b.platform != platform ||
74
+ b.channel != channel ||
75
+ b.targetAppVersion == null ||
76
+ !semverSatisfies(b.targetAppVersion!, appVersion) ||
77
+ !b.enabled ||
78
+ (minBundleId.isNotEmpty && b.id.compareTo(minBundleId) < 0)) {
79
+ continue;
80
+ }
81
+ candidateBundles.add(b);
82
+ }
83
+
84
+ if (candidateBundles.isEmpty) {
85
+ if (bundleId == nilUuid ||
86
+ (minBundleId.isNotEmpty && bundleId.compareTo(minBundleId) <= 0)) {
87
+ return null;
88
+ }
89
+ return _initBundleRollbackUpdateInfo;
90
+ }
91
+
92
+ Bundle? rollbackCandidate;
93
+ Bundle? currentBundle;
94
+
95
+ for (final b in candidateBundles) {
96
+ if (b.id == bundleId) {
97
+ currentBundle = b;
98
+ } else if (bundleId != nilUuid && b.id.compareTo(bundleId) < 0) {
99
+ if (rollbackCandidate == null ||
100
+ b.id.compareTo(rollbackCandidate.id) > 0) {
101
+ rollbackCandidate = b;
102
+ }
103
+ }
104
+ }
105
+
106
+ final updateCandidate = _findLatestEligibleUpdateCandidate(
107
+ candidateBundles,
108
+ bundleId,
109
+ cohort,
110
+ );
111
+ final currentBundleEligible =
112
+ currentBundle != null &&
113
+ _isEligibleUpdateCandidate(currentBundle, cohort);
114
+
115
+ if (bundleId == nilUuid) {
116
+ if (updateCandidate != null) {
117
+ return _makeResponse(updateCandidate, UpdateStatus.update);
118
+ }
119
+ return null;
120
+ }
121
+
122
+ if (currentBundleEligible) {
123
+ if (updateCandidate != null) {
124
+ return _makeResponse(updateCandidate, UpdateStatus.update);
125
+ }
126
+ return null;
127
+ }
128
+
129
+ if (updateCandidate != null) {
130
+ return _makeResponse(updateCandidate, UpdateStatus.update);
131
+ }
132
+ if (rollbackCandidate != null) {
133
+ return _makeResponse(rollbackCandidate, UpdateStatus.rollback);
134
+ }
135
+
136
+ if (minBundleId.isNotEmpty && bundleId.compareTo(minBundleId) <= 0) {
137
+ return null;
138
+ }
139
+ return _initBundleRollbackUpdateInfo;
140
+ }
141
+
142
+ /// Resolve update info using the fingerprint strategy.
143
+ Future<UpdateInfo?> _fingerprintStrategy(
144
+ List<Bundle> bundles,
145
+ FingerprintGetBundlesArgs args,
146
+ ) async {
147
+ final channel = args.channel;
148
+ final minBundleId = args.minBundleId;
149
+ final platform = args.platform;
150
+ final fingerprintHash = args.fingerprintHash;
151
+ final bundleId = args.bundleId;
152
+ final cohort = args.cohort;
153
+
154
+ final candidateBundles = <Bundle>[];
155
+
156
+ for (final b in bundles) {
157
+ if (b.platform != platform ||
158
+ b.channel != channel ||
159
+ b.fingerprintHash == null ||
160
+ b.fingerprintHash != fingerprintHash ||
161
+ !b.enabled ||
162
+ (minBundleId.isNotEmpty && b.id.compareTo(minBundleId) < 0)) {
163
+ continue;
164
+ }
165
+ candidateBundles.add(b);
166
+ }
167
+
168
+ if (candidateBundles.isEmpty) {
169
+ if (bundleId == nilUuid ||
170
+ (minBundleId.isNotEmpty && bundleId.compareTo(minBundleId) <= 0)) {
171
+ return null;
172
+ }
173
+ return _initBundleRollbackUpdateInfo;
174
+ }
175
+
176
+ Bundle? rollbackCandidate;
177
+ Bundle? currentBundle;
178
+
179
+ for (final b in candidateBundles) {
180
+ if (b.id == bundleId) {
181
+ currentBundle = b;
182
+ } else if (bundleId != nilUuid && b.id.compareTo(bundleId) < 0) {
183
+ if (rollbackCandidate == null ||
184
+ b.id.compareTo(rollbackCandidate.id) > 0) {
185
+ rollbackCandidate = b;
186
+ }
187
+ }
188
+ }
189
+
190
+ final updateCandidate = _findLatestEligibleUpdateCandidate(
191
+ candidateBundles,
192
+ bundleId,
193
+ cohort,
194
+ );
195
+ final currentBundleEligible =
196
+ currentBundle != null &&
197
+ _isEligibleUpdateCandidate(currentBundle, cohort);
198
+
199
+ if (bundleId == nilUuid) {
200
+ if (updateCandidate != null) {
201
+ return _makeResponse(updateCandidate, UpdateStatus.update);
202
+ }
203
+ return null;
204
+ }
205
+
206
+ if (currentBundleEligible) {
207
+ if (updateCandidate != null) {
208
+ return _makeResponse(updateCandidate, UpdateStatus.update);
209
+ }
210
+ return null;
211
+ }
212
+
213
+ if (updateCandidate != null) {
214
+ return _makeResponse(updateCandidate, UpdateStatus.update);
215
+ }
216
+ if (rollbackCandidate != null) {
217
+ return _makeResponse(rollbackCandidate, UpdateStatus.rollback);
218
+ }
219
+
220
+ if (minBundleId.isNotEmpty && bundleId.compareTo(minBundleId) <= 0) {
221
+ return null;
222
+ }
223
+ return _initBundleRollbackUpdateInfo;
224
+ }
225
+
226
+ /// Resolve update information from a list of candidate bundles.
227
+ ///
228
+ /// Dispatches to [AppVersionGetBundlesArgs] or [FingerprintGetBundlesArgs]
229
+ /// strategy based on [args].
230
+ Future<UpdateInfo?> getUpdateInfo(
231
+ List<Bundle> bundles,
232
+ GetBundlesArgs args,
233
+ ) async {
234
+ if (args is AppVersionGetBundlesArgs) {
235
+ return _appVersionStrategy(bundles, args);
236
+ }
237
+ if (args is FingerprintGetBundlesArgs) {
238
+ return _fingerprintStrategy(bundles, args);
239
+ }
240
+ return null;
241
+ }
@@ -0,0 +1,5 @@
1
+ /// Check whether a value is a plain Dart Map (not a List, not a class instance
2
+ /// with a different runtimeType).
3
+ ///
4
+ /// Faithful port of hot-updater `isObject.ts`.
5
+ bool isObject(Object? value) => value is Map<String, Object?>;
@@ -0,0 +1,9 @@
1
+ /// @internal
2
+ ///
3
+ /// Legacy manifest assets were stored below each bundle's `/files` directory
4
+ /// using their manifest-relative path. Keep all old-layout path decisions here
5
+ /// so support can be removed by deleting this module and the entrypoint branch
6
+ /// that imports it.
7
+ String getLegacyManifestAssetStoragePath({required String assetPath}) {
8
+ return assetPath;
9
+ }
@@ -0,0 +1,112 @@
1
+ import 'calculate_pagination.dart';
2
+ import 'query_bundles.dart';
3
+ import 'types.dart';
4
+
5
+ /// Paginate a list of bundles using offset or cursor-based pagination.
6
+ ///
7
+ /// Faithful port of hot-updater `paginateBundles.ts`.
8
+ Paginated<List<Bundle>> paginateBundles({
9
+ required List<Bundle> bundles,
10
+ required int limit,
11
+ int? offset,
12
+ DatabaseBundleCursor? cursor,
13
+ DatabaseBundleQueryOrder? orderBy,
14
+ }) {
15
+ final sortedBundles = sortBundles(bundles, orderBy);
16
+ final direction = orderBy?.direction ?? 'desc';
17
+ final total = sortedBundles.length;
18
+
19
+ // Offset-based pagination
20
+ if (offset != null) {
21
+ final normalizedOffset = offset < 0 ? 0 : offset;
22
+ final data = limit > 0
23
+ ? sortedBundles.sublist(
24
+ normalizedOffset,
25
+ normalizedOffset + limit > total ? total : normalizedOffset + limit,
26
+ )
27
+ : sortedBundles.sublist(normalizedOffset);
28
+ final pagination = calculatePagination(total, limit: limit, offset: normalizedOffset);
29
+ final nextCursor =
30
+ data.isNotEmpty && normalizedOffset + data.length < total
31
+ ? data.last.id
32
+ : null;
33
+ final previousCursor =
34
+ data.isNotEmpty && normalizedOffset > 0 ? data.first.id : null;
35
+
36
+ return Paginated(
37
+ data: data,
38
+ pagination: PaginationInfo(
39
+ total: pagination.total,
40
+ hasNextPage: pagination.hasNextPage,
41
+ hasPreviousPage: pagination.hasPreviousPage,
42
+ currentPage: pagination.currentPage,
43
+ totalPages: pagination.totalPages,
44
+ nextCursor: nextCursor,
45
+ previousCursor: previousCursor,
46
+ ),
47
+ );
48
+ }
49
+
50
+ // Cursor-based pagination
51
+ List<Bundle> data;
52
+ if (cursor?.after != null) {
53
+ final candidates = sortedBundles.where((bundle) {
54
+ return direction == 'desc'
55
+ ? bundle.id.compareTo(cursor!.after!) < 0
56
+ : bundle.id.compareTo(cursor!.after!) > 0;
57
+ }).toList();
58
+ data = limit > 0 ? candidates.sublist(0, limit > candidates.length ? candidates.length : limit) : candidates;
59
+ } else if (cursor?.before != null) {
60
+ final candidates = sortedBundles.where((bundle) {
61
+ return direction == 'desc'
62
+ ? bundle.id.compareTo(cursor!.before!) > 0
63
+ : bundle.id.compareTo(cursor!.before!) < 0;
64
+ }).toList();
65
+ data = limit > 0
66
+ ? candidates.sublist(
67
+ candidates.length - limit > 0 ? candidates.length - limit : 0,
68
+ )
69
+ : candidates;
70
+ } else {
71
+ data = limit > 0
72
+ ? sortedBundles.sublist(0, limit > total ? total : limit)
73
+ : List.from(sortedBundles);
74
+ }
75
+
76
+ final startIndex = data.isNotEmpty
77
+ ? sortedBundles.indexWhere((b) => b.id == data.first.id)
78
+ : cursor?.after != null
79
+ ? total
80
+ : 0;
81
+ final pagination = calculatePagination(total, limit: limit, offset: startIndex);
82
+ final nextCursor =
83
+ data.isNotEmpty && startIndex + data.length < total
84
+ ? data.last.id
85
+ : null;
86
+ final previousCursor =
87
+ data.isNotEmpty && startIndex > 0 ? data.first.id : null;
88
+
89
+ String? effectiveNextCursor = nextCursor;
90
+ String? effectivePreviousCursor = previousCursor;
91
+
92
+ // Edge case: empty result after cursor → preserve the cursor for navigation
93
+ if (data.isEmpty && cursor?.after != null) {
94
+ effectivePreviousCursor = cursor!.after;
95
+ }
96
+ if (data.isEmpty && cursor?.before != null) {
97
+ effectiveNextCursor = cursor!.before;
98
+ }
99
+
100
+ return Paginated(
101
+ data: data,
102
+ pagination: PaginationInfo(
103
+ total: pagination.total,
104
+ hasNextPage: pagination.hasNextPage,
105
+ hasPreviousPage: pagination.hasPreviousPage,
106
+ currentPage: pagination.currentPage,
107
+ totalPages: pagination.totalPages,
108
+ nextCursor: effectiveNextCursor,
109
+ previousCursor: effectivePreviousCursor,
110
+ ),
111
+ );
112
+ }
@@ -0,0 +1,44 @@
1
+ /// Parsed components of a storage URI.
2
+ class ParsedStorageUri {
3
+ const ParsedStorageUri({
4
+ required this.protocol,
5
+ required this.bucket,
6
+ required this.key,
7
+ });
8
+
9
+ final String protocol;
10
+ final String bucket;
11
+ final String key;
12
+ }
13
+
14
+ /// Decode percent-encoded storage object keys.
15
+ String decodeStorageObjectKey(String key) {
16
+ try {
17
+ return Uri.decodeComponent(key);
18
+ } catch (_) {
19
+ return key;
20
+ }
21
+ }
22
+
23
+ /// Parse a storage URI and validate the protocol.
24
+ ///
25
+ /// Example: `parseStorageUri("s3://my-bucket/path/to/file.zip", "s3")`
26
+ /// → `{ protocol: "s3", bucket: "my-bucket", key: "path/to/file.zip" }`
27
+ ///
28
+ /// Faithful port of hot-updater `parseStorageUri.ts`.
29
+ ParsedStorageUri parseStorageUri(String storageUri, String expectedProtocol) {
30
+ final uri = Uri.parse(storageUri);
31
+ final protocol = uri.scheme;
32
+
33
+ if (protocol != expectedProtocol) {
34
+ throw FormatException(
35
+ 'Invalid storage URI protocol. Expected $expectedProtocol, got $protocol',
36
+ );
37
+ }
38
+
39
+ return ParsedStorageUri(
40
+ protocol: protocol,
41
+ bucket: uri.host,
42
+ key: decodeStorageObjectKey(uri.path.startsWith('/') ? uri.path.substring(1) : uri.path),
43
+ );
44
+ }
@@ -0,0 +1,70 @@
1
+ import 'types.dart';
2
+
3
+ /// Compare a bundle ID field against an expected value with a comparator.
4
+ bool _compareValue(String value, String? expected, String comparator) {
5
+ if (expected == null) return true;
6
+ final cmp = value.compareTo(expected);
7
+ switch (comparator) {
8
+ case 'eq':
9
+ return cmp == 0;
10
+ case 'gt':
11
+ return cmp > 0;
12
+ case 'gte':
13
+ return cmp >= 0;
14
+ case 'lt':
15
+ return cmp < 0;
16
+ case 'lte':
17
+ return cmp <= 0;
18
+ default:
19
+ return true;
20
+ }
21
+ }
22
+
23
+ /// Check whether a bundle ID matches the given filter.
24
+ bool bundleIdMatchesFilter(String id, DatabaseBundleIdFilter? filter) {
25
+ if (filter == null) return true;
26
+ if (filter.ins != null && !filter.ins!.contains(id)) return false;
27
+ return _compareValue(id, filter.eq, 'eq') &&
28
+ _compareValue(id, filter.gt, 'gt') &&
29
+ _compareValue(id, filter.gte, 'gte') &&
30
+ _compareValue(id, filter.lt, 'lt') &&
31
+ _compareValue(id, filter.lte, 'lte');
32
+ }
33
+
34
+ /// Check whether a bundle matches all fields in a where clause.
35
+ bool bundleMatchesQueryWhere(Bundle bundle, DatabaseBundleQueryWhere? where) {
36
+ if (where == null) return true;
37
+ if (where.channel != null && bundle.channel != where.channel) return false;
38
+ if (where.platform != null && bundle.platform != where.platform) return false;
39
+ if (where.enabled != null && bundle.enabled != where.enabled) return false;
40
+ if (!bundleIdMatchesFilter(bundle.id, where.id)) return false;
41
+ if (where.targetAppVersionNotNull == true &&
42
+ bundle.targetAppVersion == null) {
43
+ return false;
44
+ }
45
+ if (where.targetAppVersion != null &&
46
+ bundle.targetAppVersion != where.targetAppVersion) {
47
+ return false;
48
+ }
49
+ if (where.targetAppVersionIn != null &&
50
+ !where.targetAppVersionIn!
51
+ .contains(bundle.targetAppVersion ?? '')) {
52
+ return false;
53
+ }
54
+ if (where.fingerprintHash != null &&
55
+ bundle.fingerprintHash != where.fingerprintHash) {
56
+ return false;
57
+ }
58
+ return true;
59
+ }
60
+
61
+ /// Sort bundles by ID in the specified direction.
62
+ List<Bundle> sortBundles(List<Bundle> bundles, DatabaseBundleQueryOrder? orderBy) {
63
+ final direction = orderBy?.direction ?? 'desc';
64
+ final sorted = List<Bundle>.from(bundles);
65
+ sorted.sort((a, b) {
66
+ final result = a.id.compareTo(b.id);
67
+ return direction == 'asc' ? result : -result;
68
+ });
69
+ return sorted;
70
+ }
@@ -0,0 +1,62 @@
1
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart' show Bundle;
2
+
3
+ import 'bundle_unit_of_work.dart';
4
+ import 'bundle_unit_of_work_store.dart';
5
+
6
+ /// Resolver interface for looking up bundles within a request context.
7
+ abstract class RequestUpdateBundleResolver {
8
+ bool get hasSeededBundles;
9
+ Bundle? peek(String bundleId);
10
+ Future<Bundle?> getById(
11
+ String bundleId,
12
+ Future<Bundle?> Function() loadBundleById,
13
+ );
14
+ }
15
+
16
+ List<Bundle> _toSeeds(List<Bundle?> seeds) =>
17
+ seeds.whereType<Bundle>().toList();
18
+
19
+ /// Seed bundles into the unit of work for a given context.
20
+ void seedRequestUpdateBundles(
21
+ Map<String, Object?>? context,
22
+ List<Bundle?> seeds,
23
+ ) {
24
+ final unitOfWork = getRequestBundleUnitOfWork(context);
25
+ if (unitOfWork == null) return;
26
+ final nextSeeds = _toSeeds(seeds);
27
+ if (nextSeeds.isEmpty) return;
28
+ unitOfWork.seed(nextSeeds);
29
+ }
30
+
31
+ /// Return the seeded bundles for the given context.
32
+ List<Bundle> getRequestUpdateBundleSeeds(Map<String, Object?>? context) {
33
+ return getRequestBundleUnitOfWork(context)?.seededBundles() ?? const [];
34
+ }
35
+
36
+ /// Create a resolver backed by a BundleUnitOfWork.
37
+ RequestUpdateBundleResolver createRequestUpdateBundleResolver(
38
+ Map<String, Object?>? context,
39
+ ) {
40
+ final unitOfWork =
41
+ getRequestBundleUnitOfWork(context) ?? BundleUnitOfWork();
42
+ return _RequestUpdateBundleResolverImpl(unitOfWork);
43
+ }
44
+
45
+ class _RequestUpdateBundleResolverImpl implements RequestUpdateBundleResolver {
46
+ _RequestUpdateBundleResolverImpl(this._unitOfWork);
47
+
48
+ final BundleUnitOfWork _unitOfWork;
49
+
50
+ @override
51
+ bool get hasSeededBundles => _unitOfWork.hasSeeds();
52
+
53
+ @override
54
+ Bundle? peek(String bundleId) => _unitOfWork.peek(bundleId);
55
+
56
+ @override
57
+ Future<Bundle?> getById(
58
+ String bundleId,
59
+ Future<Bundle?> Function() loadBundleById,
60
+ ) =>
61
+ _unitOfWork.getById(bundleId, loadBundleById);
62
+ }
@@ -0,0 +1,43 @@
1
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart'
2
+ show Bundle, GetBundlesArgs, UpdateInfo, nilUuid;
3
+
4
+ import 'get_update_info.dart' show getUpdateInfo;
5
+ import 'request_update_bundle_state.dart' show seedRequestUpdateBundles;
6
+
7
+ /// Options for [resolveUpdateInfoFromBundles].
8
+ class ResolveUpdateInfoFromBundlesOptions {
9
+ const ResolveUpdateInfoFromBundlesOptions({
10
+ required this.args,
11
+ required this.bundles,
12
+ this.context,
13
+ });
14
+
15
+ final GetBundlesArgs args;
16
+ final List<Bundle> bundles;
17
+ final Map<String, Object?>? context;
18
+ }
19
+
20
+ Bundle? _findSeedBundle(List<Bundle> bundles, String bundleId) {
21
+ for (final bundle in bundles) {
22
+ if (bundle.id == bundleId) return bundle;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ /// Resolve update information from pre-loaded bundles and seed the
28
+ /// request unit of work with the matched bundles.
29
+ Future<UpdateInfo?> resolveUpdateInfoFromBundles(
30
+ ResolveUpdateInfoFromBundlesOptions options,
31
+ ) async {
32
+ final info = await getUpdateInfo(options.bundles, options.args);
33
+ if (info == null) return null;
34
+
35
+ seedRequestUpdateBundles(options.context, [
36
+ _findSeedBundle(options.bundles, info.id),
37
+ options.args.bundleId == nilUuid
38
+ ? null
39
+ : _findSeedBundle(options.bundles, options.args.bundleId),
40
+ ]);
41
+
42
+ return info;
43
+ }
@@ -0,0 +1,27 @@
1
+ import 'types.dart';
2
+
3
+ /// Check whether a storage plugin provides a node profile.
4
+ bool isNodeStoragePlugin(StoragePlugin plugin) => plugin.profiles.hasNode;
5
+
6
+ /// Check whether a storage plugin provides a runtime profile.
7
+ bool isRuntimeStoragePlugin(StoragePlugin plugin) => plugin.profiles.hasRuntime;
8
+
9
+ /// Assert that a storage plugin provides a node profile.
10
+ void assertNodeStoragePlugin(StoragePlugin plugin) {
11
+ if (!isNodeStoragePlugin(plugin)) {
12
+ throw StateError(
13
+ '${plugin.name} does not implement the node storage profile '
14
+ 'for protocol "${plugin.supportedProtocol}".',
15
+ );
16
+ }
17
+ }
18
+
19
+ /// Assert that a storage plugin provides a runtime profile.
20
+ void assertRuntimeStoragePlugin(StoragePlugin plugin) {
21
+ if (!isRuntimeStoragePlugin(plugin)) {
22
+ throw StateError(
23
+ '${plugin.name} does not implement the runtime storage profile '
24
+ 'for protocol "${plugin.supportedProtocol}".',
25
+ );
26
+ }
27
+ }