@_nazmiforreal/flutter-ota 0.1.11 → 0.1.13
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/README.md +1 -1
- package/bin/flutter-ota-linux-x64 +0 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +46 -76
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +140 -166
- package/dart-src/packages/cli-tools/README.md +1 -1
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +30 -17
- package/dart-src/packages/cli-tools/lib/src/backend.dart +30 -31
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -1
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +44 -43
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +265 -239
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +75 -66
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +39 -35
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +30 -27
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +88 -72
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +66 -29
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +10 -6
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +299 -190
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +24 -23
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +111 -97
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +237 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +43 -34
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +57 -53
- package/dart-src/packages/cli-tools/lib/src/config.dart +227 -93
- package/dart-src/packages/cli-tools/lib/src/operations.dart +9 -8
- package/dart-src/packages/cli-tools/lib/src/pack.dart +26 -25
- package/dart-src/packages/cli-tools/lib/src/pocketbase/data_bootstrap.dart +39 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/hooks/bundles.pb.js +56 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +189 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +123 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +237 -0
- package/dart-src/packages/cli-tools/lib/src/runner.dart +36 -15
- package/dart-src/packages/cli-tools/lib/src/sign.dart +13 -5
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +10 -13
- package/dart-src/packages/cli-tools/lib/src/util.dart +3 -1
- package/dart-src/packages/cli-tools/pubspec.lock +69 -116
- package/dart-src/packages/cli-tools/pubspec.yaml +25 -7
- package/dart-src/packages/cli-tools/test/backends_test.dart +62 -32
- package/dart-src/packages/cli-tools/test/build_pack_test.dart +4 -4
- package/dart-src/packages/cli-tools/test/cli_test.dart +89 -42
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +8 -13
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +23 -18
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +197 -0
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +70 -46
- package/dart-src/packages/cli-tools/test/pocketbase_installer_test.dart +72 -0
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +107 -0
- package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +59 -0
- package/dart-src/packages/core/README.md +2 -2
- package/dart-src/packages/core/example/lib/main.dart +69 -0
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +1 -1
- package/dart-src/packages/core/lib/src/app_update_info.dart +17 -15
- package/dart-src/packages/core/lib/src/bundle.dart +102 -94
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +1 -2
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +15 -16
- package/dart-src/packages/core/lib/src/changed_asset.dart +24 -22
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +13 -13
- package/dart-src/packages/core/lib/src/metadata.dart +9 -7
- package/dart-src/packages/core/lib/src/patch_info.dart +8 -7
- package/dart-src/packages/core/lib/src/platform.dart +4 -3
- package/dart-src/packages/core/lib/src/rollout.dart +14 -11
- package/dart-src/packages/core/lib/src/semver_range.dart +49 -39
- package/dart-src/packages/core/lib/src/semver_version.dart +8 -7
- package/dart-src/packages/core/lib/src/status.dart +4 -3
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +7 -7
- package/dart-src/packages/core/lib/src/uuid.dart +1 -2
- package/dart-src/packages/core/pubspec.yaml +6 -7
- package/dart-src/packages/core/test/rollout_test.dart +15 -8
- package/dart-src/packages/core/test/semver_test.dart +5 -2
- package/dart-src/packages/core/test/types_test.dart +26 -24
- package/dart-src/packages/core/test/uuid_test.dart +3 -1
- package/dart-src/packages/core/tool/dbg.dart +1 -0
- package/dart-src/plugins/aws/CHANGELOG.md +6 -0
- package/dart-src/plugins/aws/example/lib/main.dart +60 -0
- package/dart-src/plugins/aws/lib/flutter_ota_kit_aws.dart +5 -1
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +11 -16
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +7 -2
- package/dart-src/plugins/aws/lib/src/aws_database.dart +14 -18
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +23 -18
- package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +5 -4
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +2 -3
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +10 -16
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +6 -9
- package/dart-src/plugins/aws/pubspec.yaml +15 -10
- package/dart-src/plugins/aws/test/aws_database_test.dart +51 -57
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +8 -13
- package/dart-src/plugins/cloudflare/CHANGELOG.md +7 -0
- package/dart-src/plugins/cloudflare/example/lib/main.dart +62 -0
- package/dart-src/plugins/cloudflare/lib/flutter_ota_kit_cloudflare.dart +4 -2
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +3 -3
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +3 -2
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +24 -27
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +22 -30
- package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +21 -29
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +7 -7
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +7 -15
- package/dart-src/plugins/cloudflare/pubspec.yaml +14 -9
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +46 -42
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +100 -90
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +23 -18
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +8 -13
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +1 -4
- package/dart-src/plugins/plugin-core/CHANGELOG.md +16 -0
- package/dart-src/plugins/plugin-core/example/lib/main.dart +61 -0
- package/dart-src/plugins/plugin-core/lib/flutter_ota_kit_plugin_core.dart +3 -7
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +5 -5
- package/dart-src/plugins/plugin-core/lib/src/bundle_helpers.dart +23 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +16 -10
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +31 -26
- package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +5 -1
- package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +78 -71
- package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +56 -35
- package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +10 -13
- package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +30 -17
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +3 -1
- package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +5 -3
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +2 -4
- package/dart-src/plugins/plugin-core/lib/src/storage_helpers.dart +18 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +7 -6
- package/dart-src/plugins/plugin-core/pubspec.yaml +12 -9
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +13 -9
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +28 -15
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +254 -122
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +36 -32
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +18 -15
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +8 -3
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +682 -693
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +45 -21
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +76 -13
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +20 -9
- package/dart-src/plugins/pocketbase/CHANGELOG.md +8 -0
- package/dart-src/plugins/pocketbase/LICENSE +21 -0
- package/dart-src/plugins/pocketbase/README.md +54 -0
- package/dart-src/plugins/pocketbase/example/lib/main.dart +62 -0
- package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +28 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +121 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +349 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_config.dart +61 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +278 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +255 -0
- package/dart-src/plugins/pocketbase/pubspec.yaml +26 -0
- package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +12 -0
- package/dart-src/plugins/postgres/CHANGELOG.md +6 -0
- package/dart-src/plugins/postgres/example/lib/main.dart +67 -0
- package/dart-src/plugins/postgres/lib/flutter_ota_kit_postgres.dart +3 -6
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +32 -52
- package/dart-src/plugins/postgres/lib/src/postgres_config.dart +3 -1
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +28 -38
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +16 -14
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +26 -14
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +27 -28
- package/dart-src/plugins/postgres/pubspec.yaml +14 -9
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +62 -39
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +3 -4
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +102 -73
- package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +60 -0
- package/dart-src/plugins/supabase/CHANGELOG.md +17 -0
- package/dart-src/plugins/supabase/README.md +2 -2
- package/dart-src/plugins/supabase/example/lib/main.dart +63 -0
- package/dart-src/plugins/supabase/lib/flutter_ota_kit_supabase.dart +2 -0
- package/dart-src/plugins/supabase/lib/src/error_message.dart +11 -0
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +34 -58
- package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +3 -13
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +34 -52
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +38 -30
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +21 -29
- package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +7 -19
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +18 -29
- package/dart-src/plugins/supabase/lib/src/types.dart +26 -26
- package/dart-src/plugins/supabase/pubspec.yaml +14 -9
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +57 -57
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +30 -27
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +26 -35
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +27 -34
- package/package.json +2 -2
- package/scripts/vendor.sh +1 -1
|
@@ -25,20 +25,19 @@ Bundle _makeBundle({
|
|
|
25
25
|
String? targetAppVersion,
|
|
26
26
|
String? message,
|
|
27
27
|
List<BundlePatchArtifact>? patches,
|
|
28
|
-
}) =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
);
|
|
28
|
+
}) => Bundle(
|
|
29
|
+
id: id,
|
|
30
|
+
channel: channel,
|
|
31
|
+
enabled: enabled,
|
|
32
|
+
shouldForceUpdate: false,
|
|
33
|
+
fileHash: 'h-$id',
|
|
34
|
+
platform: Platform.android,
|
|
35
|
+
targetAppVersion: targetAppVersion,
|
|
36
|
+
storageUri: 's3://$id',
|
|
37
|
+
fingerprintHash: fingerprintHash,
|
|
38
|
+
message: message,
|
|
39
|
+
patches: patches,
|
|
40
|
+
);
|
|
42
41
|
|
|
43
42
|
void main() {
|
|
44
43
|
group('cloudflare d1Database', () {
|
|
@@ -61,48 +60,51 @@ void main() {
|
|
|
61
60
|
expect(channels, containsAll(['production', 'staging']));
|
|
62
61
|
});
|
|
63
62
|
|
|
64
|
-
test(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
() async {
|
|
95
|
-
await plugin.appendBundle(_makeBundle(id: 'b1'));
|
|
96
|
-
await plugin.commitBundle();
|
|
63
|
+
test(
|
|
64
|
+
'getBundles returns bundles and pagination; filters by channel',
|
|
65
|
+
() async {
|
|
66
|
+
for (final id in ['b1', 'b2', 'b3']) {
|
|
67
|
+
await plugin.appendBundle(_makeBundle(id: id));
|
|
68
|
+
}
|
|
69
|
+
await plugin.commitBundle();
|
|
70
|
+
|
|
71
|
+
final res = await plugin.getBundles(
|
|
72
|
+
DatabaseBundleQueryOptions(
|
|
73
|
+
where: const DatabaseBundleQueryWhere(),
|
|
74
|
+
limit: 2,
|
|
75
|
+
offset: 0,
|
|
76
|
+
orderBy: const DatabaseBundleQueryOrder(direction: 'desc'),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
expect(res.data, hasLength(2));
|
|
80
|
+
expect(res.pagination.total, 3);
|
|
81
|
+
expect(res.pagination.hasNextPage, isTrue);
|
|
82
|
+
|
|
83
|
+
final staging = await plugin.getBundles(
|
|
84
|
+
DatabaseBundleQueryOptions(
|
|
85
|
+
where: const DatabaseBundleQueryWhere(channel: 'staging'),
|
|
86
|
+
limit: 10,
|
|
87
|
+
offset: 0,
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
expect(staging.data, isEmpty);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
97
93
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
test(
|
|
95
|
+
'getBundleById returns null for missing, bundle for existing',
|
|
96
|
+
() async {
|
|
97
|
+
await plugin.appendBundle(_makeBundle(id: 'b1'));
|
|
98
|
+
await plugin.commitBundle();
|
|
99
|
+
|
|
100
|
+
expect(await plugin.getBundleById('missing'), isNull);
|
|
101
|
+
final got = await plugin.getBundleById('b1');
|
|
102
|
+
expect(got, isNotNull);
|
|
103
|
+
expect(got!.id, 'b1');
|
|
104
|
+
},
|
|
105
|
+
);
|
|
103
106
|
|
|
104
|
-
test('commitBundle append -> update -> delete reflects in store',
|
|
105
|
-
() async {
|
|
107
|
+
test('commitBundle append -> update -> delete reflects in store', () async {
|
|
106
108
|
await plugin.appendBundle(_makeBundle(id: 'b1', message: 'original'));
|
|
107
109
|
await plugin.commitBundle();
|
|
108
110
|
expect((await plugin.getBundleById('b1'))!.message, 'original');
|
|
@@ -118,43 +120,51 @@ void main() {
|
|
|
118
120
|
expect(await plugin.getBundleById('b1'), isNull);
|
|
119
121
|
});
|
|
120
122
|
|
|
121
|
-
test(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await plugin.commitBundle();
|
|
145
|
-
|
|
146
|
-
final info = await plugin.getUpdateInfo(
|
|
147
|
-
AppVersionGetBundlesArgs(
|
|
148
|
-
platform: Platform.android,
|
|
149
|
-
bundleId: nilUuid,
|
|
150
|
-
appVersion: '9.9.9',
|
|
151
|
-
channel: 'production',
|
|
152
|
-
minBundleId: nilUuid,
|
|
153
|
-
),
|
|
154
|
-
);
|
|
123
|
+
test(
|
|
124
|
+
'getUpdateInfo (appVersion) returns UpdateInfo for compatible bundle',
|
|
125
|
+
() async {
|
|
126
|
+
await plugin.appendBundle(
|
|
127
|
+
_makeBundle(id: 'b1', targetAppVersion: '1.0.0'),
|
|
128
|
+
);
|
|
129
|
+
await plugin.commitBundle();
|
|
130
|
+
|
|
131
|
+
final info = await plugin.getUpdateInfo(
|
|
132
|
+
AppVersionGetBundlesArgs(
|
|
133
|
+
platform: Platform.android,
|
|
134
|
+
bundleId: nilUuid,
|
|
135
|
+
appVersion: '1.0.0',
|
|
136
|
+
channel: 'production',
|
|
137
|
+
minBundleId: nilUuid,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
expect(info, isNotNull);
|
|
142
|
+
expect(info!.id, 'b1');
|
|
143
|
+
expect(info.status, UpdateStatus.update);
|
|
144
|
+
},
|
|
145
|
+
);
|
|
155
146
|
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
test(
|
|
148
|
+
'getUpdateInfo (appVersion) returns null when no compatible bundle',
|
|
149
|
+
() async {
|
|
150
|
+
await plugin.appendBundle(
|
|
151
|
+
_makeBundle(id: 'b1', targetAppVersion: '1.0.0'),
|
|
152
|
+
);
|
|
153
|
+
await plugin.commitBundle();
|
|
154
|
+
|
|
155
|
+
final info = await plugin.getUpdateInfo(
|
|
156
|
+
AppVersionGetBundlesArgs(
|
|
157
|
+
platform: Platform.android,
|
|
158
|
+
bundleId: nilUuid,
|
|
159
|
+
appVersion: '9.9.9',
|
|
160
|
+
channel: 'production',
|
|
161
|
+
minBundleId: nilUuid,
|
|
162
|
+
),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect(info, isNull);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
158
168
|
|
|
159
169
|
test('getUpdateInfo (fingerprint) returns matching bundle', () async {
|
|
160
170
|
await plugin.appendBundle(_makeBundle(id: 'fp', fingerprintHash: 'fp1'));
|
|
@@ -76,7 +76,9 @@ class MockD1Client implements D1ClientLike {
|
|
|
76
76
|
if (where.contains('target_app_version = ?')) {
|
|
77
77
|
if (row['target_app_version'] != params[p++]) return false;
|
|
78
78
|
}
|
|
79
|
-
if (where.contains(
|
|
79
|
+
if (where.contains(
|
|
80
|
+
'target_app_version IN (SELECT value FROM json_each(?))',
|
|
81
|
+
)) {
|
|
80
82
|
if (!_asList(params[p++]).contains(row['target_app_version'])) {
|
|
81
83
|
return false;
|
|
82
84
|
}
|
|
@@ -140,7 +142,9 @@ class MockD1Client implements D1ClientLike {
|
|
|
140
142
|
final channels = {
|
|
141
143
|
for (final row in store.bundleRows) row['channel'] as String,
|
|
142
144
|
};
|
|
143
|
-
return [
|
|
145
|
+
return [
|
|
146
|
+
for (final c in channels) {'channel': c},
|
|
147
|
+
];
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
if (sql.contains('SELECT target_app_version FROM bundles')) {
|
|
@@ -164,9 +168,11 @@ class MockD1Client implements D1ClientLike {
|
|
|
164
168
|
.where((row) => ids.contains(row['bundle_id']))
|
|
165
169
|
.toList();
|
|
166
170
|
rows.sort(
|
|
167
|
-
(a, b) =>
|
|
168
|
-
(a['
|
|
169
|
-
|
|
171
|
+
(a, b) =>
|
|
172
|
+
(a['order_index'] as num).compareTo(b['order_index'] as num) +
|
|
173
|
+
(a['base_bundle_id'] as String).compareTo(
|
|
174
|
+
b['base_bundle_id'] as String,
|
|
175
|
+
),
|
|
170
176
|
);
|
|
171
177
|
return rows;
|
|
172
178
|
}
|
|
@@ -195,8 +201,7 @@ class MockD1Client implements D1ClientLike {
|
|
|
195
201
|
'rollout_cohort_count': params[15],
|
|
196
202
|
'target_cohorts': params[16],
|
|
197
203
|
};
|
|
198
|
-
store.bundleRows
|
|
199
|
-
.removeWhere((r) => r['id'] == row['id']);
|
|
204
|
+
store.bundleRows.removeWhere((r) => r['id'] == row['id']);
|
|
200
205
|
store.bundleRows.add(row);
|
|
201
206
|
return const [];
|
|
202
207
|
}
|
|
@@ -219,8 +224,9 @@ class MockD1Client implements D1ClientLike {
|
|
|
219
224
|
if (sql.contains('DELETE FROM bundles')) {
|
|
220
225
|
final id = params[0];
|
|
221
226
|
store.bundleRows.removeWhere((r) => r['id'] == id);
|
|
222
|
-
store.patchRows
|
|
223
|
-
|
|
227
|
+
store.patchRows.removeWhere(
|
|
228
|
+
(r) => r['bundle_id'] == id || r['base_bundle_id'] == id,
|
|
229
|
+
);
|
|
224
230
|
return const [];
|
|
225
231
|
}
|
|
226
232
|
|
|
@@ -240,17 +246,16 @@ class MockD1Client implements D1ClientLike {
|
|
|
240
246
|
|
|
241
247
|
/// Build a [D1DatabaseConfig] wired to an in-memory [Store].
|
|
242
248
|
D1DatabaseConfig mockConfig(Store store) => D1DatabaseConfig(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
249
|
+
databaseId: 'test-db',
|
|
250
|
+
accountId: 'test-account',
|
|
251
|
+
cloudflareApiToken: 'test-token',
|
|
252
|
+
clientFactory: (_) => MockD1Client(store),
|
|
253
|
+
);
|
|
248
254
|
|
|
249
255
|
/// Construct the plugin with an in-memory store.
|
|
250
256
|
DatabasePlugin newPlugin(Store store) => d1Database(mockConfig(store))();
|
|
251
257
|
|
|
252
258
|
/// Construct the Workers D1 variant with an in-memory store.
|
|
253
|
-
DatabasePlugin newWorkerPlugin(Store store) =>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)();
|
|
259
|
+
DatabasePlugin newWorkerPlugin(Store store) => cloudflareWorkerDatabase(
|
|
260
|
+
CloudflareWorkerDatabaseConfig(getDb: () => MockD1Client(store)),
|
|
261
|
+
)();
|
|
@@ -29,8 +29,7 @@ class MockR2Client implements R2S3ClientLike {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
@override
|
|
32
|
-
Future<bool> headObject(String key) async =>
|
|
33
|
-
store.objects.containsKey(key);
|
|
32
|
+
Future<bool> headObject(String key) async => store.objects.containsKey(key);
|
|
34
33
|
|
|
35
34
|
@override
|
|
36
35
|
Future<List<int>> getObjectAsBytes(String key) async {
|
|
@@ -64,20 +63,16 @@ class MockR2Client implements R2S3ClientLike {
|
|
|
64
63
|
}
|
|
65
64
|
|
|
66
65
|
@override
|
|
67
|
-
Future<void> putObject(
|
|
68
|
-
String key,
|
|
69
|
-
List<int> body,
|
|
70
|
-
String contentType,
|
|
71
|
-
) async {
|
|
66
|
+
Future<void> putObject(String key, List<int> body, String contentType) async {
|
|
72
67
|
store.objects[key] = body;
|
|
73
68
|
}
|
|
74
69
|
}
|
|
75
70
|
|
|
76
71
|
/// Build an [R2S3StorageConfig] wired to an in-memory [Store].
|
|
77
72
|
R2S3StorageConfig mockR2Config(Store store) => R2S3StorageConfig(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
73
|
+
accountId: 'test-account',
|
|
74
|
+
bucketName: 'test-bucket',
|
|
75
|
+
accessKeyId: 'test-access-key',
|
|
76
|
+
secretAccessKey: 'test-secret',
|
|
77
|
+
clientFactory: (_) => MockR2Client(store),
|
|
78
|
+
);
|
|
@@ -35,10 +35,7 @@ void main() {
|
|
|
35
35
|
test('node upload + exists + downloadFile + delete', () async {
|
|
36
36
|
final node = plugin.profiles.node!;
|
|
37
37
|
final res = await node.upload('v1', tmpFile);
|
|
38
|
-
expect(
|
|
39
|
-
res['storageUri'],
|
|
40
|
-
'r2://test-bucket/v1/${basename(tmpFile)}',
|
|
41
|
-
);
|
|
38
|
+
expect(res['storageUri'], 'r2://test-bucket/v1/${basename(tmpFile)}');
|
|
42
39
|
expect(await node.exists(res['storageUri']!), isTrue);
|
|
43
40
|
|
|
44
41
|
await node.downloadFile(res['storageUri']!, outFile);
|
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 0.0.2
|
|
2
|
+
|
|
3
|
+
### Changed
|
|
4
|
+
|
|
5
|
+
- **Shared previously-duplicated backend helpers in one place.** Extracted
|
|
6
|
+
`ensureExpectedBucket` (the S3/R2 bucket-mismatch guard, formerly copied
|
|
7
|
+
byte-for-byte in the cloudflare + aws storage profiles), `normalizeMetadata`
|
|
8
|
+
(bundle `metadata` JSON-string/object normalization, formerly in the supabase +
|
|
9
|
+
postgres mappers and cloudflare's `parseMetadata`), `buildBundlePatchId`
|
|
10
|
+
(the `${bundleId}:${baseBundleId}` patch primary key, formerly in three mappers),
|
|
11
|
+
and `updateJsonKeyRegex` (the `update.json` key matcher, formerly duplicated in
|
|
12
|
+
the aws blob database + `createBlobDatabasePlugin`). The four backend plugins
|
|
13
|
+
now import these from `plugin-core` instead of maintaining their own copies.
|
|
14
|
+
Public API of the backend packages is unchanged (cloudflare still re-exports
|
|
15
|
+
`buildBundlePatchId` / `parseMetadata` as aliases of the shared helpers).
|
|
16
|
+
|
|
1
17
|
## 0.0.1
|
|
2
18
|
|
|
3
19
|
- Initial release of `flutter_ota_kit_plugin_core` as part of the flutter_ota_kit OTA toolkit.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// flutter_ota_kit_plugin_core example
|
|
2
|
+
//
|
|
3
|
+
// `flutter_ota_kit_plugin_core` provides the shared abstractions that
|
|
4
|
+
// every backend plugin is built on top of: `DatabasePlugin`,
|
|
5
|
+
// `StoragePlugin`, `Bundle` filtering, storage-URI parsing, pagination,
|
|
6
|
+
// and the "blob database" factory used by the AWS, Cloudflare, and
|
|
7
|
+
// PocketBase backends.
|
|
8
|
+
//
|
|
9
|
+
// Most apps don't import this package directly; the SDK's update
|
|
10
|
+
// pipeline does. This file shows the standalone helpers so you can
|
|
11
|
+
// use them while writing a custom backend or a CLI tool.
|
|
12
|
+
//
|
|
13
|
+
// Run: `dart run example/main.dart`
|
|
14
|
+
|
|
15
|
+
import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart';
|
|
16
|
+
|
|
17
|
+
void main() {
|
|
18
|
+
// ── 1. Storage URI parser. ───────────────────────────────────────
|
|
19
|
+
// `parseStorageUri` validates the protocol and returns the bucket +
|
|
20
|
+
// key. Throws on unknown protocols so misuse is caught early.
|
|
21
|
+
const supabaseUri =
|
|
22
|
+
'supabase-storage://flutter-ota-bundles/'
|
|
23
|
+
'01a059a6/patch.zip';
|
|
24
|
+
final parsed = parseStorageUri(supabaseUri, 'supabase-storage');
|
|
25
|
+
print('Parsed protocol: ${parsed.protocol}');
|
|
26
|
+
print('Parsed bucket: ${parsed.bucket}');
|
|
27
|
+
print('Parsed key: ${parsed.key}');
|
|
28
|
+
|
|
29
|
+
// ── 2. Bundle storage keys. ──────────────────────────────────────
|
|
30
|
+
// `createBundleStorageKey` returns a flat key for the patch blob
|
|
31
|
+
// inside a storage bucket. The default layout is `{id}/patch.zip`;
|
|
32
|
+
// for assets it's `{id}/assets/{path}`.
|
|
33
|
+
final patchKey = createBundleStorageKey('01a059a6');
|
|
34
|
+
print('Patch key: $patchKey');
|
|
35
|
+
|
|
36
|
+
final assetKey = createBundleStorageKey('01a059a6', [
|
|
37
|
+
'assets',
|
|
38
|
+
'images',
|
|
39
|
+
'logo.png',
|
|
40
|
+
]);
|
|
41
|
+
print('Asset key: $assetKey');
|
|
42
|
+
|
|
43
|
+
// ── 3. Patch ID helper. ──────────────────────────────────────────
|
|
44
|
+
// The device SDK reports the currently-installed patch id back to
|
|
45
|
+
// the server (so the server can compute diffs). For patches with a
|
|
46
|
+
// base, the id is `{currentId}-h{patchCount}`; for standalone
|
|
47
|
+
// patches, the id is the current id.
|
|
48
|
+
final patchId = buildBundlePatchId(
|
|
49
|
+
'01a059a6-7d3c-4f3e-8a2c-9c2e3a4b5d6e',
|
|
50
|
+
'00000000-0000-0000-0000-000000000000',
|
|
51
|
+
);
|
|
52
|
+
print('Patch id: $patchId');
|
|
53
|
+
|
|
54
|
+
// ── 4. Pagination math. ──────────────────────────────────────────
|
|
55
|
+
// `calculatePagination` returns total/page/hasNext/hasPrev info
|
|
56
|
+
// used by the CLI's `bundle list` command.
|
|
57
|
+
final p = calculatePagination(247, limit: 20, offset: 40);
|
|
58
|
+
print('Page ${p.currentPage} of ${p.totalPages} (${p.total} items)');
|
|
59
|
+
print(' hasNextPage: ${p.hasNextPage}');
|
|
60
|
+
print(' hasPreviousPage: ${p.hasPreviousPage}');
|
|
61
|
+
}
|
|
@@ -2,17 +2,14 @@ library;
|
|
|
2
2
|
|
|
3
3
|
// Re-export core types for convenience.
|
|
4
4
|
export 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart'
|
|
5
|
-
show
|
|
6
|
-
Bundle,
|
|
7
|
-
GetBundlesArgs,
|
|
8
|
-
Platform,
|
|
9
|
-
UpdateInfo,
|
|
10
|
-
semverSatisfies;
|
|
5
|
+
show Bundle, GetBundlesArgs, Platform, UpdateInfo, semverSatisfies;
|
|
11
6
|
|
|
12
7
|
// Plugin-core types.
|
|
13
8
|
export 'src/types.dart';
|
|
14
9
|
|
|
15
10
|
// Helpers.
|
|
11
|
+
export 'src/storage_helpers.dart';
|
|
12
|
+
export 'src/bundle_helpers.dart';
|
|
16
13
|
export 'src/calculate_pagination.dart';
|
|
17
14
|
export 'src/query_bundles.dart';
|
|
18
15
|
export 'src/paginate_bundles.dart';
|
|
@@ -39,4 +36,3 @@ export 'src/generate_min_bundle_id.dart';
|
|
|
39
36
|
export 'src/uuidv7.dart';
|
|
40
37
|
export 'src/get_update_info.dart';
|
|
41
38
|
export 'src/resolve_update_info_from_bundles.dart';
|
|
42
|
-
export 'src/resolve_update_info_from_bundles.dart';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import 'content_addressed_assets.dart' show getContentAddressedAssetStoragePath;
|
|
2
|
-
import 'legacy_asset_storage_layout.dart'
|
|
2
|
+
import 'legacy_asset_storage_layout.dart'
|
|
3
|
+
show getLegacyManifestAssetStoragePath;
|
|
3
4
|
|
|
4
5
|
/// The two supported asset storage layout strategies.
|
|
5
6
|
typedef AssetStorageLayout = String;
|
|
@@ -27,8 +28,7 @@ String createStorageUriWithRelativePath({
|
|
|
27
28
|
required String relativePath,
|
|
28
29
|
}) {
|
|
29
30
|
final storageUrl = Uri.parse(baseStorageUri);
|
|
30
|
-
final normalizedBasePath =
|
|
31
|
-
storageUrl.path.replaceAll(RegExp(r'/+$'), '');
|
|
31
|
+
final normalizedBasePath = storageUrl.path.replaceAll(RegExp(r'/+$'), '');
|
|
32
32
|
final normalizedRelativePath = relativePath
|
|
33
33
|
.replaceAll('\\', '/')
|
|
34
34
|
.split('/')
|
|
@@ -46,8 +46,8 @@ String createStorageUriWithRelativePath({
|
|
|
46
46
|
/// Returns `'content-addressed'` if the URI path ends with `/assets`,
|
|
47
47
|
/// otherwise `'legacy-files'`.
|
|
48
48
|
AssetStorageLayout getAssetStorageLayout(String assetBaseStorageUri) {
|
|
49
|
-
final pathname =
|
|
50
|
-
|
|
49
|
+
final pathname = Uri.parse(assetBaseStorageUri).path
|
|
50
|
+
.replaceAll(RegExp(r'/+$'), '');
|
|
51
51
|
return pathname.endsWith('/assets') || pathname == '/assets'
|
|
52
52
|
? assetLayoutContentAddressed
|
|
53
53
|
: assetLayoutLegacyFiles;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import 'dart:convert' show jsonDecode;
|
|
2
|
+
|
|
3
|
+
/// Normalize bundle `metadata` from various shapes (a JSON string or an
|
|
4
|
+
/// already-decoded object) to a plain map. Identical across every backend
|
|
5
|
+
/// mapper (supabase / postgres / cloudflare), so it lives here once.
|
|
6
|
+
Map<String, Object?>? normalizeMetadata(Object? value) {
|
|
7
|
+
if (value == null) return null;
|
|
8
|
+
if (value is String) {
|
|
9
|
+
try {
|
|
10
|
+
final parsed = jsonDecode(value);
|
|
11
|
+
return normalizeMetadata(parsed);
|
|
12
|
+
} catch (_) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (value is Map) return value.cast<String, Object?>();
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Compound primary key for a patch row: `${bundleId}:${baseBundleId}`.
|
|
21
|
+
/// Shared by every backend mapper.
|
|
22
|
+
String buildBundlePatchId(String bundleId, String baseBundleId) =>
|
|
23
|
+
'$bundleId:$baseBundleId';
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
/// The top-level prefix for bundle storage keys.
|
|
2
2
|
const String bundleStoragePrefix = 'bundles';
|
|
3
3
|
|
|
4
|
-
/// Build a storage key for a bundle, e.g. `bundles
|
|
5
|
-
String createBundleStorageKey(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
/// Build a storage key for a bundle, e.g. `bundles/{bundleId}/patch.zip`.
|
|
5
|
+
String createBundleStorageKey(
|
|
6
|
+
String bundleId, [
|
|
7
|
+
List<String> relativePaths = const [],
|
|
8
|
+
]) {
|
|
9
|
+
return [
|
|
10
|
+
bundleStoragePrefix,
|
|
11
|
+
bundleId,
|
|
12
|
+
...relativePaths,
|
|
13
|
+
].where((s) => s.isNotEmpty).join('/');
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
/// Replace the bundle-id segment in a storage URI with a relative path.
|
|
12
17
|
///
|
|
13
|
-
/// For example, given a storage URI containing `.../bundles
|
|
18
|
+
/// For example, given a storage URI containing `.../bundles/{bundleId}/patch.zip`
|
|
14
19
|
/// and a [relativePath] of `files/manifest.json`, this returns a URI pointing
|
|
15
20
|
/// to `.../files/manifest.json`.
|
|
16
21
|
String createStorageRootUriWithPath(
|
|
@@ -19,12 +24,13 @@ String createStorageRootUriWithPath(
|
|
|
19
24
|
String relativePath,
|
|
20
25
|
) {
|
|
21
26
|
final storageUrl = Uri.parse(storageUri);
|
|
22
|
-
final segments = storageUrl.path
|
|
27
|
+
final segments = storageUrl.path
|
|
28
|
+
.split('/')
|
|
29
|
+
.where((s) => s.isNotEmpty)
|
|
30
|
+
.toList();
|
|
23
31
|
final bundleIndex = segments.lastIndexOf(bundleId);
|
|
24
32
|
if (bundleIndex < 0) {
|
|
25
|
-
throw StateError(
|
|
26
|
-
'Storage URI does not contain bundle id: $bundleId',
|
|
27
|
-
);
|
|
33
|
+
throw StateError('Storage URI does not contain bundle id: $bundleId');
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
final rootSegments = segments.sublist(0, bundleIndex);
|
|
@@ -105,9 +105,7 @@ class BundleUnitOfWork {
|
|
|
105
105
|
return const TrackedBundleNotFound();
|
|
106
106
|
}
|
|
107
107
|
final entry = _entries[bundleId];
|
|
108
|
-
return TrackedBundleFound(
|
|
109
|
-
entry is _PresentEntry ? entry.bundle : null,
|
|
110
|
-
);
|
|
108
|
+
return TrackedBundleFound(entry is _PresentEntry ? entry.bundle : null);
|
|
111
109
|
}
|
|
112
110
|
|
|
113
111
|
/// Deduped async lookup: returns cached entry, coalesces concurrent loads,
|
|
@@ -124,19 +122,23 @@ class BundleUnitOfWork {
|
|
|
124
122
|
final pending = _pendingLoads[bundleId];
|
|
125
123
|
if (pending != null) return pending;
|
|
126
124
|
|
|
127
|
-
final load = loadBundleById().then<Bundle?>(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
125
|
+
final load = loadBundleById().then<Bundle?>(
|
|
126
|
+
(bundle) {
|
|
127
|
+
_pendingLoads.remove(bundleId);
|
|
128
|
+
final currentEntry = _entries[bundleId];
|
|
129
|
+
if (currentEntry != null) {
|
|
130
|
+
return currentEntry is _PresentEntry ? currentEntry.bundle : null;
|
|
131
|
+
}
|
|
132
|
+
_entries[bundleId] = bundle != null
|
|
133
|
+
? _PresentEntry(bundle)
|
|
134
|
+
: const _MissingEntry();
|
|
135
|
+
return bundle;
|
|
136
|
+
},
|
|
137
|
+
onError: (Object error) {
|
|
138
|
+
_pendingLoads.remove(bundleId);
|
|
139
|
+
throw error;
|
|
140
|
+
},
|
|
141
|
+
);
|
|
140
142
|
_pendingLoads[bundleId] = load;
|
|
141
143
|
return load;
|
|
142
144
|
}
|
|
@@ -177,9 +179,7 @@ class BundleUnitOfWork {
|
|
|
177
179
|
}
|
|
178
180
|
}
|
|
179
181
|
|
|
180
|
-
return sortBundles(dataById.values.toList(), orderBy)
|
|
181
|
-
.take(limit)
|
|
182
|
-
.toList();
|
|
182
|
+
return sortBundles(dataById.values.toList(), orderBy).take(limit).toList();
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
void markInsert(Bundle bundle) {
|
|
@@ -199,7 +199,8 @@ class BundleUnitOfWork {
|
|
|
199
199
|
_changes[bundle.id] = _TrackedBundleChange(
|
|
200
200
|
operation: op,
|
|
201
201
|
data: bundle,
|
|
202
|
-
before:
|
|
202
|
+
before:
|
|
203
|
+
prev?.before ??
|
|
203
204
|
(prevEntry is _PresentEntry ? prevEntry.bundle : null),
|
|
204
205
|
);
|
|
205
206
|
}
|
|
@@ -211,7 +212,8 @@ class BundleUnitOfWork {
|
|
|
211
212
|
_changes[bundle.id] = _TrackedBundleChange(
|
|
212
213
|
operation: BundleChangeOperation.delete,
|
|
213
214
|
data: bundle,
|
|
214
|
-
before:
|
|
215
|
+
before:
|
|
216
|
+
prev?.before ??
|
|
215
217
|
(prevEntry is _PresentEntry ? prevEntry.bundle : bundle),
|
|
216
218
|
);
|
|
217
219
|
}
|
|
@@ -226,9 +228,11 @@ class BundleUnitOfWork {
|
|
|
226
228
|
|
|
227
229
|
int listFetchExtraCount() {
|
|
228
230
|
return _changes.values
|
|
229
|
-
.where(
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
.where(
|
|
232
|
+
(c) =>
|
|
233
|
+
c.operation == BundleChangeOperation.update ||
|
|
234
|
+
c.operation == BundleChangeOperation.delete,
|
|
235
|
+
)
|
|
232
236
|
.length;
|
|
233
237
|
}
|
|
234
238
|
|
|
@@ -237,10 +241,11 @@ class BundleUnitOfWork {
|
|
|
237
241
|
for (final change in _changes.values) {
|
|
238
242
|
if (change.operation == BundleChangeOperation.insert) continue;
|
|
239
243
|
final matchedBefore =
|
|
240
|
-
change.before != null &&
|
|
244
|
+
change.before != null &&
|
|
245
|
+
bundleMatchesQueryWhere(change.before!, where);
|
|
241
246
|
final matchesAfter =
|
|
242
247
|
change.operation == BundleChangeOperation.update &&
|
|
243
|
-
|
|
248
|
+
bundleMatchesQueryWhere(change.data, where);
|
|
244
249
|
if (matchedBefore && !matchesAfter) {
|
|
245
250
|
total -= 1;
|
|
246
251
|
} else if (!matchedBefore && matchesAfter) {
|