@_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,422 @@
|
|
|
1
|
+
/// Faithful port of hot-updater `plugins/supabase/src/supabaseDatabase.ts`.
|
|
2
|
+
library;
|
|
3
|
+
|
|
4
|
+
import 'dart:convert';
|
|
5
|
+
|
|
6
|
+
import 'package:flutter_patcher_core/flutter_patcher_core.dart';
|
|
7
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
|
|
8
|
+
|
|
9
|
+
import 'supabase_bundle_mapper.dart';
|
|
10
|
+
import 'supabase_client_adapter.dart';
|
|
11
|
+
import 'supabase_client_http.dart';
|
|
12
|
+
import 'supabase_config.dart';
|
|
13
|
+
import 'types.dart';
|
|
14
|
+
|
|
15
|
+
/// Configuration for the Supabase database plugin.
|
|
16
|
+
typedef SupabaseDatabaseConfig = SupabaseServiceRoleConfig;
|
|
17
|
+
|
|
18
|
+
/// Row shape returned by the update-info RPCs.
|
|
19
|
+
class _SupabaseUpdateInfoRow {
|
|
20
|
+
final String id;
|
|
21
|
+
final bool shouldForceUpdate;
|
|
22
|
+
final String? message;
|
|
23
|
+
final String status;
|
|
24
|
+
final String? storageUri;
|
|
25
|
+
final String? fileHash;
|
|
26
|
+
|
|
27
|
+
const _SupabaseUpdateInfoRow({
|
|
28
|
+
required this.id,
|
|
29
|
+
required this.shouldForceUpdate,
|
|
30
|
+
required this.message,
|
|
31
|
+
required this.status,
|
|
32
|
+
required this.storageUri,
|
|
33
|
+
required this.fileHash,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Row shape returned by `get_target_app_version_list`.
|
|
38
|
+
class _SupabaseTargetAppVersionRow {
|
|
39
|
+
final String? targetAppVersion;
|
|
40
|
+
|
|
41
|
+
const _SupabaseTargetAppVersionRow(this.targetAppVersion);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Build a throwable error from an unknown Supabase error value.
|
|
45
|
+
Object createSupabaseError(Object error) {
|
|
46
|
+
if (error is Error) return error;
|
|
47
|
+
if (error is Exception) return error;
|
|
48
|
+
|
|
49
|
+
if (error is Map) {
|
|
50
|
+
final props = <String, Object?>{};
|
|
51
|
+
error.forEach((key, value) {
|
|
52
|
+
props[key.toString()] = value;
|
|
53
|
+
});
|
|
54
|
+
return StateError(jsonEncode(props));
|
|
55
|
+
}
|
|
56
|
+
return StateError(error.toString());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
UpdateInfo mapUpdateInfoRow(_SupabaseUpdateInfoRow row) => UpdateInfo(
|
|
60
|
+
id: row.id,
|
|
61
|
+
shouldForceUpdate: row.shouldForceUpdate,
|
|
62
|
+
message: row.message,
|
|
63
|
+
status: row.status == 'ROLLBACK' ? UpdateStatus.rollback : UpdateStatus.update,
|
|
64
|
+
storageUri: row.storageUri,
|
|
65
|
+
fileHash: row.fileHash,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
/// Supabase-backed [DatabasePlugin] factory.
|
|
69
|
+
///
|
|
70
|
+
/// Mirrors `supabaseDatabase` from hot-updater: a `createDatabasePlugin`
|
|
71
|
+
/// wrapper whose inner factory builds a Supabase client and returns an
|
|
72
|
+
/// [AbstractDatabasePlugin] implementation.
|
|
73
|
+
DatabasePlugin Function() Function(
|
|
74
|
+
SupabaseDatabaseConfig config, [
|
|
75
|
+
DatabasePluginHooks? hooks,
|
|
76
|
+
]) supabaseDatabase = createDatabasePlugin<SupabaseDatabaseConfig>(
|
|
77
|
+
name: 'supabaseDatabase',
|
|
78
|
+
factory: (config) {
|
|
79
|
+
final supabase = (config.clientFactory != null
|
|
80
|
+
? config.clientFactory!(config.supabaseUrl, config.resolveKey())
|
|
81
|
+
: createSupabaseHttpClient(
|
|
82
|
+
config.supabaseUrl,
|
|
83
|
+
config.resolveKey(),
|
|
84
|
+
));
|
|
85
|
+
|
|
86
|
+
Future<Map<String, List<SupabaseBundlePatchRow>>> fetchPatchMap(
|
|
87
|
+
List<String> bundleIds,
|
|
88
|
+
) async {
|
|
89
|
+
final patchMap = <String, List<SupabaseBundlePatchRow>>{};
|
|
90
|
+
if (bundleIds.isEmpty) return patchMap;
|
|
91
|
+
|
|
92
|
+
final res = await supabase
|
|
93
|
+
.from('bundle_patches')
|
|
94
|
+
.select('*')
|
|
95
|
+
.in_('bundle_id', bundleIds)
|
|
96
|
+
.order('order_index', ascending: true)
|
|
97
|
+
.execute();
|
|
98
|
+
|
|
99
|
+
if (res.error != null) {
|
|
100
|
+
throw createSupabaseError(res.error!);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
final data = (res.data as List?) ?? [];
|
|
104
|
+
for (final raw in data) {
|
|
105
|
+
final row = SupabaseBundlePatchRow.fromJson(
|
|
106
|
+
(raw as Map).cast<String, dynamic>(),
|
|
107
|
+
);
|
|
108
|
+
final current = patchMap[row.bundleId] ?? [];
|
|
109
|
+
current.add(row);
|
|
110
|
+
patchMap[row.bundleId] = current;
|
|
111
|
+
}
|
|
112
|
+
return patchMap;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return _SupabaseDatabasePlugin(
|
|
116
|
+
supabase: supabase,
|
|
117
|
+
fetchPatchMap: fetchPatchMap,
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
class _SupabaseDatabasePlugin implements AbstractDatabasePlugin {
|
|
123
|
+
final SupabaseClientLike supabase;
|
|
124
|
+
final Future<Map<String, List<SupabaseBundlePatchRow>>> Function(
|
|
125
|
+
List<String> bundleIds,
|
|
126
|
+
) fetchPatchMap;
|
|
127
|
+
|
|
128
|
+
_SupabaseDatabasePlugin({
|
|
129
|
+
required this.supabase,
|
|
130
|
+
required this.fetchPatchMap,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
@override
|
|
134
|
+
bool get supportsCursorPagination => false;
|
|
135
|
+
|
|
136
|
+
@override
|
|
137
|
+
Future<UpdateInfo?> getUpdateInfo(GetBundlesArgs args) async {
|
|
138
|
+
final channel = args.channel;
|
|
139
|
+
final minBundleId = args.minBundleId;
|
|
140
|
+
|
|
141
|
+
if (args.updateStrategy == UpdateStrategy.appVersion) {
|
|
142
|
+
final appArgs = args as AppVersionGetBundlesArgs;
|
|
143
|
+
final targetRes = await supabase.rpc(
|
|
144
|
+
'get_target_app_version_list',
|
|
145
|
+
params: {
|
|
146
|
+
'app_platform': appArgs.platform.value,
|
|
147
|
+
'min_bundle_id': minBundleId,
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
if (targetRes.error != null) {
|
|
151
|
+
throw createSupabaseError(targetRes.error!);
|
|
152
|
+
}
|
|
153
|
+
final targetAppVersionList = filterCompatibleAppVersions(
|
|
154
|
+
((targetRes.data as List?) ?? [])
|
|
155
|
+
.map((row) =>
|
|
156
|
+
_SupabaseTargetAppVersionRow((row as Map)['target_app_version']))
|
|
157
|
+
.where((row) => row.targetAppVersion != null)
|
|
158
|
+
.map((row) => row.targetAppVersion!)
|
|
159
|
+
.toList(),
|
|
160
|
+
appArgs.appVersion,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
final res = await supabase.rpc(
|
|
164
|
+
'get_update_info_by_app_version',
|
|
165
|
+
params: {
|
|
166
|
+
'app_platform': appArgs.platform.value,
|
|
167
|
+
'app_version': appArgs.appVersion,
|
|
168
|
+
'bundle_id': appArgs.bundleId,
|
|
169
|
+
'min_bundle_id': minBundleId,
|
|
170
|
+
'target_channel': channel,
|
|
171
|
+
'target_app_version_list': targetAppVersionList,
|
|
172
|
+
'cohort': appArgs.cohort,
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
if (res.error != null) {
|
|
176
|
+
throw createSupabaseError(res.error!);
|
|
177
|
+
}
|
|
178
|
+
final list = (res.data as List?) ?? [];
|
|
179
|
+
if (list.isEmpty) return null;
|
|
180
|
+
return mapUpdateInfoRow(_rowToUpdateInfo(list.first));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
final fpArgs = args as FingerprintGetBundlesArgs;
|
|
184
|
+
final res = await supabase.rpc(
|
|
185
|
+
'get_update_info_by_fingerprint_hash',
|
|
186
|
+
params: {
|
|
187
|
+
'app_platform': fpArgs.platform.value,
|
|
188
|
+
'bundle_id': fpArgs.bundleId,
|
|
189
|
+
'min_bundle_id': minBundleId,
|
|
190
|
+
'target_channel': channel,
|
|
191
|
+
'target_fingerprint_hash': fpArgs.fingerprintHash,
|
|
192
|
+
'cohort': fpArgs.cohort,
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
if (res.error != null) {
|
|
196
|
+
throw createSupabaseError(res.error!);
|
|
197
|
+
}
|
|
198
|
+
final list = (res.data as List?) ?? [];
|
|
199
|
+
if (list.isEmpty) return null;
|
|
200
|
+
return mapUpdateInfoRow(_rowToUpdateInfo(list.first));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_SupabaseUpdateInfoRow _rowToUpdateInfo(Object raw) {
|
|
204
|
+
final row = (raw as Map).cast<String, dynamic>();
|
|
205
|
+
return _SupabaseUpdateInfoRow(
|
|
206
|
+
id: row['id'] as String,
|
|
207
|
+
shouldForceUpdate: row['should_force_update'] as bool,
|
|
208
|
+
message: row['message'] as String?,
|
|
209
|
+
status: row['status'] as String,
|
|
210
|
+
storageUri: row['storage_uri'] as String?,
|
|
211
|
+
fileHash: row['file_hash'] as String?,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
@override
|
|
216
|
+
Future<Bundle?> getBundleById(String bundleId) async {
|
|
217
|
+
final results = await Future.wait([
|
|
218
|
+
supabase
|
|
219
|
+
.from('bundles')
|
|
220
|
+
.select(bundleSelectColumns)
|
|
221
|
+
.eq('id', bundleId)
|
|
222
|
+
.single()
|
|
223
|
+
.execute(),
|
|
224
|
+
fetchPatchMap([bundleId]),
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
final res = results[0] as SupabaseResponseLike;
|
|
228
|
+
if (res.data == null || res.error != null) return null;
|
|
229
|
+
final row = SupabaseBundleRow.fromJson(
|
|
230
|
+
(res.data as Map).cast<String, dynamic>(),
|
|
231
|
+
);
|
|
232
|
+
final patches = (results[1] as Map<String, List<SupabaseBundlePatchRow>>)[bundleId] ?? [];
|
|
233
|
+
return mapRowToBundle(row, patches);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
@override
|
|
237
|
+
Future<Paginated<List<Bundle>>> getBundles(
|
|
238
|
+
DatabaseBundleQueryOptions options,
|
|
239
|
+
) async {
|
|
240
|
+
final where = options.where;
|
|
241
|
+
final limit = options.limit;
|
|
242
|
+
final orderBy = options.orderBy ??
|
|
243
|
+
const DatabaseBundleQueryOrder(direction: 'desc');
|
|
244
|
+
final offset = options.offset ??
|
|
245
|
+
(options.page != null ? (options.page! - 1) * limit : 0);
|
|
246
|
+
|
|
247
|
+
if ((where?.targetAppVersionIn != null &&
|
|
248
|
+
where!.targetAppVersionIn!.isEmpty) ||
|
|
249
|
+
(where?.id?.ins != null && where!.id!.ins!.isEmpty)) {
|
|
250
|
+
return Paginated(
|
|
251
|
+
data: const [],
|
|
252
|
+
pagination: calculatePagination(0, limit: limit, offset: offset),
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
var countQuery = supabase
|
|
257
|
+
.from('bundles')
|
|
258
|
+
.select('*', count: true, head: true);
|
|
259
|
+
countQuery = _applyWhereFilters(countQuery, where);
|
|
260
|
+
|
|
261
|
+
final countRes = await countQuery.execute();
|
|
262
|
+
final total = countRes.count ?? 0;
|
|
263
|
+
|
|
264
|
+
var query = supabase
|
|
265
|
+
.from('bundles')
|
|
266
|
+
.select(bundleSelectColumns)
|
|
267
|
+
.order('id', ascending: orderBy.direction == 'asc');
|
|
268
|
+
query = _applyWhereFilters(query, where);
|
|
269
|
+
|
|
270
|
+
if (limit > 0) query = query.limit(limit);
|
|
271
|
+
if (offset > 0) query = query.range(offset, offset + limit - 1);
|
|
272
|
+
|
|
273
|
+
final res = await query.execute();
|
|
274
|
+
if (res.error != null) {
|
|
275
|
+
throw createSupabaseError(res.error!);
|
|
276
|
+
}
|
|
277
|
+
final data = (res.data as List?) ?? [];
|
|
278
|
+
final patchMap = await fetchPatchMap(
|
|
279
|
+
data.map((b) => (b as Map)['id'] as String).toList(),
|
|
280
|
+
);
|
|
281
|
+
final bundles = data.map((b) {
|
|
282
|
+
final row = SupabaseBundleRow.fromJson((b as Map).cast<String, dynamic>());
|
|
283
|
+
return mapRowToBundle(row, patchMap[row.id] ?? []);
|
|
284
|
+
}).toList();
|
|
285
|
+
|
|
286
|
+
return Paginated(
|
|
287
|
+
data: bundles,
|
|
288
|
+
pagination: calculatePagination(total, limit: limit, offset: offset),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
SupabaseFilterBuilderLike _applyWhereFilters(
|
|
293
|
+
SupabaseFilterBuilderLike query,
|
|
294
|
+
DatabaseBundleQueryWhere? where,
|
|
295
|
+
) {
|
|
296
|
+
if (where == null) return query;
|
|
297
|
+
if (where.channel != null) {
|
|
298
|
+
query = query.eq('channel', where.channel!);
|
|
299
|
+
}
|
|
300
|
+
if (where.platform != null) {
|
|
301
|
+
query = query.eq('platform', where.platform!.value);
|
|
302
|
+
}
|
|
303
|
+
if (where.enabled != null) {
|
|
304
|
+
query = query.eq('enabled', where.enabled!);
|
|
305
|
+
}
|
|
306
|
+
if (where.fingerprintHash != null) {
|
|
307
|
+
query = where.fingerprintHash == null
|
|
308
|
+
? query.isFilter('fingerprint_hash', null)
|
|
309
|
+
: query.eq('fingerprint_hash', where.fingerprintHash!);
|
|
310
|
+
}
|
|
311
|
+
if (where.targetAppVersion != null) {
|
|
312
|
+
query = where.targetAppVersion == null
|
|
313
|
+
? query.isFilter('target_app_version', null)
|
|
314
|
+
: query.eq('target_app_version', where.targetAppVersion!);
|
|
315
|
+
}
|
|
316
|
+
if (where.targetAppVersionIn != null) {
|
|
317
|
+
query = query.in_('target_app_version', where.targetAppVersionIn!);
|
|
318
|
+
}
|
|
319
|
+
if (where.targetAppVersionNotNull == true) {
|
|
320
|
+
query = query.not('target_app_version', 'is', null);
|
|
321
|
+
}
|
|
322
|
+
if (where.id != null) {
|
|
323
|
+
if (where.id!.eq != null) query = query.eq('id', where.id!.eq!);
|
|
324
|
+
if (where.id!.gt != null) query = query.gt('id', where.id!.gt!);
|
|
325
|
+
if (where.id!.gte != null) query = query.gte('id', where.id!.gte!);
|
|
326
|
+
if (where.id!.lt != null) query = query.lt('id', where.id!.lt!);
|
|
327
|
+
if (where.id!.lte != null) query = query.lte('id', where.id!.lte!);
|
|
328
|
+
if (where.id!.ins != null) {
|
|
329
|
+
query = query.in_('id', where.id!.ins!);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return query;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@override
|
|
336
|
+
Future<List<String>> getChannels() async {
|
|
337
|
+
final res = await supabase.rpc('get_channels');
|
|
338
|
+
if (res.error != null) {
|
|
339
|
+
throw createSupabaseError(res.error!);
|
|
340
|
+
}
|
|
341
|
+
final data = (res.data as List?) ?? [];
|
|
342
|
+
return data
|
|
343
|
+
.map((row) => (row as Map)['channel'] as String)
|
|
344
|
+
.toList();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
@override
|
|
348
|
+
Future<void> commitBundle({
|
|
349
|
+
required List<BundleChange> changedSets,
|
|
350
|
+
}) async {
|
|
351
|
+
if (changedSets.isEmpty) return;
|
|
352
|
+
|
|
353
|
+
for (final op in changedSets) {
|
|
354
|
+
if (op.operation == BundleChangeOperation.delete) {
|
|
355
|
+
final id = op.data.id;
|
|
356
|
+
final patchDelete = await supabase
|
|
357
|
+
.from('bundle_patches')
|
|
358
|
+
.delete()
|
|
359
|
+
.eq('bundle_id', id)
|
|
360
|
+
.execute();
|
|
361
|
+
if (patchDelete.error != null) {
|
|
362
|
+
throw StateError(
|
|
363
|
+
'Failed to delete bundle patches: ${patchDelete.error}',
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
final basePatchDelete = await supabase
|
|
367
|
+
.from('bundle_patches')
|
|
368
|
+
.delete()
|
|
369
|
+
.eq('base_bundle_id', id)
|
|
370
|
+
.execute();
|
|
371
|
+
if (basePatchDelete.error != null) {
|
|
372
|
+
throw StateError(
|
|
373
|
+
'Failed to delete base bundle patches: ${basePatchDelete.error}',
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
final deleteRes = await supabase
|
|
377
|
+
.from('bundles')
|
|
378
|
+
.delete()
|
|
379
|
+
.eq('id', id)
|
|
380
|
+
.execute();
|
|
381
|
+
if (deleteRes.error != null) {
|
|
382
|
+
throw StateError('Failed to delete bundle: ${deleteRes.error}');
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
final bundle = op.data;
|
|
386
|
+
final patchRows = bundleToPatchRows(bundle);
|
|
387
|
+
final upsertRes = await supabase
|
|
388
|
+
.from('bundles')
|
|
389
|
+
.upsert(bundleToRow(bundle), onConflict: 'id');
|
|
390
|
+
if (upsertRes is SupabaseResponseLike && upsertRes.error != null) {
|
|
391
|
+
throw createSupabaseError(upsertRes.error!);
|
|
392
|
+
} else if (upsertRes is Map && upsertRes['error'] != null) {
|
|
393
|
+
throw createSupabaseError(upsertRes['error']!);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
final patchDelete = await supabase
|
|
397
|
+
.from('bundle_patches')
|
|
398
|
+
.delete()
|
|
399
|
+
.eq('bundle_id', bundle.id)
|
|
400
|
+
.execute();
|
|
401
|
+
if (patchDelete.error != null) {
|
|
402
|
+
throw createSupabaseError(patchDelete.error!);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (patchRows.isNotEmpty) {
|
|
406
|
+
final patchUpsert = await supabase
|
|
407
|
+
.from('bundle_patches')
|
|
408
|
+
.upsert(patchRows.map((p) => p.toJson()).toList(),
|
|
409
|
+
onConflict: 'id');
|
|
410
|
+
if (patchUpsert is SupabaseResponseLike && patchUpsert.error != null) {
|
|
411
|
+
throw createSupabaseError(patchUpsert.error!);
|
|
412
|
+
} else if (patchUpsert is Map && patchUpsert['error'] != null) {
|
|
413
|
+
throw createSupabaseError(patchUpsert['error']!);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@override
|
|
421
|
+
Future<void> onUnmount() async {}
|
|
422
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/// Faithful port of hot-updater `plugins/supabase/src/supabaseEdgeFunctionDatabase.ts`.
|
|
2
|
+
library;
|
|
3
|
+
|
|
4
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
|
|
5
|
+
|
|
6
|
+
import 'supabase_database.dart';
|
|
7
|
+
|
|
8
|
+
/// Config for the Supabase edge-function database plugin.
|
|
9
|
+
class SupabaseEdgeFunctionDatabaseConfig {
|
|
10
|
+
final String supabaseUrl;
|
|
11
|
+
final String supabaseServiceRoleKey;
|
|
12
|
+
|
|
13
|
+
const SupabaseEdgeFunctionDatabaseConfig({
|
|
14
|
+
required this.supabaseUrl,
|
|
15
|
+
required this.supabaseServiceRoleKey,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Edge-function variant of [supabaseDatabase]: builds the same database plugin
|
|
20
|
+
/// from a url + service-role key pair (no anon key).
|
|
21
|
+
DatabasePlugin Function() supabaseEdgeFunctionDatabase(
|
|
22
|
+
SupabaseEdgeFunctionDatabaseConfig config, [
|
|
23
|
+
DatabasePluginHooks? hooks,
|
|
24
|
+
]) {
|
|
25
|
+
return supabaseDatabase(
|
|
26
|
+
SupabaseDatabaseConfig(
|
|
27
|
+
supabaseUrl: config.supabaseUrl,
|
|
28
|
+
supabaseServiceRoleKey: config.supabaseServiceRoleKey,
|
|
29
|
+
),
|
|
30
|
+
hooks,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/// Faithful port of hot-updater `plugins/supabase/src/supabaseEdgeFunctionStorage.ts`.
|
|
2
|
+
library;
|
|
3
|
+
|
|
4
|
+
import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
|
|
5
|
+
|
|
6
|
+
import 'supabase_client_adapter.dart';
|
|
7
|
+
import 'supabase_client_http.dart';
|
|
8
|
+
import 'supabase_signed_url_batcher.dart';
|
|
9
|
+
|
|
10
|
+
/// Config for the Supabase edge-function storage plugin.
|
|
11
|
+
class SupabaseEdgeFunctionStorageConfig {
|
|
12
|
+
final String supabaseUrl;
|
|
13
|
+
final String supabaseServiceRoleKey;
|
|
14
|
+
final int? signedUrlExpiresIn;
|
|
15
|
+
|
|
16
|
+
const SupabaseEdgeFunctionStorageConfig({
|
|
17
|
+
required this.supabaseUrl,
|
|
18
|
+
required this.supabaseServiceRoleKey,
|
|
19
|
+
this.signedUrlExpiresIn,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
SupabaseClientFactory get clientFactory =>
|
|
23
|
+
(url, key) => createSupabaseHttpClient(url, key);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// Parse a `supabase-storage://` URI into bucket + key.
|
|
27
|
+
ParsedStorageUri parseSupabaseEdgeStorageUri(String storageUri) =>
|
|
28
|
+
parseStorageUri(storageUri, 'supabase-storage');
|
|
29
|
+
|
|
30
|
+
/// Edge-function variant of [supabaseStorage]: a runtime-only storage plugin
|
|
31
|
+
/// (no node profile) backed by the Supabase client.
|
|
32
|
+
final supabaseEdgeFunctionStorage =
|
|
33
|
+
createRuntimeStoragePlugin<SupabaseEdgeFunctionStorageConfig>(
|
|
34
|
+
name: 'supabaseEdgeFunctionStorage',
|
|
35
|
+
supportedProtocol: 'supabase-storage',
|
|
36
|
+
factory: (config) {
|
|
37
|
+
final supabase = config.clientFactory(
|
|
38
|
+
config.supabaseUrl,
|
|
39
|
+
config.supabaseServiceRoleKey,
|
|
40
|
+
);
|
|
41
|
+
final resolveSignedUrl = createSupabaseSignedUrlBatcher(
|
|
42
|
+
createSignedUrls: (bucketName, keys, expiresIn) =>
|
|
43
|
+
supabase.storage.from(bucketName).createSignedUrls(keys, expiresIn),
|
|
44
|
+
expiresIn: config.signedUrlExpiresIn ?? 3600,
|
|
45
|
+
formatObjectPath: (bucketName, key) => '$bucketName/$key',
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return RuntimeStorageProfileImplEdge(
|
|
49
|
+
supabase: supabase,
|
|
50
|
+
resolveSignedUrl: resolveSignedUrl,
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
class RuntimeStorageProfileImplEdge implements RuntimeStorageProfile {
|
|
56
|
+
RuntimeStorageProfileImplEdge({
|
|
57
|
+
required this.supabase,
|
|
58
|
+
required this.resolveSignedUrl,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
final SupabaseClientLike supabase;
|
|
62
|
+
final ResolveSignedUrl resolveSignedUrl;
|
|
63
|
+
|
|
64
|
+
@override
|
|
65
|
+
Future<String?> readText(String storageUri) async {
|
|
66
|
+
final parsed = parseSupabaseEdgeStorageUri(storageUri);
|
|
67
|
+
if (parsed.bucket.isEmpty || parsed.key.isEmpty) {
|
|
68
|
+
throw StateError('Invalid Supabase storage URI');
|
|
69
|
+
}
|
|
70
|
+
final res = await supabase.storage.from(parsed.bucket).download(parsed.key);
|
|
71
|
+
if (res.error != null) {
|
|
72
|
+
final msg = (res.message ?? _errorToString(res.error))
|
|
73
|
+
.toString();
|
|
74
|
+
if (msg.contains('not found')) return null;
|
|
75
|
+
throw StateError('Failed to read storage text: $msg');
|
|
76
|
+
}
|
|
77
|
+
if (res.data == null) return null;
|
|
78
|
+
return String.fromCharCodes(res.data!);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@override
|
|
82
|
+
Future<Map<String, String>> getDownloadUrl(String storageUri) async {
|
|
83
|
+
final parsed = parseSupabaseEdgeStorageUri(storageUri);
|
|
84
|
+
if (parsed.bucket.isEmpty || parsed.key.isEmpty) {
|
|
85
|
+
throw StateError('Invalid Supabase storage URI');
|
|
86
|
+
}
|
|
87
|
+
final signedUrl = await resolveSignedUrl(parsed.bucket, parsed.key);
|
|
88
|
+
return {'fileUrl': signedUrl};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
String _errorToString(Object? error) {
|
|
92
|
+
if (error is Error) return error.toString();
|
|
93
|
+
if (error is Exception) return error.toString();
|
|
94
|
+
if (error is Map && error['message'] is String) {
|
|
95
|
+
return error['message'] as String;
|
|
96
|
+
}
|
|
97
|
+
return error.toString();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/// Faithful port of hot-updater `plugins/supabase/src/supabaseSignedUrlBatcher.ts`.
|
|
2
|
+
library;
|
|
3
|
+
|
|
4
|
+
import 'dart:async';
|
|
5
|
+
|
|
6
|
+
import 'supabase_client_adapter.dart';
|
|
7
|
+
|
|
8
|
+
/// A pending signed URL request waiting for the next flush.
|
|
9
|
+
class _PendingSignedUrl {
|
|
10
|
+
final String key;
|
|
11
|
+
final void Function(Object error) reject;
|
|
12
|
+
final void Function(String signedUrl) resolve;
|
|
13
|
+
|
|
14
|
+
_PendingSignedUrl({
|
|
15
|
+
required this.key,
|
|
16
|
+
required this.reject,
|
|
17
|
+
required this.resolve,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Extract a human-readable message from an unknown error value.
|
|
22
|
+
String _getErrorMessage(Object? error) {
|
|
23
|
+
if (error is Error) return error.toString();
|
|
24
|
+
if (error is Exception) return error.toString();
|
|
25
|
+
if (error is Map && error['message'] is String) {
|
|
26
|
+
return error['message'] as String;
|
|
27
|
+
}
|
|
28
|
+
return error.toString();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Signature of the batch signed URL generator. Returns the same shape as the
|
|
32
|
+
/// Supabase storage `createSignedUrls` call: a list of results aligned to the
|
|
33
|
+
/// input key order.
|
|
34
|
+
typedef CreateSignedUrls = Future<SupabaseSignedUrlListResult> Function(
|
|
35
|
+
String bucketName,
|
|
36
|
+
List<String> keys,
|
|
37
|
+
int expiresIn,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
/// Creates a function that batches signed URL generation requests per
|
|
41
|
+
/// bucket and flushes them on the next microtask, mirroring Supabase's
|
|
42
|
+
/// `createSignedUrls` batch API.
|
|
43
|
+
///
|
|
44
|
+
/// [createSignedUrls] must take a bucket name, list of keys, and expiry,
|
|
45
|
+
/// and return a [SupabaseSignedUrlListResult] whose `data` entries are aligned
|
|
46
|
+
/// to the input key order.
|
|
47
|
+
/// [formatObjectPath] builds the human-readable object path used in errors.
|
|
48
|
+
ResolveSignedUrl createSupabaseSignedUrlBatcher({
|
|
49
|
+
required CreateSignedUrls createSignedUrls,
|
|
50
|
+
required int expiresIn,
|
|
51
|
+
required String Function(String bucketName, String key) formatObjectPath,
|
|
52
|
+
}) {
|
|
53
|
+
var pendingByBucket = <String, List<_PendingSignedUrl>>{};
|
|
54
|
+
var flushScheduled = false;
|
|
55
|
+
|
|
56
|
+
Object createSignedUrlError(
|
|
57
|
+
String bucketName,
|
|
58
|
+
String key,
|
|
59
|
+
Object error,
|
|
60
|
+
) =>
|
|
61
|
+
StateError(
|
|
62
|
+
'Failed to generate download URL for '
|
|
63
|
+
'"${formatObjectPath(bucketName, key)}": ${_getErrorMessage(error)}',
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
Future<void> flush() async {
|
|
67
|
+
final batches = pendingByBucket;
|
|
68
|
+
pendingByBucket = <String, List<_PendingSignedUrl>>{};
|
|
69
|
+
flushScheduled = false;
|
|
70
|
+
|
|
71
|
+
await Future.wait(
|
|
72
|
+
batches.entries.map((entry) async {
|
|
73
|
+
final bucketName = entry.key;
|
|
74
|
+
final pending = entry.value;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
final result = await createSignedUrls(
|
|
78
|
+
bucketName,
|
|
79
|
+
pending.map((p) => p.key).toList(),
|
|
80
|
+
expiresIn,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
final data = result.data;
|
|
84
|
+
if (result.error != null || data == null) {
|
|
85
|
+
final batchError =
|
|
86
|
+
result.error ?? StateError('missing signed URL response');
|
|
87
|
+
for (final request in pending) {
|
|
88
|
+
request.reject(
|
|
89
|
+
createSignedUrlError(bucketName, request.key, batchError),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (var i = 0; i < pending.length; i++) {
|
|
96
|
+
final request = pending[i];
|
|
97
|
+
final row = data[i];
|
|
98
|
+
if (row.error == null && row.signedUrl != null) {
|
|
99
|
+
request.resolve(row.signedUrl!);
|
|
100
|
+
} else {
|
|
101
|
+
request.reject(
|
|
102
|
+
createSignedUrlError(
|
|
103
|
+
bucketName,
|
|
104
|
+
request.key,
|
|
105
|
+
row.error ?? StateError('missing signed URL'),
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
for (final request in pending) {
|
|
112
|
+
request.reject(createSignedUrlError(bucketName, request.key, error));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (String bucketName, String key) {
|
|
120
|
+
final completer = Completer<String>();
|
|
121
|
+
final pending =
|
|
122
|
+
pendingByBucket[bucketName] ?? <_PendingSignedUrl>[];
|
|
123
|
+
pending.add(
|
|
124
|
+
_PendingSignedUrl(
|
|
125
|
+
key: key,
|
|
126
|
+
reject: completer.completeError,
|
|
127
|
+
resolve: completer.complete,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
pendingByBucket[bucketName] = pending;
|
|
131
|
+
|
|
132
|
+
if (!flushScheduled) {
|
|
133
|
+
flushScheduled = true;
|
|
134
|
+
Future.microtask(() => flush());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return completer.future;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Resolved function returned by [createSupabaseSignedUrlBatcher].
|
|
142
|
+
typedef ResolveSignedUrl = Future<String> Function(
|
|
143
|
+
String bucketName,
|
|
144
|
+
String key,
|
|
145
|
+
);
|