@_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,853 @@
1
+ import 'dart:convert';
2
+
3
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart';
4
+
5
+ import 'bundle_unit_of_work.dart';
6
+ import 'create_database_plugin.dart';
7
+ import 'filter_compatible_app_versions.dart';
8
+ import 'paginate_bundles.dart';
9
+ import 'query_bundles.dart';
10
+ import 'resolve_update_info_from_bundles.dart';
11
+ import 'types.dart';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Internal types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ class _BundleWithKey {
18
+ _BundleWithKey(this.bundle, {required this.updateJsonKey});
19
+
20
+ final Bundle bundle;
21
+ final String updateJsonKey;
22
+ }
23
+
24
+ class _TargetVersionMutation {
25
+ _TargetVersionMutation({
26
+ required this.channel,
27
+ required this.platform,
28
+ });
29
+
30
+ final String channel;
31
+ final String platform;
32
+ final Set<String> additions = <String>{};
33
+ final Set<String> removals = <String>{};
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Concurrency helpers
38
+ // ---------------------------------------------------------------------------
39
+
40
+ const int _storageOperationConcurrency = 8;
41
+
42
+ Future<List<TResult>> _mapWithConcurrency<T, TResult>(
43
+ List<T> items,
44
+ int concurrency,
45
+ Future<TResult> Function(T item, int index) mapper,
46
+ ) async {
47
+ final results = List<TResult?>.filled(items.length, null);
48
+ var nextIndex = 0;
49
+ final workerCount = concurrency < items.length ? concurrency : items.length;
50
+
51
+ await Future.wait(
52
+ List.generate(workerCount, (_) async {
53
+ while (true) {
54
+ final index = nextIndex;
55
+ nextIndex += 1;
56
+ if (index >= items.length) break;
57
+ results[index] = await mapper(items[index], index);
58
+ }
59
+ }),
60
+ );
61
+
62
+ return results.cast<TResult>();
63
+ }
64
+
65
+ Future<void> _forEachWithConcurrency<T>(
66
+ List<T> items,
67
+ int concurrency,
68
+ Future<void> Function(T item, int index) mapper,
69
+ ) async {
70
+ for (var i = 0; i < items.length; i += concurrency) {
71
+ final batch = items.sublist(
72
+ i,
73
+ i + concurrency > items.length ? items.length : i + concurrency,
74
+ );
75
+ await Future.wait(
76
+ List.generate(batch.length, (j) => mapper(batch[j], i + j)),
77
+ );
78
+ }
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Helpers
83
+ // ---------------------------------------------------------------------------
84
+
85
+ Bundle _removeBundleInternalKeys(Bundle bundle) {
86
+ return Bundle(
87
+ id: bundle.id,
88
+ platform: bundle.platform,
89
+ shouldForceUpdate: bundle.shouldForceUpdate,
90
+ enabled: bundle.enabled,
91
+ fileHash: bundle.fileHash,
92
+ storageUri: bundle.storageUri,
93
+ channel: bundle.channel,
94
+ gitCommitHash: bundle.gitCommitHash,
95
+ message: bundle.message,
96
+ targetAppVersion: bundle.targetAppVersion,
97
+ fingerprintHash: bundle.fingerprintHash,
98
+ metadata: bundle.metadata,
99
+ manifestStorageUri: bundle.manifestStorageUri,
100
+ manifestFileHash: bundle.manifestFileHash,
101
+ assetBaseStorageUri: bundle.assetBaseStorageUri,
102
+ patches: bundle.patches,
103
+ patchBaseBundleId: bundle.patchBaseBundleId,
104
+ patchBaseFileHash: bundle.patchBaseFileHash,
105
+ patchFileHash: bundle.patchFileHashLegacy,
106
+ patchStorageUri: bundle.patchStorageUri,
107
+ rolloutCohortCount: bundle.rolloutCohortCount,
108
+ targetCohorts: bundle.targetCohorts,
109
+ );
110
+ }
111
+
112
+ String? normalizeTargetAppVersion(String? version) {
113
+ if (version == null || version.isEmpty) return null;
114
+
115
+ var normalized = version.replaceAll(RegExp(r'\s+'), ' ').trim();
116
+ normalized = normalized.replaceAllMapped(
117
+ RegExp(r'([><=~^]+)\s+(\d)'),
118
+ (m) => '${m.group(1)}${m.group(2)}',
119
+ );
120
+
121
+ return normalized;
122
+ }
123
+
124
+ bool _isExactVersion(String? version) {
125
+ if (version == null || version.isEmpty) return false;
126
+ final normalized = normalizeTargetAppVersion(version);
127
+ if (normalized == null) return false;
128
+ return SemVer.tryParse(normalized) != null;
129
+ }
130
+
131
+ List<String> _getSemverNormalizedVersions(String version) {
132
+ final normalized = normalizeTargetAppVersion(version) ?? version;
133
+ final coerced = semverCoerce(normalized);
134
+ if (coerced == null) return [normalized];
135
+
136
+ final versions = <String>{};
137
+ versions.add(coerced.toString());
138
+
139
+ if (coerced.patch == 0) {
140
+ versions.add('${coerced.major}.${coerced.minor}');
141
+ }
142
+ if (coerced.minor == 0 && coerced.patch == 0) {
143
+ versions.add('${coerced.major}');
144
+ }
145
+
146
+ return versions.toList();
147
+ }
148
+
149
+ String _resolveStorageTarget({
150
+ String? targetAppVersion,
151
+ String? fingerprintHash,
152
+ }) {
153
+ final target = normalizeTargetAppVersion(targetAppVersion) ?? fingerprintHash;
154
+ if (target == null || target.isEmpty) {
155
+ throw StateError('target not found');
156
+ }
157
+ return target;
158
+ }
159
+
160
+ List<String> _getManagementListPrefixes(DatabaseBundleQueryWhere? where) {
161
+ if (where?.channel != null &&
162
+ where?.platform != null &&
163
+ where?.targetAppVersion is String) {
164
+ final normalized =
165
+ normalizeTargetAppVersion(where!.targetAppVersion as String);
166
+ if (normalized != null) {
167
+ return ['${where.channel}/${where.platform?.value}/$normalized/'];
168
+ }
169
+ }
170
+
171
+ if (where?.channel != null && where?.platform != null) {
172
+ return ['${where!.channel}/${where.platform?.value}/'];
173
+ }
174
+
175
+ if (where?.channel != null) {
176
+ return ['${where!.channel}/'];
177
+ }
178
+
179
+ return [''];
180
+ }
181
+
182
+ String? _getChannelFromUpdateJsonKey(String key) {
183
+ final match =
184
+ RegExp(r'^([^/]+)/(?:ios|android)/[^/]+/update\.json$').firstMatch(key);
185
+ return match?.group(1);
186
+ }
187
+
188
+ const _defaultDescOrder =
189
+ DatabaseBundleQueryOrder(field: 'id', direction: 'desc');
190
+
191
+ List<Bundle> _sortManagedBundles(
192
+ List<Bundle> bundles,
193
+ DatabaseBundleQueryOrder? orderBy,
194
+ ) {
195
+ return sortBundles(bundles, orderBy ?? _defaultDescOrder);
196
+ }
197
+
198
+ Bundle _mergeBundleUpdate(Bundle base, Map<String, Object?> patch) {
199
+ const replaceOnUpdateKeys = ['patches', 'targetCohorts'];
200
+ final baseMap = base.toJson();
201
+ for (final key in patch.keys) {
202
+ final srcValue = patch[key];
203
+ if (replaceOnUpdateKeys.contains(key)) {
204
+ baseMap[key] = srcValue;
205
+ } else if (srcValue != null) {
206
+ baseMap[key] = srcValue;
207
+ }
208
+ }
209
+ return Bundle.fromJson(baseMap);
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // BlobOperations interface
214
+ // ---------------------------------------------------------------------------
215
+
216
+ abstract class BlobOperations {
217
+ Future<List<String>> listObjects(String prefix);
218
+ Future<T?> loadObject<T>(String key);
219
+ Future<void> uploadObject<T>(String key, T data);
220
+ Future<void> deleteObject(String key);
221
+ bool shouldSkipLoadObjectError(Object error, String key);
222
+ void validateChannel(String channel);
223
+ Future<void> invalidatePaths(List<String> paths);
224
+ String get apiBasePath;
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // createBlobDatabasePlugin
229
+ // ---------------------------------------------------------------------------
230
+
231
+ DatabasePlugin Function() Function(TConfig, [DatabasePluginHooks? hooks])
232
+ createBlobDatabasePlugin<TConfig>({
233
+ required String name,
234
+ required BlobOperations Function(TConfig config) blobFactory,
235
+ }) {
236
+ return (TConfig config, [DatabasePluginHooks? hooks]) {
237
+ final ops = blobFactory(config);
238
+
239
+ final bundlesMap = <String, _BundleWithKey>{};
240
+ final pendingBundlesMap = <String, _BundleWithKey>{};
241
+ final locallyDeletedBundleIds = <String>{};
242
+
243
+ final createPlugin = createDatabasePlugin<TConfig>(
244
+ name: name,
245
+ factory: (_) => _BlobDatabasePlugin(
246
+ hooks: hooks,
247
+ ops: ops,
248
+ bundlesMap: bundlesMap,
249
+ pendingBundlesMap: pendingBundlesMap,
250
+ locallyDeletedBundleIds: locallyDeletedBundleIds,
251
+ ),
252
+ )(config, hooks);
253
+
254
+ return () => createPlugin();
255
+ };
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Plugin implementation
260
+ // ---------------------------------------------------------------------------
261
+
262
+ class _BlobDatabasePlugin implements AbstractDatabasePlugin {
263
+ _BlobDatabasePlugin({
264
+ required this.hooks,
265
+ required this.ops,
266
+ required this.bundlesMap,
267
+ required this.pendingBundlesMap,
268
+ required this.locallyDeletedBundleIds,
269
+ });
270
+
271
+ final DatabasePluginHooks? hooks;
272
+ final BlobOperations ops;
273
+ final Map<String, _BundleWithKey> bundlesMap;
274
+ final Map<String, _BundleWithKey> pendingBundlesMap;
275
+ final Set<String> locallyDeletedBundleIds;
276
+
277
+ // -- Storage helpers --
278
+
279
+ Future<T?> loadOptionalObject<T>(String key) async {
280
+ try {
281
+ return await ops.loadObject<T>(key);
282
+ } catch (error) {
283
+ if (ops.shouldSkipLoadObjectError(error, key)) return null;
284
+ rethrow;
285
+ }
286
+ }
287
+
288
+ List<Bundle> bundlesFromKey(String key, List<Bundle> bundles) {
289
+ for (final bundle in bundles) {
290
+ if (locallyDeletedBundleIds.contains(bundle.id) ||
291
+ pendingBundlesMap.containsKey(bundle.id)) {
292
+ continue;
293
+ }
294
+ bundlesMap[bundle.id] = _BundleWithKey(bundle, updateJsonKey: key);
295
+ }
296
+ return bundles;
297
+ }
298
+
299
+ Future<List<Bundle>> loadBundleObject(String key) async {
300
+ final bundles = (await loadOptionalObject<List>(key))
301
+ ?.map((e) => Bundle.fromJson((e as Map).cast<String, dynamic>()))
302
+ .toList() ??
303
+ <Bundle>[];
304
+ bundlesFromKey(key, bundles);
305
+ return bundles;
306
+ }
307
+
308
+ Future<List<_BundleWithKey>> reloadBundles([
309
+ List<String> prefixes = const [''],
310
+ ]) async {
311
+ bundlesMap.clear();
312
+ pendingBundlesMap.clear();
313
+ locallyDeletedBundleIds.clear();
314
+
315
+ final updateJsonKeys = (await _mapWithConcurrency(
316
+ prefixes,
317
+ _storageOperationConcurrency,
318
+ (prefix, _) => ops.listObjects(prefix),
319
+ ))
320
+ .expand((keys) => keys)
321
+ .where((key) =>
322
+ RegExp(r'^[^/]+/(?:ios|android)/[^/]+/update\.json$')
323
+ .hasMatch(key))
324
+ .toList();
325
+
326
+ final allBundles = (await _mapWithConcurrency(
327
+ updateJsonKeys,
328
+ _storageOperationConcurrency,
329
+ (key, _) async {
330
+ final bundlesData = await loadBundleObject(key);
331
+ return bundlesData
332
+ .map((b) => _BundleWithKey(b, updateJsonKey: key))
333
+ .toList();
334
+ },
335
+ ))
336
+ .expand((b) => b)
337
+ .toList();
338
+
339
+ for (final entry in allBundles) {
340
+ bundlesMap[entry.bundle.id] = entry;
341
+ }
342
+
343
+ for (final entry in pendingBundlesMap.entries) {
344
+ bundlesMap[entry.key] = entry.value;
345
+ }
346
+
347
+ final sorted = List<_BundleWithKey>.from(bundlesMap.values);
348
+ sorted.sort((a, b) => b.bundle.id.compareTo(a.bundle.id));
349
+ return sorted;
350
+ }
351
+
352
+ Future<List<Bundle>> loadAllBundlesForManagementFallback(
353
+ DatabaseBundleQueryWhere? where,
354
+ ) async {
355
+ return _sortManagedBundles(
356
+ (await reloadBundles(_getManagementListPrefixes(where)))
357
+ .map((e) => _removeBundleInternalKeys(e.bundle))
358
+ .toList(),
359
+ null,
360
+ );
361
+ }
362
+
363
+ // -- Target version mutations --
364
+
365
+ _TargetVersionMutation _getTargetVersionMutation(
366
+ Map<String, _TargetVersionMutation> mutations,
367
+ Bundle bundle,
368
+ ) {
369
+ final key = '${bundle.channel}/${bundle.platform}';
370
+ var existing = mutations[key];
371
+ if (existing != null) return existing;
372
+ existing = _TargetVersionMutation(
373
+ channel: bundle.channel,
374
+ platform: bundle.platform.value,
375
+ );
376
+ mutations[key] = existing;
377
+ return existing;
378
+ }
379
+
380
+ void _addTargetVersionAddition(
381
+ Map<String, _TargetVersionMutation> mutations,
382
+ Bundle bundle,
383
+ ) {
384
+ final v = normalizeTargetAppVersion(bundle.targetAppVersion);
385
+ if (v == null) return;
386
+ _getTargetVersionMutation(mutations, bundle).additions.add(v);
387
+ }
388
+
389
+ void _addTargetVersionRemoval(
390
+ Map<String, _TargetVersionMutation> mutations,
391
+ Bundle bundle,
392
+ ) {
393
+ final v = normalizeTargetAppVersion(bundle.targetAppVersion);
394
+ if (v == null) return;
395
+ _getTargetVersionMutation(mutations, bundle).removals.add(v);
396
+ }
397
+
398
+ Future<void> applyTargetVersionMutations(
399
+ Map<String, _TargetVersionMutation> mutations,
400
+ ) async {
401
+ await Future.wait(
402
+ mutations.values.map((mutation) async {
403
+ final targetKey =
404
+ '${mutation.channel}/${mutation.platform}/target-app-versions.json';
405
+ final oldTargetVersions =
406
+ (await loadOptionalObject<List>(targetKey))
407
+ ?.map((e) => e.toString())
408
+ .toList() ??
409
+ <String>[];
410
+ final newTargetVersions = oldTargetVersions
411
+ .where((v) =>
412
+ !mutation.removals.contains(v) ||
413
+ mutation.additions.contains(v))
414
+ .toList();
415
+ for (final version in mutation.additions) {
416
+ if (!newTargetVersions.contains(version)) {
417
+ newTargetVersions.add(version);
418
+ }
419
+ }
420
+ if (jsonEncode(oldTargetVersions) != jsonEncode(newTargetVersions)) {
421
+ await ops.uploadObject(targetKey, newTargetVersions);
422
+ }
423
+ }),
424
+ );
425
+ }
426
+
427
+ // -- Invalidation paths --
428
+
429
+ void _addAppVersionInvalidationPaths(
430
+ Set<String> pathsToInvalidate, {
431
+ required String platform,
432
+ required String channel,
433
+ required String targetAppVersion,
434
+ }) {
435
+ if (!_isExactVersion(targetAppVersion)) {
436
+ pathsToInvalidate.add('${ops.apiBasePath}/app-version/$platform/*');
437
+ return;
438
+ }
439
+ for (final v in _getSemverNormalizedVersions(targetAppVersion)) {
440
+ pathsToInvalidate
441
+ .add('${ops.apiBasePath}/app-version/$platform/$v/$channel/*');
442
+ }
443
+ }
444
+
445
+ void _addLookupInvalidationPaths(
446
+ Set<String> pathsToInvalidate, {
447
+ required String platform,
448
+ required String channel,
449
+ String? targetAppVersion,
450
+ String? fingerprintHash,
451
+ }) {
452
+ if (fingerprintHash != null && fingerprintHash.isNotEmpty) {
453
+ pathsToInvalidate.add(
454
+ '${ops.apiBasePath}/fingerprint/$platform/$fingerprintHash/$channel/*',
455
+ );
456
+ return;
457
+ }
458
+ if (targetAppVersion != null && targetAppVersion.isNotEmpty) {
459
+ _addAppVersionInvalidationPaths(
460
+ pathsToInvalidate,
461
+ platform: platform,
462
+ channel: channel,
463
+ targetAppVersion: targetAppVersion,
464
+ );
465
+ }
466
+ }
467
+
468
+ // -- Update info resolvers --
469
+
470
+ Future<UpdateInfo?> _getAppVersionUpdateInfo(
471
+ AppVersionGetBundlesArgs args,
472
+ Map<String, Object?>? context,
473
+ ) async {
474
+ final channel = args.channel;
475
+ final platform = args.platform;
476
+
477
+ final targetVersionsKey = '$channel/${platform.value}/target-app-versions.json';
478
+ final targetAppVersions =
479
+ (await loadOptionalObject<List>(targetVersionsKey))
480
+ ?.map((e) => e.toString())
481
+ .toList() ??
482
+ <String>[];
483
+ final matchingVersions =
484
+ filterCompatibleAppVersions(targetAppVersions, args.appVersion);
485
+
486
+ final bundles = (await _mapWithConcurrency(
487
+ matchingVersions,
488
+ _storageOperationConcurrency,
489
+ (targetAppVersion, _) async {
490
+ final nv =
491
+ normalizeTargetAppVersion(targetAppVersion) ?? targetAppVersion;
492
+ return loadBundleObject('$channel/${platform.value}/$nv/update.json');
493
+ },
494
+ ))
495
+ .expand((b) => b)
496
+ .toList();
497
+
498
+ return resolveUpdateInfoFromBundles(
499
+ ResolveUpdateInfoFromBundlesOptions(
500
+ args: AppVersionGetBundlesArgs(
501
+ appVersion: args.appVersion,
502
+ bundleId: args.bundleId,
503
+ channel: channel,
504
+ cohort: args.cohort,
505
+ minBundleId: args.minBundleId,
506
+ platform: platform,
507
+ ),
508
+ bundles: bundles,
509
+ context: context,
510
+ ),
511
+ );
512
+ }
513
+
514
+ Future<UpdateInfo?> _getFingerprintUpdateInfo(
515
+ FingerprintGetBundlesArgs args,
516
+ Map<String, Object?>? context,
517
+ ) async {
518
+ final channel = args.channel;
519
+ final platform = args.platform;
520
+
521
+ final bundles = await loadBundleObject(
522
+ '$channel/${platform.value}/${args.fingerprintHash}/update.json',
523
+ );
524
+
525
+ return resolveUpdateInfoFromBundles(
526
+ ResolveUpdateInfoFromBundlesOptions(
527
+ args: FingerprintGetBundlesArgs(
528
+ bundleId: args.bundleId,
529
+ channel: channel,
530
+ cohort: args.cohort,
531
+ fingerprintHash: args.fingerprintHash,
532
+ minBundleId: args.minBundleId,
533
+ platform: platform,
534
+ ),
535
+ bundles: bundles,
536
+ context: context,
537
+ ),
538
+ );
539
+ }
540
+
541
+ // -- DatabasePlugin interface --
542
+
543
+ @override
544
+ Future<Bundle?> getBundleById(String bundleId) async {
545
+ if (locallyDeletedBundleIds.contains(bundleId)) return null;
546
+
547
+ final pending = pendingBundlesMap[bundleId];
548
+ if (pending != null) return _removeBundleInternalKeys(pending.bundle);
549
+
550
+ final cached = bundlesMap[bundleId];
551
+ if (cached != null) return _removeBundleInternalKeys(cached.bundle);
552
+
553
+ final bundles = await reloadBundles();
554
+ for (final entry in bundles) {
555
+ if (entry.bundle.id == bundleId) {
556
+ return _removeBundleInternalKeys(entry.bundle);
557
+ }
558
+ }
559
+ return null;
560
+ }
561
+
562
+ @override
563
+ Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) async {
564
+ if (args is AppVersionGetBundlesArgs) {
565
+ return _getAppVersionUpdateInfo(args, null);
566
+ }
567
+ return _getFingerprintUpdateInfo(args as FingerprintGetBundlesArgs, null);
568
+ }
569
+
570
+ @override
571
+ Future<Paginated<List<Bundle>>> getBundles(
572
+ DatabaseBundleQueryOptions options,
573
+ ) async {
574
+ var allBundles =
575
+ await loadAllBundlesForManagementFallback(options.where);
576
+ if (options.where != null) {
577
+ allBundles = allBundles
578
+ .where((b) => bundleMatchesQueryWhere(b, options.where))
579
+ .toList();
580
+ }
581
+ return paginateBundles(
582
+ bundles: allBundles,
583
+ limit: options.limit,
584
+ offset: options.offset ??
585
+ (options.page != null ? (options.page! - 1) * options.limit : null),
586
+ cursor: options.cursor,
587
+ orderBy: options.orderBy,
588
+ );
589
+ }
590
+
591
+ @override
592
+ Future<List<String>> getChannels() async {
593
+ final channels = (await ops.listObjects(''))
594
+ .map(_getChannelFromUpdateJsonKey)
595
+ .whereType<String>()
596
+ .toSet()
597
+ .toList()
598
+ ..sort();
599
+ return channels;
600
+ }
601
+
602
+ @override
603
+ Future<void> commitBundle({
604
+ required List<BundleChange> changedSets,
605
+ }) async {
606
+ if (changedSets.isEmpty) return;
607
+
608
+ for (final change in changedSets) {
609
+ if (change.operation == BundleChangeOperation.insert ||
610
+ (change.operation == BundleChangeOperation.update &&
611
+ change.data.channel.isNotEmpty)) {
612
+ ops.validateChannel(change.data.channel);
613
+ }
614
+ }
615
+
616
+ final changedBundlesByKey = <String, List<Bundle>>{};
617
+ final removalsByKey = <String, List<String>>{};
618
+ final targetVersionRemovalsByKey = <String, List<_BundleWithKey>>{};
619
+ final pathsToInvalidate = <String>{};
620
+ final targetVersionMutations = <String, _TargetVersionMutation>{};
621
+
622
+ for (final change in changedSets) {
623
+ final data = change.data;
624
+
625
+ // Insert
626
+ if (change.operation == BundleChangeOperation.insert) {
627
+ final target = _resolveStorageTarget(
628
+ targetAppVersion: data.targetAppVersion,
629
+ fingerprintHash: data.fingerprintHash,
630
+ );
631
+ final key =
632
+ '${data.channel}/${data.platform.value}/$target/update.json';
633
+ final bundleWithKey = _BundleWithKey(data, updateJsonKey: key);
634
+
635
+ locallyDeletedBundleIds.remove(data.id);
636
+ bundlesMap[data.id] = bundleWithKey;
637
+ pendingBundlesMap[data.id] = bundleWithKey;
638
+
639
+ changedBundlesByKey
640
+ .putIfAbsent(key, () => <Bundle>[])
641
+ .add(_removeBundleInternalKeys(data));
642
+
643
+ _addTargetVersionAddition(targetVersionMutations, data);
644
+ _addLookupInvalidationPaths(
645
+ pathsToInvalidate,
646
+ platform: data.platform.value,
647
+ channel: data.channel,
648
+ targetAppVersion: data.targetAppVersion,
649
+ fingerprintHash: data.fingerprintHash,
650
+ );
651
+ continue;
652
+ }
653
+
654
+ // Delete
655
+ if (change.operation == BundleChangeOperation.delete) {
656
+ _BundleWithKey? bk = pendingBundlesMap[data.id];
657
+ bk ??= bundlesMap[data.id];
658
+ if (bk == null) throw StateError('Bundle to delete not found');
659
+
660
+ bundlesMap.remove(data.id);
661
+ pendingBundlesMap.remove(data.id);
662
+ locallyDeletedBundleIds.add(data.id);
663
+
664
+ removalsByKey
665
+ .putIfAbsent(bk.updateJsonKey, () => <String>[])
666
+ .add(bk.bundle.id);
667
+ targetVersionRemovalsByKey
668
+ .putIfAbsent(bk.updateJsonKey, () => <_BundleWithKey>[])
669
+ .add(bk);
670
+
671
+ _addLookupInvalidationPaths(
672
+ pathsToInvalidate,
673
+ platform: bk.bundle.platform.value,
674
+ channel: bk.bundle.channel,
675
+ targetAppVersion: bk.bundle.targetAppVersion,
676
+ fingerprintHash: bk.bundle.fingerprintHash,
677
+ );
678
+ continue;
679
+ }
680
+
681
+ // Update
682
+ _BundleWithKey? bk = pendingBundlesMap[data.id];
683
+ bk ??= bundlesMap[data.id];
684
+ if (bk == null) throw StateError('targetBundleId not found');
685
+
686
+ if (change.operation == BundleChangeOperation.update) {
687
+ final merged = _mergeBundleUpdate(bk.bundle, data.toJson());
688
+ final newKey =
689
+ '${merged.channel}/${merged.platform.value}/${_resolveStorageTarget(
690
+ targetAppVersion: merged.targetAppVersion,
691
+ fingerprintHash: merged.fingerprintHash,
692
+ )}/update.json';
693
+
694
+ if (newKey != bk.updateJsonKey) {
695
+ // Key changed — remove from old, add to new.
696
+ removalsByKey
697
+ .putIfAbsent(bk.updateJsonKey, () => <String>[])
698
+ .add(bk.bundle.id);
699
+ targetVersionRemovalsByKey
700
+ .putIfAbsent(bk.updateJsonKey, () => <_BundleWithKey>[])
701
+ .add(bk);
702
+
703
+ final updated = _BundleWithKey(merged, updateJsonKey: newKey);
704
+ bundlesMap[data.id] = updated;
705
+ pendingBundlesMap[data.id] = updated;
706
+ locallyDeletedBundleIds.remove(data.id);
707
+
708
+ changedBundlesByKey
709
+ .putIfAbsent(newKey, () => <Bundle>[])
710
+ .add(_removeBundleInternalKeys(merged));
711
+
712
+ final oldChannel = bk.bundle.channel;
713
+ final nextChannel = merged.channel;
714
+ if (oldChannel != nextChannel) {
715
+ _addLookupInvalidationPaths(
716
+ pathsToInvalidate,
717
+ platform: bk.bundle.platform.value,
718
+ channel: oldChannel,
719
+ targetAppVersion: bk.bundle.targetAppVersion,
720
+ fingerprintHash: bk.bundle.fingerprintHash,
721
+ );
722
+ if (bk.bundle.targetAppVersion != null &&
723
+ bk.bundle.fingerprintHash == null) {
724
+ _addLookupInvalidationPaths(
725
+ pathsToInvalidate,
726
+ platform: merged.platform.value,
727
+ channel: nextChannel,
728
+ targetAppVersion: bk.bundle.targetAppVersion,
729
+ fingerprintHash: bk.bundle.fingerprintHash,
730
+ );
731
+ }
732
+ }
733
+
734
+ _addTargetVersionAddition(targetVersionMutations, merged);
735
+ _addLookupInvalidationPaths(
736
+ pathsToInvalidate,
737
+ platform: merged.platform.value,
738
+ channel: merged.channel,
739
+ targetAppVersion: merged.targetAppVersion,
740
+ fingerprintHash: merged.fingerprintHash,
741
+ );
742
+ if (bk.bundle.targetAppVersion != null &&
743
+ bk.bundle.targetAppVersion != merged.targetAppVersion) {
744
+ _addLookupInvalidationPaths(
745
+ pathsToInvalidate,
746
+ platform: bk.bundle.platform.value,
747
+ channel: bk.bundle.channel,
748
+ targetAppVersion: bk.bundle.targetAppVersion,
749
+ fingerprintHash: bk.bundle.fingerprintHash,
750
+ );
751
+ }
752
+ continue;
753
+ }
754
+
755
+ // No key change
756
+ final currentKey = bk.updateJsonKey;
757
+ final updated = _BundleWithKey(merged, updateJsonKey: currentKey);
758
+ bundlesMap[data.id] = updated;
759
+ pendingBundlesMap[data.id] = updated;
760
+ locallyDeletedBundleIds.remove(data.id);
761
+
762
+ changedBundlesByKey
763
+ .putIfAbsent(currentKey, () => <Bundle>[])
764
+ .add(_removeBundleInternalKeys(merged));
765
+
766
+ _addLookupInvalidationPaths(
767
+ pathsToInvalidate,
768
+ platform: merged.platform.value,
769
+ channel: merged.channel,
770
+ targetAppVersion: merged.targetAppVersion,
771
+ fingerprintHash: merged.fingerprintHash,
772
+ );
773
+ _addTargetVersionAddition(targetVersionMutations, merged);
774
+ if (bk.bundle.targetAppVersion != null &&
775
+ bk.bundle.targetAppVersion != merged.targetAppVersion) {
776
+ _addLookupInvalidationPaths(
777
+ pathsToInvalidate,
778
+ platform: bk.bundle.platform.value,
779
+ channel: bk.bundle.channel,
780
+ targetAppVersion: bk.bundle.targetAppVersion,
781
+ fingerprintHash: bk.bundle.fingerprintHash,
782
+ );
783
+ }
784
+ }
785
+ }
786
+
787
+ // Remove bundles from their old keys.
788
+ await _forEachWithConcurrency(
789
+ removalsByKey.keys.toList(),
790
+ _storageOperationConcurrency,
791
+ (oldKey, _) async {
792
+ final currentBundles =
793
+ (await loadOptionalObject<List>(oldKey))
794
+ ?.map((e) =>
795
+ Bundle.fromJson((e as Map).cast<String, dynamic>()))
796
+ .toList() ??
797
+ <Bundle>[];
798
+ final removalIds = removalsByKey[oldKey]!;
799
+ final updatedBundles =
800
+ currentBundles.where((b) => !removalIds.contains(b.id)).toList();
801
+ updatedBundles.sort((a, b) => b.id.compareTo(a.id));
802
+
803
+ if (updatedBundles.isEmpty) {
804
+ await ops.deleteObject(oldKey);
805
+ for (final removed in targetVersionRemovalsByKey[oldKey] ?? []) {
806
+ _addTargetVersionRemoval(targetVersionMutations, removed.bundle);
807
+ }
808
+ } else {
809
+ await ops.uploadObject(oldKey, updatedBundles);
810
+ }
811
+ },
812
+ );
813
+
814
+ // Add or update bundles in their new keys.
815
+ await _forEachWithConcurrency(
816
+ changedBundlesByKey.keys.toList(),
817
+ _storageOperationConcurrency,
818
+ (key, _) async {
819
+ final currentBundles =
820
+ (await loadOptionalObject<List>(key))
821
+ ?.map((e) =>
822
+ Bundle.fromJson((e as Map).cast<String, dynamic>()))
823
+ .toList() ??
824
+ <Bundle>[];
825
+ for (final changedBundle in changedBundlesByKey[key]!) {
826
+ final idx =
827
+ currentBundles.indexWhere((b) => b.id == changedBundle.id);
828
+ if (idx >= 0) {
829
+ currentBundles[idx] = changedBundle;
830
+ } else {
831
+ currentBundles.add(changedBundle);
832
+ }
833
+ }
834
+ currentBundles.sort((a, b) => b.id.compareTo(a.id));
835
+ await ops.uploadObject(key, currentBundles);
836
+ },
837
+ );
838
+
839
+ if (targetVersionMutations.isNotEmpty) {
840
+ await applyTargetVersionMutations(targetVersionMutations);
841
+ }
842
+
843
+ await ops.invalidatePaths(pathsToInvalidate.toList());
844
+
845
+ pendingBundlesMap.clear();
846
+ }
847
+
848
+ @override
849
+ final bool supportsCursorPagination = true;
850
+
851
+ @override
852
+ Future<void> onUnmount() async {}
853
+ }