@_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.
- package/bin/flutter-patcher.js +48 -39
- package/dart-src/packages/cli-tools/.gitkeep +0 -0
- package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
- package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
- package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
- package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
- package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
- package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
- package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
- package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
- package/dart-src/packages/core/lib/src/bundle.dart +191 -0
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
- package/dart-src/packages/core/lib/src/metadata.dart +31 -0
- package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
- package/dart-src/packages/core/lib/src/platform.dart +15 -0
- package/dart-src/packages/core/lib/src/rollout.dart +209 -0
- package/dart-src/packages/core/lib/src/semver.dart +70 -0
- package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
- package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
- package/dart-src/packages/core/lib/src/status.dart +24 -0
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
- package/dart-src/packages/core/lib/src/uuid.dart +45 -0
- package/dart-src/packages/core/pubspec.yaml +9 -0
- package/dart-src/packages/core/tool/dbg.dart +6 -0
- package/dart-src/plugins/aws/.gitkeep +0 -0
- package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
- package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
- package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
- package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
- package/dart-src/plugins/aws/pubspec.yaml +22 -0
- package/dart-src/plugins/cloudflare/.gitkeep +0 -0
- package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
- package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
- package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
- package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
- package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
- package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
- package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
- package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
- package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
- package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
- package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
- package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
- package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
- package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
- package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
- package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
- package/dart-src/plugins/postgres/.gitkeep +0 -0
- package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
- package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
- package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
- package/dart-src/plugins/postgres/pubspec.yaml +18 -0
- package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
- package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
- package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
- package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
- package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
- package/dart-src/plugins/standalone/pubspec.yaml +18 -0
- package/dart-src/plugins/supabase/lib/edge.dart +10 -0
- package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
- package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
- package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
- package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
- package/dart-src/plugins/supabase/pubspec.yaml +19 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +53 -25
- /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
|
+
}
|