@_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,525 @@
1
+ import 'bundle_unit_of_work.dart';
2
+ import 'bundle_unit_of_work_store.dart';
3
+ import 'calculate_pagination.dart';
4
+ import 'types.dart';
5
+
6
+ const List<String> _replaceOnUpdateKeys = ['patches', 'targetCohorts'];
7
+ const DatabaseBundleQueryOrder _defaultDescOrder =
8
+ DatabaseBundleQueryOrder(field: 'id', direction: 'desc');
9
+
10
+ int? _normalizePage(int? value) {
11
+ if (value == null || value < 1) return null;
12
+ return value;
13
+ }
14
+
15
+ /// Deep-merge a bundle patch onto a base bundle, replacing array fields
16
+ /// specified by [replaceKeys] rather than concatenating them.
17
+ Bundle mergeBundleUpdate(Bundle base, Map<String, Object?> patch) {
18
+ final baseMap = base.toJson();
19
+ for (final key in patch.keys) {
20
+ final srcValue = patch[key];
21
+ if (_replaceOnUpdateKeys.contains(key)) {
22
+ baseMap[key] = srcValue;
23
+ } else if (srcValue != null) {
24
+ baseMap[key] = srcValue;
25
+ }
26
+ }
27
+ return Bundle.fromJson(baseMap);
28
+ }
29
+
30
+ DatabaseBundleIdFilter _mergeIdFilter(
31
+ DatabaseBundleIdFilter? base,
32
+ DatabaseBundleIdFilter patch,
33
+ ) {
34
+ return DatabaseBundleIdFilter(
35
+ eq: patch.eq ?? base?.eq,
36
+ gt: patch.gt ?? base?.gt,
37
+ gte: patch.gte ?? base?.gte,
38
+ lt: patch.lt ?? base?.lt,
39
+ lte: patch.lte ?? base?.lte,
40
+ ins: patch.ins ?? base?.ins,
41
+ );
42
+ }
43
+
44
+ DatabaseBundleQueryWhere _mergeWhereWithIdFilter(
45
+ DatabaseBundleQueryWhere? where,
46
+ DatabaseBundleIdFilter idFilter,
47
+ ) {
48
+ return DatabaseBundleQueryWhere(
49
+ channel: where?.channel,
50
+ platform: where?.platform,
51
+ enabled: where?.enabled,
52
+ id: _mergeIdFilter(where?.id, idFilter),
53
+ targetAppVersion: where?.targetAppVersion,
54
+ targetAppVersionIn: where?.targetAppVersionIn,
55
+ targetAppVersionNotNull: where?.targetAppVersionNotNull,
56
+ fingerprintHash: where?.fingerprintHash,
57
+ );
58
+ }
59
+
60
+ class _CursorPageQuery {
61
+ const _CursorPageQuery({
62
+ required this.reverseData,
63
+ required this.where,
64
+ required this.orderBy,
65
+ });
66
+
67
+ final bool reverseData;
68
+ final DatabaseBundleQueryWhere where;
69
+ final DatabaseBundleQueryOrder orderBy;
70
+ }
71
+
72
+ _CursorPageQuery _buildCursorPageQuery(
73
+ DatabaseBundleQueryWhere? where,
74
+ DatabaseBundleCursor cursor,
75
+ DatabaseBundleQueryOrder orderBy,
76
+ ) {
77
+ final direction = orderBy.direction;
78
+
79
+ if (cursor.after != null) {
80
+ return _CursorPageQuery(
81
+ reverseData: false,
82
+ where: _mergeWhereWithIdFilter(
83
+ where,
84
+ DatabaseBundleIdFilter(
85
+ lt: direction == 'desc' ? cursor.after : null,
86
+ gt: direction == 'asc' ? cursor.after : null,
87
+ ),
88
+ ),
89
+ orderBy: orderBy,
90
+ );
91
+ }
92
+
93
+ if (cursor.before != null) {
94
+ return _CursorPageQuery(
95
+ reverseData: true,
96
+ where: _mergeWhereWithIdFilter(
97
+ where,
98
+ DatabaseBundleIdFilter(
99
+ gt: direction == 'desc' ? cursor.before : null,
100
+ lt: direction == 'asc' ? cursor.before : null,
101
+ ),
102
+ ),
103
+ orderBy: DatabaseBundleQueryOrder(
104
+ field: orderBy.field,
105
+ direction: direction == 'desc' ? 'asc' : 'desc',
106
+ ),
107
+ );
108
+ }
109
+
110
+ return _CursorPageQuery(
111
+ reverseData: false,
112
+ where: where ?? const DatabaseBundleQueryWhere(),
113
+ orderBy: orderBy,
114
+ );
115
+ }
116
+
117
+ DatabaseBundleQueryWhere _buildCountBeforeWhere(
118
+ DatabaseBundleQueryWhere? where,
119
+ String firstBundleId,
120
+ DatabaseBundleQueryOrder orderBy,
121
+ ) {
122
+ return _mergeWhereWithIdFilter(
123
+ where,
124
+ DatabaseBundleIdFilter(
125
+ gt: orderBy.direction == 'desc' ? firstBundleId : null,
126
+ lt: orderBy.direction == 'asc' ? firstBundleId : null,
127
+ ),
128
+ );
129
+ }
130
+
131
+ Paginated<List<Bundle>> _createPaginatedResult(
132
+ int total,
133
+ int limit,
134
+ int startIndex,
135
+ List<Bundle> data,
136
+ ) {
137
+ final pagination = calculatePagination(total, limit: limit, offset: startIndex);
138
+ final nextCursor =
139
+ data.isNotEmpty && startIndex + data.length < total ? data.last.id : null;
140
+ final previousCursor =
141
+ data.isNotEmpty && startIndex > 0 ? data.first.id : null;
142
+
143
+ return Paginated(
144
+ data: data,
145
+ pagination: PaginationInfo(
146
+ total: pagination.total,
147
+ hasNextPage: pagination.hasNextPage,
148
+ hasPreviousPage: pagination.hasPreviousPage,
149
+ currentPage: pagination.currentPage,
150
+ totalPages: pagination.totalPages,
151
+ nextCursor: nextCursor,
152
+ previousCursor: previousCursor,
153
+ ),
154
+ );
155
+ }
156
+
157
+ DatabaseBundleQueryOptions _expandLimitForUnitOfWork(
158
+ DatabaseBundleQueryOptions options,
159
+ BundleUnitOfWork unitOfWork,
160
+ ) {
161
+ final extra = unitOfWork.listFetchExtraCount();
162
+ if (extra == 0) return options;
163
+ return DatabaseBundleQueryOptions(
164
+ where: options.where,
165
+ limit: options.limit + extra,
166
+ page: options.page,
167
+ offset: options.offset,
168
+ cursor: options.cursor,
169
+ orderBy: options.orderBy,
170
+ );
171
+ }
172
+
173
+ PaginationInfo _adjustPaginationTotal(
174
+ PaginationInfo pagination, {
175
+ required int limit,
176
+ required int totalDelta,
177
+ }) {
178
+ if (totalDelta == 0) return pagination;
179
+ final total = (pagination.total + totalDelta).clamp(0, 0x7FFFFFFF);
180
+ final hasPreviousPage = pagination.currentPage > 1;
181
+ final hasNextPage = pagination.currentPage * limit < total;
182
+ return PaginationInfo(
183
+ total: total,
184
+ hasNextPage: hasNextPage,
185
+ hasPreviousPage: hasPreviousPage,
186
+ currentPage: pagination.currentPage,
187
+ totalPages: total == 0 ? 0 : (total / limit).ceil(),
188
+ nextCursor: pagination.nextCursor,
189
+ previousCursor: pagination.previousCursor,
190
+ );
191
+ }
192
+
193
+ /// The underlying database methods a plugin must implement.
194
+ abstract class AbstractDatabasePlugin {
195
+ bool get supportsCursorPagination => false;
196
+
197
+ Future<Bundle?> getBundleById(String bundleId);
198
+
199
+ Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args);
200
+
201
+ Future<Paginated<List<Bundle>>> getBundles(
202
+ DatabaseBundleQueryOptions options,
203
+ );
204
+
205
+ Future<List<String>> getChannels();
206
+
207
+ Future<void> commitBundle({
208
+ required List<BundleChange> changedSets,
209
+ });
210
+
211
+ Future<void> onUnmount();
212
+ }
213
+
214
+ /// Configuration for [createDatabasePlugin].
215
+ class CreateDatabasePluginOptions<TConfig> {
216
+ const CreateDatabasePluginOptions({
217
+ required this.name,
218
+ required this.factory,
219
+ });
220
+
221
+ final String name;
222
+ final AbstractDatabasePlugin Function(TConfig config) factory;
223
+ }
224
+
225
+ /// Double-curried factory that creates a [DatabasePlugin] with lazy
226
+ /// initialization, UnitOfWork integration, and hook support.
227
+ ///
228
+ /// Usage:
229
+ /// ```dart
230
+ /// final supabaseDatabase = createDatabasePlugin<SupabaseConfig>(
231
+ /// name: 'supabaseDatabase',
232
+ /// factory: (config) => SupabaseDatabaseImpl(config),
233
+ /// );
234
+ /// // First curry: config + hooks → factory
235
+ /// final createDb = supabaseDatabase(config, hooks);
236
+ /// // Second curry: new plugin instance
237
+ /// final db = createDb();
238
+ /// ```
239
+ DatabasePlugin Function() Function(TConfig config, [DatabasePluginHooks? hooks])
240
+ createDatabasePlugin<TConfig>({
241
+ required String name,
242
+ required AbstractDatabasePlugin Function(TConfig config) factory,
243
+ }) {
244
+ return (TConfig config, [DatabasePluginHooks? hooks]) {
245
+ AbstractDatabasePlugin? cachedMethods;
246
+ AbstractDatabasePlugin getMethods() {
247
+ cachedMethods ??= factory(config);
248
+ return cachedMethods!;
249
+ }
250
+
251
+ return () {
252
+ final instanceUnitOfWork = BundleUnitOfWork();
253
+
254
+ BundleUnitOfWork getMutationUnitOfWork(
255
+ Map<String, Object?>? context) =>
256
+ getRequestBundleUnitOfWork(context) ?? instanceUnitOfWork;
257
+
258
+ Future<Paginated<List<Bundle>>> runGetBundles(
259
+ DatabaseBundleQueryOptions options,
260
+ ) =>
261
+ getMethods().getBundles(options);
262
+
263
+ Future<Paginated<List<Bundle>>> getBundlesWithLegacyCursorFallback(
264
+ DatabaseBundleQueryOptions options,
265
+ ) async {
266
+ final orderBy = options.orderBy ?? _defaultDescOrder;
267
+ final baseWhere = options.where;
268
+
269
+ final totalResult = await runGetBundles(
270
+ DatabaseBundleQueryOptions(
271
+ where: baseWhere,
272
+ limit: 1,
273
+ offset: 0,
274
+ orderBy: orderBy,
275
+ ),
276
+ );
277
+ final total = totalResult.pagination.total;
278
+
279
+ if (options.cursor?.after == null && options.cursor?.before == null) {
280
+ final firstPage = await runGetBundles(
281
+ DatabaseBundleQueryOptions(
282
+ where: baseWhere,
283
+ limit: options.limit,
284
+ offset: 0,
285
+ orderBy: orderBy,
286
+ ),
287
+ );
288
+ return _createPaginatedResult(total, options.limit, 0, firstPage.data);
289
+ }
290
+
291
+ final cursorQuery = _buildCursorPageQuery(
292
+ baseWhere,
293
+ options.cursor!,
294
+ orderBy,
295
+ );
296
+ final cursorPage = await runGetBundles(
297
+ DatabaseBundleQueryOptions(
298
+ where: cursorQuery.where,
299
+ limit: options.limit,
300
+ offset: 0,
301
+ orderBy: cursorQuery.orderBy,
302
+ ),
303
+ );
304
+ final data = cursorQuery.reverseData
305
+ ? cursorPage.data.reversed.toList()
306
+ : cursorPage.data;
307
+
308
+ if (data.isEmpty) {
309
+ return Paginated(
310
+ data: data,
311
+ pagination: PaginationInfo(
312
+ total: total,
313
+ hasNextPage: false,
314
+ hasPreviousPage: false,
315
+ currentPage: 1,
316
+ totalPages: 0,
317
+ nextCursor: options.cursor?.before,
318
+ previousCursor: options.cursor?.after,
319
+ ),
320
+ );
321
+ }
322
+
323
+ final firstBundleId = data.first.id;
324
+ final countBeforeResult = await runGetBundles(
325
+ DatabaseBundleQueryOptions(
326
+ where: _buildCountBeforeWhere(baseWhere, firstBundleId, orderBy),
327
+ limit: 1,
328
+ offset: 0,
329
+ orderBy: orderBy,
330
+ ),
331
+ );
332
+
333
+ return _createPaginatedResult(
334
+ total,
335
+ options.limit,
336
+ countBeforeResult.pagination.total,
337
+ data,
338
+ );
339
+ }
340
+
341
+ // Build the plugin methods.
342
+ return _DatabasePluginImpl(
343
+ name: name,
344
+ getMethods: getMethods,
345
+ getMutationUnitOfWork: getMutationUnitOfWork,
346
+ instanceUnitOfWork: instanceUnitOfWork,
347
+ runGetBundles: runGetBundles,
348
+ getBundlesWithLegacyCursorFallback: getBundlesWithLegacyCursorFallback,
349
+ hooks: hooks,
350
+ ) as DatabasePlugin;
351
+ };
352
+ };
353
+ }
354
+
355
+ /// Internal implementation of DatabasePlugin.
356
+ class _DatabasePluginImpl implements DatabasePlugin {
357
+ _DatabasePluginImpl({
358
+ required this.name,
359
+ required this.getMethods,
360
+ required this.getMutationUnitOfWork,
361
+ required this.instanceUnitOfWork,
362
+ required this.runGetBundles,
363
+ required this.getBundlesWithLegacyCursorFallback,
364
+ this.hooks,
365
+ });
366
+
367
+ @override
368
+ final String name;
369
+ final AbstractDatabasePlugin Function() getMethods;
370
+ final BundleUnitOfWork Function(Map<String, Object?>?) getMutationUnitOfWork;
371
+ final BundleUnitOfWork instanceUnitOfWork;
372
+ final Future<Paginated<List<Bundle>>> Function(DatabaseBundleQueryOptions)
373
+ runGetBundles;
374
+ final Future<Paginated<List<Bundle>>> Function(DatabaseBundleQueryOptions)
375
+ getBundlesWithLegacyCursorFallback;
376
+ final DatabasePluginHooks? hooks;
377
+
378
+ @override
379
+ Future<void> onUnmount() => getMethods().onUnmount();
380
+
381
+ @override
382
+ Future<List<String>> getChannels() => getMethods().getChannels();
383
+
384
+ @override
385
+ Future<Bundle?> getBundleById(String bundleId) async {
386
+ final requestUoW =
387
+ getRequestBundleUnitOfWork(null); // context not passed through here
388
+ if (requestUoW != null) {
389
+ return requestUoW.getById(bundleId, () =>
390
+ getMethods().getBundleById(bundleId));
391
+ }
392
+ final pending = instanceUnitOfWork.peekChanged(bundleId);
393
+ if (pending is TrackedBundleFound) return pending.value;
394
+ return getMethods().getBundleById(bundleId);
395
+ }
396
+
397
+ @override
398
+ Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) async {
399
+ final methods = getMethods();
400
+ return methods.getUpdateInfo(args);
401
+ }
402
+
403
+ @override
404
+ Future<Paginated<List<Bundle>>> getBundles(
405
+ DatabaseBundleQueryOptions options,
406
+ ) async {
407
+ final methods = getMethods();
408
+ final unitOfWork = getMutationUnitOfWork(null);
409
+ final shouldOverlay = instanceUnitOfWork.hasChanges();
410
+ final normalizedOptions = DatabaseBundleQueryOptions(
411
+ where: options.where,
412
+ limit: options.limit,
413
+ page: _normalizePage(options.page),
414
+ cursor: options.cursor,
415
+ orderBy: options.orderBy ?? _defaultDescOrder,
416
+ );
417
+
418
+ Paginated<List<Bundle>> overlayResult(Paginated<List<Bundle>> result) {
419
+ return Paginated(
420
+ data: unitOfWork.overlayList(
421
+ result.data,
422
+ limit: normalizedOptions.limit,
423
+ orderBy: normalizedOptions.orderBy,
424
+ where: normalizedOptions.where,
425
+ ),
426
+ pagination: _adjustPaginationTotal(
427
+ result.pagination,
428
+ limit: normalizedOptions.limit,
429
+ totalDelta: unitOfWork.totalDelta(normalizedOptions.where),
430
+ ),
431
+ );
432
+ }
433
+
434
+ if (normalizedOptions.page != null) {
435
+ final page = normalizedOptions.page!;
436
+ final requestedOffset = (page - 1) * normalizedOptions.limit;
437
+ final fetchOptions = _expandLimitForUnitOfWork(normalizedOptions, unitOfWork);
438
+ var pageResult = await runGetBundles(
439
+ DatabaseBundleQueryOptions(
440
+ where: fetchOptions.where,
441
+ limit: fetchOptions.limit,
442
+ offset: requestedOffset,
443
+ orderBy: fetchOptions.orderBy,
444
+ ),
445
+ );
446
+
447
+ final total = pageResult.pagination.total;
448
+ final totalPages =
449
+ total == 0 ? 0 : (total / normalizedOptions.limit).ceil();
450
+ final maxOffset =
451
+ totalPages == 0 ? 0 : (totalPages - 1) * normalizedOptions.limit;
452
+ final resolvedOffset =
453
+ requestedOffset < maxOffset ? requestedOffset : maxOffset;
454
+
455
+ if (resolvedOffset != requestedOffset) {
456
+ pageResult = await runGetBundles(
457
+ DatabaseBundleQueryOptions(
458
+ where: fetchOptions.where,
459
+ limit: fetchOptions.limit,
460
+ offset: resolvedOffset,
461
+ orderBy: fetchOptions.orderBy,
462
+ ),
463
+ );
464
+ }
465
+
466
+ final result = _createPaginatedResult(
467
+ total,
468
+ normalizedOptions.limit,
469
+ resolvedOffset,
470
+ pageResult.data,
471
+ );
472
+ return shouldOverlay ? overlayResult(result) : result;
473
+ }
474
+
475
+ if (methods.supportsCursorPagination) {
476
+ final fetchOptions = _expandLimitForUnitOfWork(normalizedOptions, unitOfWork);
477
+ final result = await runGetBundles(fetchOptions);
478
+ return shouldOverlay ? overlayResult(result) : result;
479
+ }
480
+
481
+ final result = await getBundlesWithLegacyCursorFallback(
482
+ shouldOverlay
483
+ ? _expandLimitForUnitOfWork(normalizedOptions, unitOfWork)
484
+ : normalizedOptions,
485
+ );
486
+ return shouldOverlay ? overlayResult(result) : result;
487
+ }
488
+
489
+ @override
490
+ Future<void> commitBundle() async {
491
+ final methods = getMethods();
492
+ final unitOfWork = getMutationUnitOfWork(null);
493
+ await methods.commitBundle(
494
+ changedSets: unitOfWork.changedSets(),
495
+ );
496
+ unitOfWork.clear();
497
+ final cb = hooks?.onDatabaseUpdated;
498
+ if (cb != null) await cb();
499
+ }
500
+
501
+ @override
502
+ Future<void> updateBundle(
503
+ String targetBundleId,
504
+ Map<String, Object?> newBundle,
505
+ ) async {
506
+ final unitOfWork = getMutationUnitOfWork(null);
507
+ final current = await unitOfWork.getById(targetBundleId, () =>
508
+ getMethods().getBundleById(targetBundleId));
509
+ if (current == null) {
510
+ throw StateError('targetBundleId not found');
511
+ }
512
+ final updated = mergeBundleUpdate(current, newBundle);
513
+ unitOfWork.markUpdate(updated);
514
+ }
515
+
516
+ @override
517
+ Future<void> appendBundle(Bundle insertBundle) async {
518
+ getMutationUnitOfWork(null).markInsert(insertBundle);
519
+ }
520
+
521
+ @override
522
+ Future<void> deleteBundle(Bundle deleteBundle) async {
523
+ getMutationUnitOfWork(null).markDelete(deleteBundle);
524
+ }
525
+ }
@@ -0,0 +1,15 @@
1
+ /// Create a curried storage key builder that prepends an optional base path.
2
+ ///
3
+ /// ```dart
4
+ /// final getKey = createStorageKeyBuilder('bundles');
5
+ /// getKey('v1', 'patch.zip'); // → 'bundles/v1/patch.zip'
6
+ /// ```
7
+ ///
8
+ /// Faithful port of hot-updater `createStorageKeyBuilder.ts`.
9
+ String Function(String, [String, String, String]) createStorageKeyBuilder(
10
+ String? basePath,
11
+ ) {
12
+ return (String a, [String b = '', String c = '', String d = '']) {
13
+ return [basePath ?? '', a, b, c, d].where((s) => s.isNotEmpty).join('/');
14
+ };
15
+ }
@@ -0,0 +1,130 @@
1
+ import 'types.dart';
2
+
3
+ /// Create a deploy/CLI/console storage plugin (node profile only).
4
+ StoragePlugin Function(TConfig config, [StoragePluginHooks? hooks])
5
+ createNodeStoragePlugin<TConfig>({
6
+ required String name,
7
+ required String supportedProtocol,
8
+ required NodeStorageProfile Function(TConfig config) factory,
9
+ }) {
10
+ return (TConfig config, [StoragePluginHooks? hooks]) {
11
+ NodeStorageProfile? cached;
12
+ NodeStorageProfile getProfile() {
13
+ cached ??= factory(config);
14
+ return cached!;
15
+ }
16
+
17
+ return _ProfiledStoragePlugin(
18
+ name: name,
19
+ supportedProtocol: supportedProtocol,
20
+ nodeProfile: _WrappedNodeProfile(getProfile, hooks),
21
+ );
22
+ };
23
+ }
24
+
25
+ /// Create an update-check runtime storage plugin.
26
+ StoragePlugin Function(TConfig config, [StoragePluginHooks? hooks])
27
+ createRuntimeStoragePlugin<TConfig>({
28
+ required String name,
29
+ required String supportedProtocol,
30
+ required RuntimeStorageProfile Function(TConfig config) factory,
31
+ }) {
32
+ return (TConfig config, [StoragePluginHooks? hooks]) {
33
+ RuntimeStorageProfile? cached;
34
+ RuntimeStorageProfile getProfile() {
35
+ cached ??= factory(config);
36
+ return cached!;
37
+ }
38
+
39
+ return _ProfiledStoragePlugin(
40
+ name: name,
41
+ supportedProtocol: supportedProtocol,
42
+ runtimeProfile: getProfile,
43
+ );
44
+ };
45
+ }
46
+
47
+ /// Create a storage plugin supporting both node and runtime profiles.
48
+ StoragePlugin Function(TConfig config, [StoragePluginHooks? hooks])
49
+ createUniversalStoragePlugin<TConfig>({
50
+ required String name,
51
+ required String supportedProtocol,
52
+ required ({
53
+ NodeStorageProfile node,
54
+ RuntimeStorageProfile runtime,
55
+ }) Function(TConfig config) factory,
56
+ }) {
57
+ return (TConfig config, [StoragePluginHooks? hooks]) {
58
+ ({NodeStorageProfile node, RuntimeStorageProfile runtime})? cached;
59
+ ({NodeStorageProfile node, RuntimeStorageProfile runtime}) getProfiles() {
60
+ cached ??= factory(config);
61
+ return cached!;
62
+ }
63
+
64
+ return _ProfiledStoragePlugin(
65
+ name: name,
66
+ supportedProtocol: supportedProtocol,
67
+ nodeProfile: _WrappedNodeProfile(() => getProfiles().node, hooks),
68
+ runtimeProfile: () => getProfiles().runtime,
69
+ );
70
+ };
71
+ }
72
+
73
+ /// Wraps a node profile's upload with hook callback.
74
+ class _WrappedNodeProfile implements NodeStorageProfile {
75
+ _WrappedNodeProfile(this._delegate, this._hooks);
76
+
77
+ final NodeStorageProfile Function() _delegate;
78
+ final StoragePluginHooks? _hooks;
79
+
80
+ NodeStorageProfile get _node => _delegate();
81
+
82
+ @override
83
+ Future<Map<String, String>> upload(String key, String filePath) async {
84
+ final result = await _node.upload(key, filePath);
85
+ final cb = _hooks?.onStorageUploaded;
86
+ if (cb != null) await cb();
87
+ return result;
88
+ }
89
+
90
+ @override
91
+ Future<bool> exists(String storageUri) => _node.exists(storageUri);
92
+
93
+ @override
94
+ Future<void> delete(String storageUri) => _node.delete(storageUri);
95
+
96
+ @override
97
+ Future<void> downloadFile(String storageUri, String filePath) =>
98
+ _node.downloadFile(storageUri, filePath);
99
+
100
+ @override
101
+ Future<List<StorageObject>> listObjects([String? prefix]) =>
102
+ _node.listObjects(prefix);
103
+
104
+ @override
105
+ Future<void> deleteObjects(List<String> keys) =>
106
+ _node.deleteObjects(keys);
107
+ }
108
+
109
+ /// Storage plugin with lazy profile resolution.
110
+ class _ProfiledStoragePlugin implements StoragePlugin {
111
+ _ProfiledStoragePlugin({
112
+ required this.name,
113
+ required this.supportedProtocol,
114
+ this.nodeProfile,
115
+ this.runtimeProfile,
116
+ });
117
+
118
+ @override
119
+ final String name;
120
+ @override
121
+ final String supportedProtocol;
122
+ final NodeStorageProfile? nodeProfile;
123
+ final RuntimeStorageProfile Function()? runtimeProfile;
124
+
125
+ @override
126
+ StoragePluginProfiles get profiles => StoragePluginProfiles(
127
+ node: nodeProfile,
128
+ runtime: runtimeProfile?.call(),
129
+ );
130
+ }
@@ -0,0 +1,18 @@
1
+ import 'package:flutter_patcher_core/flutter_patcher_core.dart'
2
+ show semverSatisfies;
3
+
4
+ /// Filter target app versions that are compatible with the current app version.
5
+ ///
6
+ /// Returns only versions compatible per semver, sorted descending (newest first).
7
+ ///
8
+ /// Faithful port of hot-updater `filterCompatibleAppVersions.ts`.
9
+ List<String> filterCompatibleAppVersions(
10
+ List<String> targetAppVersionList,
11
+ String currentVersion,
12
+ ) {
13
+ final compatible = targetAppVersionList
14
+ .where((version) => semverSatisfies(version, currentVersion))
15
+ .toList();
16
+ compatible.sort((a, b) => b.compareTo(a));
17
+ return compatible;
18
+ }
@@ -0,0 +1,19 @@
1
+ /// Generate a minimum-priority bundle ID: UUIDv7 with zeroed random bits.
2
+ ///
3
+ /// Used by the DB plugin to create bundle IDs that sort before any
4
+ /// random-bit bundle created at the same millisecond.
5
+ ///
6
+ /// Faithful port of hot-updater `generateMinBundleId.ts`.
7
+ String generateMinBundleId() {
8
+ final timestamp = DateTime.now().millisecondsSinceEpoch;
9
+ final tsHex = timestamp.toRadixString(16).padLeft(12, '0');
10
+
11
+ // UUIDv7 layout: 48-bit ts | ver(0111)+12 rand_a(0) | var(10)+62 rand_b(0)
12
+ final timeHigh = tsHex.substring(0, 8);
13
+ final timeLow = tsHex.substring(8, 12);
14
+ const versionAndRandom = '7000'; // version 7, rand_a = 0
15
+ const variantAndRandom = '8000'; // variant 10, rand_b = 0
16
+ const node = '000000000000';
17
+
18
+ return '$timeHigh-$timeLow-$versionAndRandom-$variantAndRandom-$node';
19
+ }