From a627abc813ed92332373588c31fab49bc8fcbbc7 Mon Sep 17 00:00:00 2001 From: Rody Davis Date: Wed, 10 Jun 2026 21:35:21 -0700 Subject: [PATCH] chore(web_file_system): sync changes from open_xml including idb_block_store, getAsBlob, and tests --- .../web_file_system/example/.gitignore | 50 + .../web_file_system/example/lib/main.dart | 75 +- .../web_file_system/example/pubspec.lock | 260 +++++ .../lib/src/backend/idb_block_store.dart | 123 +++ .../lib/src/backend/idb_inode_service.dart | 147 +-- .../lib/src/backend/opfs_block_store.dart | 31 +- .../lib/src/backend/sync_rpc_helper.dart | 234 ----- .../lib/src/entities/web_directory.dart | 267 ++--- .../lib/src/entities/web_file.dart | 301 +++--- .../lib/src/entities/web_link.dart | 147 +-- .../lib/src/web_file_system.dart | 122 +-- .../web_file_system/lib/web_file_system.dart | 2 +- experimental/web_file_system/pubspec.lock | 5 +- experimental/web_file_system/pubspec.yaml | 8 +- .../test/cleanup_troubleshoot_test.dart | 94 ++ .../test/robust_correctness_test.dart | 113 ++ .../test/web_file_system_test.dart | 981 +----------------- 17 files changed, 1037 insertions(+), 1923 deletions(-) create mode 100644 experimental/web_file_system/example/.gitignore create mode 100644 experimental/web_file_system/example/pubspec.lock create mode 100644 experimental/web_file_system/lib/src/backend/idb_block_store.dart delete mode 100644 experimental/web_file_system/lib/src/backend/sync_rpc_helper.dart create mode 100644 experimental/web_file_system/test/cleanup_troubleshoot_test.dart create mode 100644 experimental/web_file_system/test/robust_correctness_test.dart diff --git a/experimental/web_file_system/example/.gitignore b/experimental/web_file_system/example/.gitignore new file mode 100644 index 0000000..ca109dd --- /dev/null +++ b/experimental/web_file_system/example/.gitignore @@ -0,0 +1,50 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/dist/ +/coverage/ +/scripts/temp_bin/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Xcode data +/macos/DerivedData/ \ No newline at end of file diff --git a/experimental/web_file_system/example/lib/main.dart b/experimental/web_file_system/example/lib/main.dart index 1e0a6ba..c3045dc 100644 --- a/experimental/web_file_system/example/lib/main.dart +++ b/experimental/web_file_system/example/lib/main.dart @@ -153,18 +153,23 @@ class _FileSystemDemoState extends State { spacing: 8, children: [ ElevatedButton( - onPressed: _createFile, child: const Text('New File')), + onPressed: _createFile, + child: const Text('New File'), + ), ElevatedButton( - onPressed: _createDir, child: const Text('New Directory')), + onPressed: _createDir, + child: const Text('New Directory'), + ), if (_currentPath != '/') ElevatedButton( - onPressed: () { - setState(() { - _currentPath = _fs.path.dirname(_currentPath); - }); - _refreshFiles(); - }, - child: const Text('Go Up')), + onPressed: () { + setState(() { + _currentPath = _fs.path.dirname(_currentPath); + }); + _refreshFiles(); + }, + child: const Text('Go Up'), + ), ], ), const Divider(), @@ -181,13 +186,14 @@ class _FileSystemDemoState extends State { ), title: Text(entity.basename), subtitle: FutureBuilder( - future: _getSize(entity), - builder: (context, snapshot) { - if (snapshot.hasData) { - return Text(_formatBytes(snapshot.data!)); - } - return const Text('Loading...'); - }), + future: _getSize(entity), + builder: (context, snapshot) { + if (snapshot.hasData) { + return Text(_formatBytes(snapshot.data!)); + } + return const Text('Loading...'); + }, + ), trailing: IconButton( icon: const Icon(Icons.delete, color: Colors.red), onPressed: () => _delete(entity), @@ -201,17 +207,20 @@ class _FileSystemDemoState extends State { } else if (entity is File) { entity.readAsString().then((content) { showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(entity.basename), - content: SingleChildScrollView( - child: Text(content)), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Close')) - ], - )); + context: context, + builder: (context) => AlertDialog( + title: Text(entity.basename), + content: SingleChildScrollView( + child: Text(content), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ); }); } }, @@ -229,11 +238,13 @@ class _FileSystemDemoState extends State { itemCount: _logs.length, itemBuilder: (context, index) => Padding( padding: const EdgeInsets.all(4.0), - child: Text(_logs[_logs.length - 1 - index], - style: const TextStyle( - fontSize: 12, - fontFamily: 'monospace', - )), + child: Text( + _logs[_logs.length - 1 - index], + style: const TextStyle( + fontSize: 12, + fontFamily: 'monospace', + ), + ), ), ), ), diff --git a/experimental/web_file_system/example/pubspec.lock b/experimental/web_file_system/example/pubspec.lock new file mode 100644 index 0000000..2a917db --- /dev/null +++ b/experimental/web_file_system/example/pubspec.lock @@ -0,0 +1,260 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: "direct dev" + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd" + url: "https://pub.dev" + source: hosted + version: "0.12.20" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d + url: "https://pub.dev" + source: hosted + version: "1.18.3" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11" + url: "https://pub.dev" + source: hosted + version: "0.7.12" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "1d774bbdf6b72a0b12122fc1560c9c2d2a67db5a4a4cc2bd8a5c990ab20e3188" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_file_system: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" +sdks: + dart: ">=3.10.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/experimental/web_file_system/lib/src/backend/idb_block_store.dart b/experimental/web_file_system/lib/src/backend/idb_block_store.dart new file mode 100644 index 0000000..51abcfb --- /dev/null +++ b/experimental/web_file_system/lib/src/backend/idb_block_store.dart @@ -0,0 +1,123 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:typed_data'; +import 'package:web/web.dart' as web; +import 'package:uuid/uuid.dart'; + +class IdbBlockStore { + static const String _dbName = 'WebFileSystemBlobs'; + static const int _version = 1; + static const String _storeName = 'blobs'; + + web.IDBDatabase? _db; + final Completer _initCompleter = Completer(); + final Uuid _uuid = Uuid(); + + Future _ensureReady() async { + if (_db != null) return; + if (_initCompleter.isCompleted) return _initCompleter.future; + + final request = web.window.indexedDB.open(_dbName, _version); + + request.onupgradeneeded = (web.IDBVersionChangeEvent event) { + final db = + (event.target as web.IDBOpenDBRequest).result as web.IDBDatabase; + if (!db.objectStoreNames.contains(_storeName)) { + db.createObjectStore(_storeName); + } + }.toJS; + + final completer = Completer(); + + request.onsuccess = (web.Event event) { + _db = (event.target as web.IDBOpenDBRequest).result as web.IDBDatabase; + if (!_initCompleter.isCompleted) completer.complete(); + }.toJS; + + request.onerror = (web.Event event) { + if (!_initCompleter.isCompleted) { + completer.completeError(Exception('Failed to open Blob IDB')); + } + }.toJS; + + return completer.future; + } + + Future writeBlob(Stream> stream) async { + await _ensureReady(); + final blockId = _uuid.v4(); + + // Read all bytes into memory to store in IDB (IDB requires complete blob/buffer usually) + // For streams, we must materialize them. + final chunks = await stream.toList(); + final allBytes = chunks.expand((x) => x).toList(); + final uint8Array = Uint8List.fromList(allBytes).toJS; + + final transaction = _db!.transaction( + _storeName.toJS, + 'readwrite', + ); + final store = transaction.objectStore(_storeName); + + // Using put(value, key) since we didn't specify keyPath/autoIncrement + final request = store.put(uint8Array, blockId.toJS); + await _requestToFuture(request); + + return blockId; + } + + Stream> readBlob(String blockId) async* { + await _ensureReady(); + final transaction = _db!.transaction( + _storeName.toJS, + 'readonly', + ); + final store = transaction.objectStore(_storeName); + final request = store.get(blockId.toJS); + + final result = await _requestToFuture(request); + if (result == null) throw Exception('Blob $blockId not found'); + + // result is JSUint8Array or ArrayBuffer + final uint8Array = result as JSUint8Array; + yield uint8Array.toDart; + } + + Future deleteBlob(String blockId) async { + await _ensureReady(); + final transaction = _db!.transaction( + _storeName.toJS, + 'readwrite', + ); + final store = transaction.objectStore(_storeName); + final request = store.delete(blockId.toJS); + await _requestToFuture(request); + } + + Future getBlob(String blockId) async { + await _ensureReady(); + final transaction = _db!.transaction( + _storeName.toJS, + 'readonly', + ); + final store = transaction.objectStore(_storeName); + final request = store.get(blockId.toJS); + + final result = await _requestToFuture(request); + if (result == null) throw Exception('Blob $blockId not found'); + + final uint8Array = result as JSUint8Array; + return web.Blob([uint8Array].toJS); + } + + Future _requestToFuture(web.IDBRequest request) { + final completer = Completer(); + request.onsuccess = (web.Event e) { + completer.complete((e.target as web.IDBRequest).result); + }.toJS; + request.onerror = (web.Event e) { + completer.completeError(Exception('IDB Blob Error')); + }.toJS; + return completer.future; + } +} diff --git a/experimental/web_file_system/lib/src/backend/idb_inode_service.dart b/experimental/web_file_system/lib/src/backend/idb_inode_service.dart index 7770020..d63240a 100644 --- a/experimental/web_file_system/lib/src/backend/idb_inode_service.dart +++ b/experimental/web_file_system/lib/src/backend/idb_inode_service.dart @@ -1,29 +1,9 @@ import 'dart:async'; import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; import 'package:web/web.dart' as web; extension type InodeJS._(JSObject _) implements JSObject { - external String get id; - external set id(String value); - - external String get parentId; - external set parentId(String value); - - external String get name; - external set name(String value); - - external int get nodeType; - external set nodeType(int value); - - external String? get blobId; - external set blobId(String? value); - - external int get size; - external set size(int value); - - external int get modified; - external set modified(int value); - factory InodeJS({ required String id, required String parentId, @@ -32,16 +12,18 @@ extension type InodeJS._(JSObject _) implements JSObject { String? blobId, int size = 0, required int modified, + int storageType = 0, }) { - final obj = JSObject() as InodeJS; - obj.id = id; - obj.parentId = parentId; - obj.name = name; - obj.nodeType = nodeType; - obj.blobId = blobId; - obj.size = size; - obj.modified = modified; - return obj; + final obj = JSObject(); + obj.setProperty('id'.toJS, id.toJS); + obj.setProperty('parentId'.toJS, parentId.toJS); + obj.setProperty('name'.toJS, name.toJS); + obj.setProperty('nodeType'.toJS, nodeType.toJS); + if (blobId != null) obj.setProperty('blobId'.toJS, blobId.toJS); + obj.setProperty('size'.toJS, size.toJS); + obj.setProperty('modified'.toJS, modified.toJS); + obj.setProperty('storageType'.toJS, storageType.toJS); + return obj as InodeJS; } } @@ -53,6 +35,7 @@ class Inode { final String? blobId; final int size; final int modified; + final int storageType; // 0: OPFS, 1: IDB Inode({ required this.id, @@ -62,6 +45,7 @@ class Inode { this.blobId, this.size = 0, required this.modified, + this.storageType = 0, }); InodeJS toJS() { @@ -73,18 +57,30 @@ class Inode { blobId: blobId, size: size, modified: modified, + storageType: storageType, ); } static Inode fromJS(InodeJS js) { + final obj = js as JSObject; + final id = obj.getProperty('id'.toJS); + final parentId = obj.getProperty('parentId'.toJS); + final name = obj.getProperty('name'.toJS); + final nodeType = obj.getProperty('nodeType'.toJS); + final blobId = obj.getProperty('blobId'.toJS); + final size = obj.getProperty('size'.toJS); + final modified = obj.getProperty('modified'.toJS); + final storageType = obj.getProperty('storageType'.toJS); + return Inode( - id: js.id, - parentId: js.parentId, - name: js.name, - nodeType: js.nodeType, - blobId: js.blobId, - size: js.size, - modified: js.modified, + id: id != null && !id.isUndefinedOrNull ? (id as JSString).toDart : '', + parentId: parentId != null && !parentId.isUndefinedOrNull ? (parentId as JSString).toDart : '', + name: name != null && !name.isUndefinedOrNull ? (name as JSString).toDart : '', + nodeType: nodeType != null && !nodeType.isUndefinedOrNull ? (nodeType as JSNumber).toDartInt : 0, + blobId: blobId != null && !blobId.isUndefinedOrNull ? (blobId as JSString).toDart : null, + size: size != null && !size.isUndefinedOrNull ? (size as JSNumber).toDartInt : 0, + modified: modified != null && !modified.isUndefinedOrNull ? (modified as JSNumber).toDartInt : 0, + storageType: storageType != null && !storageType.isUndefinedOrNull ? (storageType as JSNumber).toDartInt : 0, ); } } @@ -95,16 +91,13 @@ class IdbInodeService { static const String _storeName = 'inodes'; web.IDBDatabase? _db; - Future? _initFuture; + final Completer _initCompleter = Completer(); static const String rootId = '00000000-0000-0000-0000-000000000000'; - Future _ensureReady() { - if (_db != null) return Future.value(); - return _initFuture ??= _init(); - } + Future _ensureReady() async { + if (_db != null) return; + if (_initCompleter.isCompleted) return _initCompleter.future; - Future _init() async { - final completer = Completer(); final request = web.window.indexedDB.open(_dbName, _version); request.onupgradeneeded = (web.IDBVersionChangeEvent event) { @@ -116,23 +109,35 @@ class IdbInodeService { web.IDBObjectStoreParameters(keyPath: 'id'.toJS), ); store.createIndex( - 'parentId', 'parentId'.toJS, web.IDBIndexParameters(unique: false)); - store.createIndex('parent_name', ['parentId'.toJS, 'name'.toJS].toJS, - web.IDBIndexParameters(unique: true)); + 'parentId', + 'parentId'.toJS, + web.IDBIndexParameters(unique: false), + ); + store.createIndex( + 'parent_name', + ['parentId'.toJS, 'name'.toJS].toJS, + web.IDBIndexParameters(unique: true), + ); } }.toJS; + final completer = Completer(); + request.onsuccess = (web.Event event) { _db = (event.target as web.IDBOpenDBRequest).result as web.IDBDatabase; - _ensureRootExists().then((_) { - completer.complete(); - }).catchError((e) { - completer.completeError(e); - }); + _ensureRootExists() + .then((_) { + if (!_initCompleter.isCompleted) completer.complete(); + }) + .catchError((e) { + if (!_initCompleter.isCompleted) completer.completeError(e); + }); }.toJS; request.onerror = (web.Event event) { - completer.completeError(Exception('Failed to open IDB')); + if (!_initCompleter.isCompleted) { + completer.completeError(Exception('Failed to open IDB')); + } }.toJS; return completer.future; @@ -142,20 +147,24 @@ class IdbInodeService { try { await getInode(rootId); } catch (_) { - await createInode(Inode( - id: rootId, - parentId: 'null', - name: '', - nodeType: 1, - modified: DateTime.now().millisecondsSinceEpoch, - )); + await createInode( + Inode( + id: rootId, + parentId: 'null', + name: '', + nodeType: 1, + modified: DateTime.now().millisecondsSinceEpoch, + ), + ); } } Future createInode(Inode inode) async { if (_db == null) await _ensureReady(); final transaction = _db!.transaction( - _storeName.toJS, 'readwrite'.toJS as web.IDBTransactionMode); + _storeName.toJS, + 'readwrite', + ); final store = transaction.objectStore(_storeName); final request = store.put(inode.toJS()); await _requestToFuture(request); @@ -168,7 +177,9 @@ class IdbInodeService { Future deleteInode(String id) async { await _ensureReady(); final transaction = _db!.transaction( - _storeName.toJS, 'readwrite'.toJS as web.IDBTransactionMode); + _storeName.toJS, + 'readwrite', + ); final store = transaction.objectStore(_storeName); final request = store.delete(id.toJS); await _requestToFuture(request); @@ -178,7 +189,9 @@ class IdbInodeService { if (_db == null) await _ensureReady(); final transaction = _db!.transaction( - _storeName.toJS, 'readonly'.toJS as web.IDBTransactionMode); + _storeName.toJS, + 'readonly', + ); final store = transaction.objectStore(_storeName); final request = store.get(id.toJS); final result = await _requestToFuture(request); @@ -190,7 +203,9 @@ class IdbInodeService { Future getChild(String parentId, String name) async { await _ensureReady(); final transaction = _db!.transaction( - _storeName.toJS, 'readonly'.toJS as web.IDBTransactionMode); + _storeName.toJS, + 'readonly', + ); final store = transaction.objectStore(_storeName); final index = store.index('parent_name'); final key = JSArray(); @@ -211,7 +226,9 @@ class IdbInodeService { Future> listChildren(String parentId) async { await _ensureReady(); final transaction = _db!.transaction( - _storeName.toJS, 'readonly'.toJS as web.IDBTransactionMode); + _storeName.toJS, + 'readonly', + ); final store = transaction.objectStore(_storeName); final index = store.index('parentId'); final request = index.getAll(parentId.toJS); diff --git a/experimental/web_file_system/lib/src/backend/opfs_block_store.dart b/experimental/web_file_system/lib/src/backend/opfs_block_store.dart index a6e7209..ef31d42 100644 --- a/experimental/web_file_system/lib/src/backend/opfs_block_store.dart +++ b/experimental/web_file_system/lib/src/backend/opfs_block_store.dart @@ -12,10 +12,7 @@ class OpfsBlockStore { Future _ensureReady() async { if (_blocksDir != null) return; - final web.StorageManager? storage = web.window.navigator.storage; - if (storage == null) { - throw UnsupportedError('StorageManager not supported'); - } + final web.StorageManager storage = web.window.navigator.storage; final root = await storage.getDirectory().toDart; _blocksDir = await root @@ -26,24 +23,19 @@ class OpfsBlockStore { .toDart; } - Future<(String, int)> writeBlob(Stream> stream) async { + Future writeBlob(Stream> stream) async { await _ensureReady(); final blockId = _uuid.v4(); final fileHandle = await _blocksDir! - .getFileHandle( - blockId, - web.FileSystemGetFileOptions(create: true), - ) + .getFileHandle(blockId, web.FileSystemGetFileOptions(create: true)) .toDart; final writable = await fileHandle.createWritable().toDart; - int totalBytes = 0; try { await for (final chunk in stream) { final uint8 = Uint8List.fromList(chunk); - totalBytes += uint8.length; await writable.write(uint8.toJS).toDart; } await writable.close().toDart; @@ -52,20 +44,17 @@ class OpfsBlockStore { await writable.abort().toDart; await _blocksDir!.removeEntry(blockId).toDart; } catch (_) {} + print('OpfsBlockStore: writeBlob failed for $blockId: $e'); rethrow; } - return (blockId, totalBytes); + return blockId; } Stream> readBlob(String blockId) async* { await _ensureReady(); try { - final fileHandle = await _blocksDir! - .getFileHandle( - blockId, - ) - .toDart; + final fileHandle = await _blocksDir!.getFileHandle(blockId).toDart; final file = await fileHandle.getFile().toDart; final web.Blob blob = file; @@ -80,6 +69,7 @@ class OpfsBlockStore { yield chunk.toDart; } } catch (e) { + print('OpfsBlockStore: readBlob failed for $blockId: $e'); rethrow; } } @@ -92,4 +82,11 @@ class OpfsBlockStore { // Ignore if not found } } + + Future getBlob(String blockId) async { + await _ensureReady(); + final fileHandle = await _blocksDir!.getFileHandle(blockId).toDart; + final file = await fileHandle.getFile().toDart; + return file; + } } diff --git a/experimental/web_file_system/lib/src/backend/sync_rpc_helper.dart b/experimental/web_file_system/lib/src/backend/sync_rpc_helper.dart deleted file mode 100644 index 86e605a..0000000 --- a/experimental/web_file_system/lib/src/backend/sync_rpc_helper.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'dart:js_interop'; -import 'dart:typed_data'; - -@JS('eval') -external JSAny? _jsEval(String code); - -@JS('globalThis.registerVFSWorkerProxy') -external void _registerVFSWorkerProxy(JSAny worker, JSAny asyncFS); - -@JS('globalThis.initVFSSyncWorker') -external void _initVFSSyncWorker(); - -@JS('globalThis.sendVFSSyncRequest') -external JSUint8Array _sendVFSSyncRequest(int cmd, JSUint8Array requestBytes); - -@JS('globalThis.isVFSSyncWorkerInitialized') -external bool? get _isVFSSyncWorkerInitialized; - -@JS('globalThis.SharedArrayBuffer') -external JSAny? get _sharedArrayBufferClass; - -class SyncRpcHelper { - static bool get isSharedArrayBufferSupported { - try { - return _sharedArrayBufferClass != null; - } catch (_) { - return false; - } - } - - static bool get isWorker { - // In a worker, globalThis.document is undefined and globalThis.importScripts is defined - try { - final isWorkerResult = _jsEval("typeof importScripts !== 'undefined'"); - return (isWorkerResult as JSBoolean).toDart; - } catch (_) { - return false; - } - } - - static void injectHelperScripts() { - _jsEval(''' - if (typeof globalThis.registerVFSWorkerProxy === 'undefined') { - globalThis.registerVFSWorkerProxy = function(worker, asyncFS) { - let sab; - let statusArray; - let payloadArray; - - worker.addEventListener('message', async function(e) { - if (!e.data) return; - if (e.data.type === 'INIT_SYNC_VFS') { - sab = e.data.buffer; - statusArray = new Int32Array(sab); - payloadArray = new Uint8Array(sab); - return; - } - if (e.data.type === 'SYNC_REQ') { - const cmd = statusArray[1]; - const reqLen = statusArray[2]; - const decoder = new TextDecoder(); - const reqBytes = payloadArray.subarray(64, 64 + reqLen); - - try { - let result; - if (cmd === 1) { // exists - const path = decoder.decode(reqBytes); - const type = await asyncFS.type(path); - result = type.toString() !== 'FileSystemEntityType.notFound'; - payloadArray[64] = result ? 1 : 0; - statusArray[3] = 1; - } else if (cmd === 2) { // type - const followLinks = reqBytes[0] === 1; - const path = decoder.decode(reqBytes.subarray(1)); - const type = await asyncFS.type(path, { followLinks }); - const typeStr = type.toString().split('.').pop(); - const typeBytes = new TextEncoder().encode(typeStr); - payloadArray.set(typeBytes, 64); - statusArray[3] = typeBytes.length; - } else if (cmd === 3) { // readBytes - const path = decoder.decode(reqBytes); - const bytes = await asyncFS.file(path).readAsBytes(); - payloadArray.set(bytes, 64); - statusArray[3] = bytes.length; - } else if (cmd === 4) { // writeBytes - const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength); - const pathLen = view.getUint32(0, true); - const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen)); - const content = reqBytes.subarray(4 + pathLen); - await asyncFS.file(path).writeAsBytes(content); - statusArray[3] = 0; - } else if (cmd === 5) { // createDir - const path = decoder.decode(reqBytes); - await asyncFS.directory(path).create(recursive: true); - statusArray[3] = 0; - } else if (cmd === 6) { // delete - const path = decoder.decode(reqBytes); - await asyncFS.file(path).delete(recursive: true); - statusArray[3] = 0; - } else if (cmd === 7) { // createLink - const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength); - const pathLen = view.getUint32(0, true); - const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen)); - const target = decoder.decode(reqBytes.subarray(4 + pathLen)); - await asyncFS.link(path).create(target); - statusArray[3] = 0; - } else if (cmd === 8) { // readLink - const path = decoder.decode(reqBytes); - const target = await asyncFS.link(path).target(); - const targetBytes = new TextEncoder().encode(target); - payloadArray.set(targetBytes, 64); - statusArray[3] = targetBytes.length; - } else if (cmd === 9) { // stat - const path = decoder.decode(reqBytes); - const stat = await asyncFS.stat(path); - const statData = { - type: stat.type.toString().split('.').pop(), - size: stat.size, - modified: stat.modified.millisecondsSinceEpoch - }; - const statBytes = new TextEncoder().encode(JSON.stringify(statData)); - payloadArray.set(statBytes, 64); - statusArray[3] = statBytes.length; - } else if (cmd === 10) { // list - const path = decoder.decode(reqBytes); - const list = await asyncFS.directory(path).list(recursive: false, followLinks: false).toList(); - const entities = list.map(e => ({ - path: e.path, - type: e.runtimeType.toString().toLowerCase().replace('impl', '').replace('web', '') - })); - const listBytes = new TextEncoder().encode(JSON.stringify(entities)); - payloadArray.set(listBytes, 64); - statusArray[3] = listBytes.length; - } else if (cmd === 11) { // resolveSymbolicLinks - const path = decoder.decode(reqBytes); - const resolved = await asyncFS.resolveSymbolicLinks(path); - const resolvedBytes = new TextEncoder().encode(resolved); - payloadArray.set(resolvedBytes, 64); - statusArray[3] = resolvedBytes.length; - } else if (cmd === 12) { // rename - const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength); - const pathLen = view.getUint32(0, true); - const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen)); - const newPath = decoder.decode(reqBytes.subarray(4 + pathLen)); - - const inode = await asyncFS.resolvepath(path); - const newParentDir = asyncFS.path.dirname(newPath); - const newName = asyncFS.path.basename(newPath); - const newParentInode = await asyncFS.resolvepath(newParentDir); - - inode.parentId = newParentInode.id; - inode.name = newName; - inode.modified = Date.now(); - await asyncFS.idb.updateInode(inode); - statusArray[3] = 0; - } else if (cmd === 13) { // updateLink - const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength); - const pathLen = view.getUint32(0, true); - const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen)); - const target = decoder.decode(reqBytes.subarray(4 + pathLen)); - await asyncFS.link(path).update(target); - statusArray[3] = 0; - } - - Atomics.store(statusArray, 0, 2); // completed - } catch (err) { - console.error("VFS Proxy Error:", err); - const errBytes = new TextEncoder().encode(err.toString()); - payloadArray.set(errBytes, 64); - statusArray[3] = errBytes.length; - Atomics.store(statusArray, 0, 3); // error - } - Atomics.notify(statusArray, 0); - } - }); - }; - } - - if (typeof globalThis.initVFSSyncWorker === 'undefined') { - globalThis.isVFSSyncWorkerInitialized = false; - globalThis.initVFSSyncWorker = function() { - if (globalThis.isVFSSyncWorkerInitialized) return; - const sab = new SharedArrayBuffer(1024 * 1024 * 10); // 10MB - const statusArray = new Int32Array(sab); - const payloadArray = new Uint8Array(sab); - - globalThis.postMessage({ type: 'INIT_SYNC_VFS', buffer: sab }); - - globalThis.sendVFSSyncRequest = function(cmd, requestBytes) { - while (Atomics.load(statusArray, 0) !== 0) { - // Idle wait - } - statusArray[1] = cmd; - statusArray[2] = requestBytes.length; - payloadArray.set(requestBytes, 64); - - Atomics.store(statusArray, 0, 1); - globalThis.postMessage({ type: 'SYNC_REQ' }); - - Atomics.wait(statusArray, 0, 1); - - const status = Atomics.load(statusArray, 0); - const respLen = statusArray[3]; - const respBytes = payloadArray.slice(64, 64 + respLen); - - Atomics.store(statusArray, 0, 0); - - if (status === 3) { - throw new Error(new TextDecoder().decode(respBytes)); - } - return respBytes; - }; - globalThis.isVFSSyncWorkerInitialized = true; - }; - } - '''); - } - - static void registerWorkerProxy(JSAny worker, JSAny asyncFS) { - injectHelperScripts(); - _registerVFSWorkerProxy(worker, asyncFS); - } - - static void initSyncWorker() { - injectHelperScripts(); - _initVFSSyncWorker(); - } - - static Uint8List sendSyncRequest(int cmd, Uint8List requestBytes) { - if (_isVFSSyncWorkerInitialized != true) { - initSyncWorker(); - } - return _sendVFSSyncRequest(cmd, requestBytes.toJS).toDart; - } -} diff --git a/experimental/web_file_system/lib/src/entities/web_directory.dart b/experimental/web_file_system/lib/src/entities/web_directory.dart index 35a26b3..58fbf34 100644 --- a/experimental/web_file_system/lib/src/entities/web_directory.dart +++ b/experimental/web_file_system/lib/src/entities/web_directory.dart @@ -1,11 +1,8 @@ import 'dart:async'; -import 'dart:convert'; -import 'dart:typed_data'; import 'package:file/file.dart'; -import 'package:web_file_system/src/backend/idb_inode_service.dart'; +import '../backend/idb_inode_service.dart'; import '../web_file_system.dart'; import 'web_file.dart'; -import 'web_link.dart'; class WebDirectory extends FileSystemEntity implements Directory { final WebFileSystem _fs; @@ -36,13 +33,19 @@ class WebDirectory extends FileSystemEntity implements Directory { // We assume parent must exist if not recursive. final parentInode = await _fs.resolvepath(parentPath); // throws if missing - await _fs.idb.createInode(Inode( - id: _fs.uuid.v4(), - parentId: parentInode.id, - name: name, - nodeType: 1, // Directory - modified: DateTime.now().millisecondsSinceEpoch, - )); + try { + await _fs.idb.createInode( + Inode( + id: _fs.uuid.v4(), + parentId: parentInode.id, + name: name, + nodeType: 1, // Directory + modified: DateTime.now().millisecondsSinceEpoch, + ), + ); + } catch (_) { + if (!await exists()) rethrow; + } return this; } @@ -53,37 +56,24 @@ class WebDirectory extends FileSystemEntity implements Directory { await _createRecursiveSafe(_fs.path.dirname(p)); final parentVal = await _fs.resolvepath(_fs.path.dirname(p)); - await _fs.idb.createInode(Inode( - id: _fs.uuid.v4(), - parentId: parentVal.id, - name: _fs.path.basename(p), - nodeType: 1, - modified: DateTime.now().millisecondsSinceEpoch)); + try { + await _fs.idb.createInode( + Inode( + id: _fs.uuid.v4(), + parentId: parentVal.id, + name: _fs.path.basename(p), + nodeType: 1, + modified: DateTime.now().millisecondsSinceEpoch, + ), + ); + } catch (_) { + if (await _fs.type(p) == FileSystemEntityType.notFound) rethrow; + } } @override void createSync({bool recursive = false}) { - if (existsSync()) return; - - final parentPath = _fs.path.dirname(path); - final parentType = _fs.typeSync(parentPath); - if (parentType == FileSystemEntityType.notFound) { - if (recursive) { - _fs.directory(parentPath).createSync(recursive: true); - } else { - throw FileSystemException( - 'Cannot create directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - } else if (parentType != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot create directory, path = \'$path\' (OS Error: Not a directory, errno = 20)', - path, - ); - } - - _fs.makeSyncCall(5, utf8.encode(path)); + throw UnsupportedError('Sync create not supported'); } @override @@ -92,8 +82,15 @@ class WebDirectory extends FileSystemEntity implements Directory { final tempDir = _fs.path.join(path, name); // Ensure path exists if (!await exists()) { - throw FileSystemException( - 'Directory does not exist', path, const OSError('ENOENT', 2)); + if (path == '/tmp') { + await create(); + } else { + throw FileSystemException( + 'Directory does not exist', + path, + const OSError('ENOENT', 2), + ); + } } final dir = WebDirectory(_fs, tempDir); await dir.create(); @@ -102,15 +99,7 @@ class WebDirectory extends FileSystemEntity implements Directory { @override Directory createTempSync([String? prefix]) { - if (!existsSync()) { - throw FileSystemException( - 'Directory does not exist', path, const OSError('ENOENT', 2)); - } - final name = (prefix ?? 'temp') + _fs.uuid.v4(); - final tempDir = _fs.path.join(path, name); - final dir = WebDirectory(_fs, tempDir); - dir.createSync(); - return dir; + throw UnsupportedError('Sync not supported'); } @override @@ -120,7 +109,10 @@ class WebDirectory extends FileSystemEntity implements Directory { final children = await _fs.idb.listChildren(inode.id); if (children.isNotEmpty && !recursive) { throw FileSystemException( - 'Directory not empty', path, const OSError('ENOTEMPTY', 39)); + 'Directory not empty', + path, + const OSError('ENOTEMPTY', 39), + ); } if (recursive) { @@ -139,34 +131,8 @@ class WebDirectory extends FileSystemEntity implements Directory { } @override - void deleteSync({bool recursive = false}) { - final type = _fs.typeSync(path, followLinks: false); - if (type == FileSystemEntityType.notFound) { - throw FileSystemException( - 'Cannot delete directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - if (type != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot delete directory, path = \'$path\' (OS Error: Not a directory, errno = 20)', - path, - ); - } - - if (!recursive) { - final children = listSync(recursive: false, followLinks: false); - if (children.isNotEmpty) { - throw FileSystemException( - 'Directory not empty', - path, - const OSError('ENOTEMPTY', 39), - ); - } - } - - _fs.makeSyncCall(6, utf8.encode(path)); - } + void deleteSync({bool recursive = false}) => + throw UnsupportedError('Sync not supported'); @override Future exists() async { @@ -179,16 +145,19 @@ class WebDirectory extends FileSystemEntity implements Directory { } @override - bool existsSync() { - return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.directory; - } + bool existsSync() => throw UnsupportedError('Sync not supported'); @override - Stream list( - {bool recursive = false, bool followLinks = true}) async* { + Stream list({ + bool recursive = false, + bool followLinks = true, + }) async* { if (!await exists()) { throw FileSystemException( - 'Directory not found', path, const OSError('ENOENT', 2)); + 'Directory not found', + path, + const OSError('ENOENT', 2), + ); } final inode = await _fs.resolvepath(path); @@ -202,26 +171,6 @@ class WebDirectory extends FileSystemEntity implements Directory { if (recursive) { yield* dir.list(recursive: true, followLinks: followLinks); } - } else if (child.nodeType == 2) { - if (!followLinks) { - yield WebLink(_fs, childPath); - } else { - try { - final resolved = - await _fs.resolvepath(childPath, followLinks: true); - if (resolved.nodeType == 1) { - final dir = WebDirectory(_fs, childPath); - yield dir; - if (recursive) { - yield* dir.list(recursive: true, followLinks: true); - } - } else { - yield WebFile(_fs, childPath); - } - } catch (_) { - yield WebLink(_fs, childPath); - } - } } else { yield WebFile(_fs, childPath); } @@ -229,54 +178,11 @@ class WebDirectory extends FileSystemEntity implements Directory { } @override - List listSync( - {bool recursive = false, bool followLinks = true}) { - if (!existsSync()) { - throw FileSystemException( - 'Directory not found', path, const OSError('ENOENT', 2)); - } - - final respBytes = _fs.makeSyncCall(10, utf8.encode(path)); - final listJson = json.decode(utf8.decode(respBytes)) as List; - - final List results = []; - for (final item in listJson) { - final itemMap = item as Map; - final childPath = itemMap['path'] as String; - final typeStr = itemMap['type'] as String; - - if (typeStr == 'directory') { - final dir = WebDirectory(_fs, childPath); - results.add(dir); - if (recursive) { - results.addAll(dir.listSync(recursive: true, followLinks: followLinks)); - } - } else if (typeStr == 'link') { - if (!followLinks) { - results.add(WebLink(_fs, childPath)); - } else { - try { - final resolvedType = _fs.typeSync(childPath, followLinks: true); - if (resolvedType == FileSystemEntityType.directory) { - final dir = WebDirectory(_fs, childPath); - results.add(dir); - if (recursive) { - results.addAll(dir.listSync(recursive: true, followLinks: true)); - } - } else if (resolvedType == FileSystemEntityType.notFound) { - results.add(WebLink(_fs, childPath)); - } else { - results.add(WebFile(_fs, childPath)); - } - } catch (_) { - results.add(WebLink(_fs, childPath)); - } - } - } else { - results.add(WebFile(_fs, childPath)); - } - } - return results; + List listSync({ + bool recursive = false, + bool followLinks = true, + }) { + throw UnsupportedError('Sync list not supported'); } @override @@ -288,46 +194,22 @@ class WebDirectory extends FileSystemEntity implements Directory { final newParentInode = await _fs.resolvepath(newParentDir); final updated = Inode( - id: inode.id, - parentId: newParentInode.id, - name: newName, - nodeType: inode.nodeType, - blobId: inode.blobId, - size: inode.size, - modified: DateTime.now().millisecondsSinceEpoch); + id: inode.id, + parentId: newParentInode.id, + name: newName, + nodeType: inode.nodeType, + blobId: inode.blobId, + size: inode.size, + modified: DateTime.now().millisecondsSinceEpoch, + ); await _fs.idb.updateInode(updated); return WebDirectory(_fs, newPath); } @override - Directory renameSync(String newPath) { - final type = _fs.typeSync(path, followLinks: false); - if (type == FileSystemEntityType.notFound) { - throw FileSystemException( - 'Cannot rename directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - final newParentDir = _fs.path.dirname(newPath); - if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot rename directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - - final pathBytes = utf8.encode(path); - final newPathBytes = utf8.encode(newPath); - final request = Uint8List(4 + pathBytes.length + newPathBytes.length); - final bd = ByteData.sublistView(request); - bd.setUint32(0, pathBytes.length, Endian.little); - request.setRange(4, 4 + pathBytes.length, pathBytes); - request.setRange(4 + pathBytes.length, request.length, newPathBytes); - - _fs.makeSyncCall(12, request); - return WebDirectory(_fs, newPath); - } + Directory renameSync(String newPath) => + throw UnsupportedError('Sync not supported'); @override String get basename => _fs.path.basename(path); @@ -348,17 +230,20 @@ class WebDirectory extends FileSystemEntity implements Directory { Future stat() => _fs.stat(path); @override - FileStat statSync() => _fs.statSync(path); + FileStat statSync() => throw UnsupportedError('Sync not supported'); @override - Future resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path); + Future resolveSymbolicLinks() async => path; @override - String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path); + String resolveSymbolicLinksSync() => + throw UnsupportedError('Sync not supported'); @override - Stream watch( - {int events = FileSystemEvent.all, bool recursive = false}) { + Stream watch({ + int events = FileSystemEvent.all, + bool recursive = false, + }) { return const Stream.empty(); } diff --git a/experimental/web_file_system/lib/src/entities/web_file.dart b/experimental/web_file_system/lib/src/entities/web_file.dart index 87911db..bfa27a9 100644 --- a/experimental/web_file_system/lib/src/entities/web_file.dart +++ b/experimental/web_file_system/lib/src/entities/web_file.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:file/file.dart'; -import 'package:web_file_system/src/backend/idb_inode_service.dart'; +import '../backend/idb_inode_service.dart'; import '../web_file_system.dart'; class WebFile extends FileSystemEntity implements File { @@ -52,65 +52,28 @@ class WebFile extends FileSystemEntity implements File { } @override - void createSync({bool recursive = false, bool exclusive = false}) { - if (existsSync()) { - if (exclusive) { - throw FileSystemException( - 'File already exists', - path, - const OSError('EEXIST', 17), - ); - } - return; - } - - final parentPath = _fs.path.dirname(path); - final parentType = _fs.typeSync(parentPath); - if (parentType == FileSystemEntityType.notFound) { - if (recursive) { - _fs.directory(parentPath).createSync(recursive: true); - } else { - throw FileSystemException( - 'Cannot create file, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - } else if (parentType != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot create file, path = \'$path\' (OS Error: Not a directory, errno = 20)', - path, - ); - } - - writeAsBytesSync([]); - } + void createSync({bool recursive = false, bool exclusive = false}) => + throw UnsupportedError('Sync not supported'); @override Future copy(String newPath) async { - final inode = await _fs.resolvepath(path); - final newParent = await _fs.resolvepath(_fs.path.dirname(newPath)); + // Deep copy to ensure independence and correct storage type - await _fs.idb.createInode( - Inode( - id: _fs.uuid.v4(), - parentId: newParent.id, - name: _fs.path.basename(newPath), - nodeType: 0, - blobId: inode.blobId, - size: inode.size, - modified: DateTime.now().millisecondsSinceEpoch, - ), - ); + final newFile = WebFile(_fs, newPath); + // Use writeAsBytes to handle creation and storage logic + // We need to consume the stream to a list for now as writeAsBytes takes List + // Or we could use a stream API if available, but writeAsBytes expects List. + // Given these are images/slides, memory is likely okay. + // To be safer with memory, we should really stream it, but writeAsBytes takes List. + // Let's use readAsBytes() helper. + final bytes = await readAsBytes(); + await newFile.writeAsBytes(bytes); - return WebFile(_fs, newPath); + return newFile; } @override - File copySync(String newPath) { - final bytes = readAsBytesSync(); - _fs.file(newPath).writeAsBytesSync(bytes); - return WebFile(_fs, newPath); - } + File copySync(String newPath) => throw UnsupportedError('Sync not supported'); @override Future length() async { @@ -119,7 +82,7 @@ class WebFile extends FileSystemEntity implements File { } @override - int lengthSync() => statSync().size; + int lengthSync() => throw UnsupportedError('Sync not supported'); @override Future lastModified() async { @@ -128,7 +91,7 @@ class WebFile extends FileSystemEntity implements File { } @override - DateTime lastModifiedSync() => statSync().modified; + DateTime lastModifiedSync() => throw UnsupportedError('Sync not supported'); @override Future lastAccessed() async { @@ -136,7 +99,7 @@ class WebFile extends FileSystemEntity implements File { } @override - DateTime lastAccessedSync() => statSync().accessed; + DateTime lastAccessedSync() => throw UnsupportedError('Sync not supported'); @override Future setLastAccessed(DateTime time) async {} @@ -183,7 +146,24 @@ class WebFile extends FileSystemEntity implements File { bool flush = false, }) async { final stream = Stream.value(bytes); - final (newBlobId, _) = await _fs.opfs.writeBlob(stream); + String newBlobId; + int usedStorageType = 0; // 0 = OPFS, 1 = IDB + + try { + // Try OPFS first + newBlobId = await _fs.opfs.writeBlob(stream); + } catch (e) { + // Fallback to IDB on ANY error (TypeError, NotFoundError, etc) + try { + newBlobId = await _fs.idbStore.writeBlob(Stream.value(bytes)); + usedStorageType = 1; + } catch (e2) { + throw FileSystemException( + 'Write failed on both OPFS ($e) and IDB: $e2', + path, + ); + } + } Inode inode; try { @@ -191,15 +171,8 @@ class WebFile extends FileSystemEntity implements File { if (mode == FileMode.append) { throw UnsupportedError('Append not yet optimized'); } - } on FileSystemException catch (_) { - final parentPath = _fs.path.dirname(path); - if (await _fs.type(parentPath) != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - await create(recursive: false); + } catch (_) { + await create(recursive: true); inode = await _fs.resolvepath(path); } @@ -212,6 +185,7 @@ class WebFile extends FileSystemEntity implements File { blobId: newBlobId, size: bytes.length, modified: DateTime.now().millisecondsSinceEpoch, + storageType: usedStorageType, ), ); @@ -224,27 +198,7 @@ class WebFile extends FileSystemEntity implements File { FileMode mode = FileMode.write, bool flush = false, }) { - if (mode == FileMode.append) { - throw UnsupportedError('Append not yet optimized'); - } - - final parentPath = _fs.path.dirname(path); - if (_fs.typeSync(parentPath) != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - - final pathBytes = utf8.encode(path); - final pathLen = pathBytes.length; - final request = Uint8List(4 + pathLen + bytes.length); - final bd = ByteData.sublistView(request); - bd.setUint32(0, pathLen, Endian.little); - request.setRange(4, 4 + pathLen, pathBytes); - request.setRange(4 + pathLen, request.length, bytes); - - _fs.makeSyncCall(4, request); + throw UnsupportedError('Sync not supported'); } @override @@ -264,7 +218,7 @@ class WebFile extends FileSystemEntity implements File { Encoding encoding = utf8, bool flush = false, }) { - writeAsBytesSync(encoding.encode(contents), mode: mode, flush: flush); + throw UnsupportedError('Sync not supported'); } @override @@ -272,7 +226,37 @@ class WebFile extends FileSystemEntity implements File { final inode = await _fs.resolvepath(path); if (inode.blobId == null) return; - yield* _fs.opfs.readBlob(inode.blobId!); + if (inode.storageType == 1) { + try { + yield* _fs.idbStore.readBlob(inode.blobId!); + } catch (e) { + // Fallback to OPFS if IDB fails (maybe metadata is wrong) + print( + 'Using fallback to OPFS for reading ${inode.name} (blob: ${inode.blobId}). Error: $e', + ); + try { + yield* _fs.opfs.readBlob(inode.blobId!); + } catch (e2) { + print('OPFS fallback ALSO failed for ${inode.name}: $e2'); + rethrow; + } + } + } else { + try { + yield* _fs.opfs.readBlob(inode.blobId!); + } catch (e) { + // Fallback to IDB if OPFS fails + print( + 'Using fallback to IDB for reading ${inode.name} (blob: ${inode.blobId}). Error: $e', + ); + try { + yield* _fs.idbStore.readBlob(inode.blobId!); + } catch (e2) { + print('IDB fallback ALSO failed for ${inode.name}: $e2'); + rethrow; + } + } + } } @override @@ -281,9 +265,16 @@ class WebFile extends FileSystemEntity implements File { // Start background write but keep future to await in close() final writeFuture = _handleWrite(controller.stream, encoding, mode); - writeFuture.catchError((Object _) {}); - return _WebIOSink(controller, writeFuture, encoding); + final sink = _WebIOSink( + controller, + encoding, + onDone: () async { + await writeFuture; + }, + ); + + return sink; } Future _handleWrite( @@ -292,22 +283,23 @@ class WebFile extends FileSystemEntity implements File { FileMode mode, ) async { try { - final parentPath = _fs.path.dirname(path); - if (await _fs.type(parentPath) != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } + String newId; + int usedStorageType = 0; // 0 = OPFS, 1 = IDB - final (newId, size) = await _fs.opfs.writeBlob(stream); + try { + newId = await _fs.opfs.writeBlob(stream); + } catch (e) { + // Fallback to IDB on ANY error + newId = await _fs.idbStore.writeBlob(stream); + usedStorageType = 1; + } Inode inode; try { inode = await _fs.resolvepath(path); } catch (_) { // Create if missing - await create(recursive: false); + await create(recursive: true); inode = await _fs.resolvepath(path); } @@ -318,8 +310,10 @@ class WebFile extends FileSystemEntity implements File { name: inode.name, nodeType: 0, blobId: newId, - size: size, + size: + 0, // TODO: Size not returned by OPFS yet, so 0 for streamed content modified: DateTime.now().millisecondsSinceEpoch, + storageType: usedStorageType, ), ); } catch (e) { @@ -334,22 +328,7 @@ class WebFile extends FileSystemEntity implements File { } @override - Uint8List readAsBytesSync() { - final type = _fs.typeSync(path); - if (type == FileSystemEntityType.notFound) { - throw FileSystemException( - 'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - if (type == FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot open file, path = \'$path\' (OS Error: Is a directory, errno = 21)', - path, - ); - } - return _fs.makeSyncCall(3, utf8.encode(path)); - } + Uint8List readAsBytesSync() => throw UnsupportedError('Sync not supported'); @override Future readAsString({Encoding encoding = utf8}) async { @@ -358,21 +337,18 @@ class WebFile extends FileSystemEntity implements File { } @override - String readAsStringSync({Encoding encoding = utf8}) { - return encoding.decode(readAsBytesSync()); - } + String readAsStringSync({Encoding encoding = utf8}) => + throw UnsupportedError('Sync not supported'); @override Future> readAsLines({Encoding encoding = utf8}) async { final str = await readAsString(encoding: encoding); - return const LineSplitter().convert(str); + return str.split('\n'); } @override - List readAsLinesSync({Encoding encoding = utf8}) { - final str = readAsStringSync(encoding: encoding); - return const LineSplitter().convert(str); - } + List readAsLinesSync({Encoding encoding = utf8}) => + throw UnsupportedError('Sync not supported'); @override Future exists() async { @@ -385,9 +361,7 @@ class WebFile extends FileSystemEntity implements File { } @override - bool existsSync() { - return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.file; - } + bool existsSync() => throw UnsupportedError('Sync not supported'); @override Future rename(String newPath) async { @@ -411,58 +385,32 @@ class WebFile extends FileSystemEntity implements File { } @override - File renameSync(String newPath) { - final type = _fs.typeSync(path, followLinks: false); - if (type == FileSystemEntityType.notFound) { - throw FileSystemException( - 'Cannot rename file, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - final newParentDir = _fs.path.dirname(newPath); - if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot rename file, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - - final pathBytes = utf8.encode(path); - final newPathBytes = utf8.encode(newPath); - final request = Uint8List(4 + pathBytes.length + newPathBytes.length); - final bd = ByteData.sublistView(request); - bd.setUint32(0, pathBytes.length, Endian.little); - request.setRange(4, 4 + pathBytes.length, pathBytes); - request.setRange(4 + pathBytes.length, request.length, newPathBytes); - - _fs.makeSyncCall(12, request); - return WebFile(_fs, newPath); - } + File renameSync(String newPath) => + throw UnsupportedError('Sync not supported'); @override Future delete({bool recursive = false}) async { final inode = await _fs.resolvepath(path); + if (inode.blobId != null) { + if (inode.storageType == 1) { + await _fs.idbStore.deleteBlob(inode.blobId!); + } else { + await _fs.opfs.deleteBlob(inode.blobId!); + } + } await _fs.idb.deleteInode(inode.id); return this; } @override - void deleteSync({bool recursive = false}) { - final type = _fs.typeSync(path, followLinks: false); - if (type == FileSystemEntityType.notFound) { - throw FileSystemException( - 'Cannot delete file, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - _fs.makeSyncCall(6, utf8.encode(path)); - } + void deleteSync({bool recursive = false}) => + throw UnsupportedError('Sync not supported'); @override Future stat() => _fs.stat(path); @override - FileStat statSync() => _fs.statSync(path); + FileStat statSync() => throw UnsupportedError('Sync not supported'); @override Uri get uri => Uri.parse(path); @@ -478,10 +426,11 @@ class WebFile extends FileSystemEntity implements File { File get absolute => WebFile(_fs, _fs.path.absolute(path)); @override - Future resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path); + Future resolveSymbolicLinks() async => path; @override - String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path); + String resolveSymbolicLinksSync() => + throw UnsupportedError('Sync not supported'); @override Stream watch({ @@ -494,10 +443,10 @@ class WebFile extends FileSystemEntity implements File { class _WebIOSink implements IOSink { final StreamController> _controller; - final Future _writeFuture; + final Future Function()? onDone; Encoding _encoding; - _WebIOSink(this._controller, this._writeFuture, this._encoding); + _WebIOSink(this._controller, this._encoding, {this.onDone}); @override Encoding get encoding => _encoding; @@ -507,35 +456,27 @@ class _WebIOSink implements IOSink { @override void add(List data) { - if (_controller.isClosed) return; _controller.add(data); } @override void addError(Object error, [StackTrace? stackTrace]) { - if (_controller.isClosed) return; _controller.addError(error, stackTrace); } @override Future addStream(Stream> stream) { - return Future.any([ - _controller.addStream(stream), - _writeFuture, - ]); + return _controller.addStream(stream); } @override Future close() async { await _controller.close(); - await _writeFuture; + if (onDone != null) await onDone!(); } @override - Future get done => Future.any([ - _controller.done, - _writeFuture, - ]); + Future get done => _controller.done; @override Future flush() async {} diff --git a/experimental/web_file_system/lib/src/entities/web_link.dart b/experimental/web_file_system/lib/src/entities/web_link.dart index 9c3a156..8110976 100644 --- a/experimental/web_file_system/lib/src/entities/web_link.dart +++ b/experimental/web_file_system/lib/src/entities/web_link.dart @@ -1,8 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:file/file.dart'; -import 'package:web_file_system/src/backend/idb_inode_service.dart'; +import '../backend/idb_inode_service.dart'; import '../web_file_system.dart'; class WebLink extends FileSystemEntity implements Link { @@ -36,7 +35,7 @@ class WebLink extends FileSystemEntity implements Link { // Write target path string to OPFS blob final stream = Stream.value(utf8.encode(target)); - final (blobId, _) = await _fs.opfs.writeBlob(stream); + final blobId = await _fs.opfs.writeBlob(stream); final parentPath = _fs.path.dirname(path); final parentInode = await _fs.resolvepath(parentPath); @@ -58,41 +57,7 @@ class WebLink extends FileSystemEntity implements Link { @override void createSync(String target, {bool recursive = false}) { - if (existsSync()) { - throw FileSystemException( - 'Link already exists', - path, - const OSError('EEXIST', 17), - ); - } - - final parentPath = _fs.path.dirname(path); - final parentType = _fs.typeSync(parentPath); - if (parentType == FileSystemEntityType.notFound) { - if (recursive) { - _fs.directory(parentPath).createSync(recursive: true); - } else { - throw FileSystemException( - 'Cannot create link, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - } else if (parentType != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot create link, path = \'$path\' (OS Error: Not a directory, errno = 20)', - path, - ); - } - - final pathBytes = utf8.encode(path); - final targetBytes = utf8.encode(target); - final request = Uint8List(4 + pathBytes.length + targetBytes.length); - final bd = ByteData.sublistView(request); - bd.setUint32(0, pathBytes.length, Endian.little); - request.setRange(4, 4 + pathBytes.length, pathBytes); - request.setRange(4 + pathBytes.length, request.length, targetBytes); - - _fs.makeSyncCall(7, request); + throw UnsupportedError('Sync not supported'); } @override @@ -101,7 +66,7 @@ class WebLink extends FileSystemEntity implements Link { // Write new blob final stream = Stream.value(utf8.encode(target)); - final (blobId, _) = await _fs.opfs.writeBlob(stream); + final blobId = await _fs.opfs.writeBlob(stream); await _fs.idb.updateInode( Inode( @@ -120,30 +85,7 @@ class WebLink extends FileSystemEntity implements Link { @override void updateSync(String target) { - final type = _fs.typeSync(path, followLinks: false); - if (type == FileSystemEntityType.notFound) { - throw FileSystemException( - 'Cannot update link, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - if (type != FileSystemEntityType.link) { - throw FileSystemException( - 'Not a link', - path, - const OSError('EINVAL', 22), - ); - } - - final pathBytes = utf8.encode(path); - final targetBytes = utf8.encode(target); - final request = Uint8List(4 + pathBytes.length + targetBytes.length); - final bd = ByteData.sublistView(request); - bd.setUint32(0, pathBytes.length, Endian.little); - request.setRange(4, 4 + pathBytes.length, pathBytes); - request.setRange(4 + pathBytes.length, request.length, targetBytes); - - _fs.makeSyncCall(13, request); + throw UnsupportedError('Sync not supported'); } @override @@ -165,22 +107,7 @@ class WebLink extends FileSystemEntity implements Link { @override String targetSync() { - final type = _fs.typeSync(path, followLinks: false); - if (type == FileSystemEntityType.notFound) { - throw FileSystemException( - 'Cannot read link, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - if (type != FileSystemEntityType.link) { - throw FileSystemException( - 'Not a link', - path, - const OSError('EINVAL', 22), - ); - } - final respBytes = _fs.makeSyncCall(8, utf8.encode(path)); - return utf8.decode(respBytes); + throw UnsupportedError('Sync not supported'); } @override @@ -205,33 +132,8 @@ class WebLink extends FileSystemEntity implements Link { } @override - Link renameSync(String newPath) { - final type = _fs.typeSync(path, followLinks: false); - if (type == FileSystemEntityType.notFound) { - throw FileSystemException( - 'Cannot rename link, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - final newParentDir = _fs.path.dirname(newPath); - if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) { - throw FileSystemException( - 'Cannot rename link, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - - final pathBytes = utf8.encode(path); - final newPathBytes = utf8.encode(newPath); - final request = Uint8List(4 + pathBytes.length + newPathBytes.length); - final bd = ByteData.sublistView(request); - bd.setUint32(0, pathBytes.length, Endian.little); - request.setRange(4, 4 + pathBytes.length, pathBytes); - request.setRange(4 + pathBytes.length, request.length, newPathBytes); - - _fs.makeSyncCall(12, request); - return WebLink(_fs, newPath); - } + Link renameSync(String newPath) => + throw UnsupportedError('Sync not supported'); @override Future delete({bool recursive = false}) async { @@ -241,16 +143,8 @@ class WebLink extends FileSystemEntity implements Link { } @override - void deleteSync({bool recursive = false}) { - final type = _fs.typeSync(path, followLinks: false); - if (type == FileSystemEntityType.notFound) { - throw FileSystemException( - 'Cannot delete link, path = \'$path\' (OS Error: No such file or directory, errno = 2)', - path, - ); - } - _fs.makeSyncCall(6, utf8.encode(path)); - } + void deleteSync({bool recursive = false}) => + throw UnsupportedError('Sync not supported'); @override Future exists() async { @@ -263,15 +157,13 @@ class WebLink extends FileSystemEntity implements Link { } @override - bool existsSync() { - return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.link; - } + bool existsSync() => throw UnsupportedError('Sync not supported'); @override Future stat() async => (await _fs.stat(path)); @override - FileStat statSync() => _fs.statSync(path); + FileStat statSync() => throw UnsupportedError('Sync not supported'); @override Uri get uri => Uri.parse(path); @@ -292,10 +184,21 @@ class WebLink extends FileSystemEntity implements Link { Link get absolute => WebLink(_fs, _fs.path.absolute(path)); @override - Future resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path); + Future resolveSymbolicLinks() async { + // If we are a link, return target? No, resolveSymbolicLinks follows all the way to canonical path. + // For now, simpler: resolve path logic. + final targetPath = await target(); + // If target is relative, resolve against directory. This gets complex. + // MVP: Return target path raw? No, contract says "path with all symbolic links resolved". + // This requires full traversal logic. + // For MVP just return the path as we stored it if it's absolute, or join if relative. + if (_fs.path.isAbsolute(targetPath)) return targetPath; + return _fs.path.normalize(_fs.path.join(dirname, targetPath)); + } @override - String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path); + String resolveSymbolicLinksSync() => + throw UnsupportedError('Sync not supported'); @override Stream watch({ diff --git a/experimental/web_file_system/lib/src/web_file_system.dart b/experimental/web_file_system/lib/src/web_file_system.dart index 54b4708..c7cdfd9 100644 --- a/experimental/web_file_system/lib/src/web_file_system.dart +++ b/experimental/web_file_system/lib/src/web_file_system.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'dart:js_interop'; import 'package:file/file.dart'; +import 'package:web/web.dart' as web; import 'package:path/path.dart' as p; import 'package:uuid/uuid.dart'; -import 'package:web_file_system/src/backend/idb_inode_service.dart'; -import 'package:web_file_system/src/backend/opfs_block_store.dart'; -import 'package:web_file_system/src/backend/sync_rpc_helper.dart'; + +import 'backend/idb_inode_service.dart'; +import 'backend/opfs_block_store.dart'; +import 'backend/idb_block_store.dart'; import 'entities/web_directory.dart'; import 'entities/web_file.dart'; import 'entities/web_link.dart'; @@ -15,11 +16,13 @@ import 'entities/web_link.dart'; class WebFileSystem extends FileSystem { final IdbInodeService _idb = IdbInodeService(); final OpfsBlockStore _opfs = OpfsBlockStore(); + final IdbBlockStore _idbStore = IdbBlockStore(); final Uuid _uuid = Uuid(); // Public matchers for internal use IdbInodeService get idb => _idb; OpfsBlockStore get opfs => _opfs; + IdbBlockStore get idbStore => _idbStore; Uuid get uuid => _uuid; WebFileSystem(); @@ -63,43 +66,9 @@ class WebFileSystem extends FileSystem { } } - static void registerWorkerProxy(dynamic worker, WebFileSystem fs) { - SyncRpcHelper.registerWorkerProxy(worker as JSAny, fs as JSAny); - } - - Uint8List makeSyncCall(int cmd, Uint8List request) { - if (!SyncRpcHelper.isWorker) { - throw UnsupportedError('Synchronous operations are only supported inside Web Workers.'); - } - if (!SyncRpcHelper.isSharedArrayBufferSupported) { - throw StateError('SharedArrayBuffer is not supported. Ensure your site is cross-origin isolated with COOP/COEP headers.'); - } - return SyncRpcHelper.sendSyncRequest(cmd, request); - } - @override FileSystemEntityType typeSync(String path, {bool followLinks = true}) { - if (!SyncRpcHelper.isWorker) { - throw UnsupportedError('Synchronous operations are only supported inside Web Workers.'); - } - if (!SyncRpcHelper.isSharedArrayBufferSupported) { - throw StateError('SharedArrayBuffer is not supported. Ensure your site is cross-origin isolated with COOP/COEP headers.'); - } - try { - final pathBytes = utf8.encode(path); - final payload = Uint8List(pathBytes.length + 1); - payload[0] = followLinks ? 1 : 0; - payload.setRange(1, payload.length, pathBytes); - - final respBytes = makeSyncCall(2, payload); - final typeStr = utf8.decode(respBytes); - if (typeStr == 'file') return FileSystemEntityType.file; - if (typeStr == 'directory') return FileSystemEntityType.directory; - if (typeStr == 'link') return FileSystemEntityType.link; - return FileSystemEntityType.notFound; - } catch (_) { - return FileSystemEntityType.notFound; - } + throw UnsupportedError('Sync type not supported'); } // Internal Resolution Logic @@ -194,6 +163,7 @@ class WebFileSystem extends FileSystem { return currentInode; } + @override String getPath(dynamic path) { if (path is String) return path; if (path is FileSystemEntity) return path.path; @@ -213,34 +183,7 @@ class WebFileSystem extends FileSystem { @override FileStat statSync(String path) { - if (!SyncRpcHelper.isWorker) { - throw UnsupportedError('Synchronous operations are only supported inside Web Workers.'); - } - if (!SyncRpcHelper.isSharedArrayBufferSupported) { - throw StateError('SharedArrayBuffer is not supported. Ensure your site is cross-origin isolated with COOP/COEP headers.'); - } - try { - final respBytes = makeSyncCall(9, utf8.encode(path)); - final statMap = json.decode(utf8.decode(respBytes)) as Map; - final typeStr = statMap['type'] as String; - final size = statMap['size'] as int; - final modified = statMap['modified'] as int; - - FileSystemEntityType type; - if (typeStr == 'file') { - type = FileSystemEntityType.file; - } else if (typeStr == 'directory') { - type = FileSystemEntityType.directory; - } else if (typeStr == 'link') { - type = FileSystemEntityType.link; - } else { - type = FileSystemEntityType.notFound; - } - - return FileStatImpl(modified, size, type); - } catch (_) { - return FileStatImpl(0, 0, FileSystemEntityType.notFound); - } + throw UnsupportedError('Sync stat not supported'); } FileSystemEntityType _getType(int nodeType) { @@ -252,15 +195,15 @@ class WebFileSystem extends FileSystem { @override bool isFileSync(String path) => - typeSync(path) == FileSystemEntityType.file; + throw UnsupportedError('Sync isFile not supported'); @override bool isDirectorySync(String path) => - typeSync(path) == FileSystemEntityType.directory; + throw UnsupportedError('Sync isDirectory not supported'); @override bool isLinkSync(String path) => - typeSync(path, followLinks: false) == FileSystemEntityType.link; + throw UnsupportedError('Sync isLink not supported'); @override Future isFile(String path) async => @@ -274,6 +217,7 @@ class WebFileSystem extends FileSystem { Future isLink(String path) async => (await type(path, followLinks: false)) == FileSystemEntityType.link; + @override bool get isWatchSupported => false; @override @@ -281,8 +225,9 @@ class WebFileSystem extends FileSystem { final s1 = await stat(path1); final s2 = await stat(path2); if (s1.type == FileSystemEntityType.notFound || - s2.type == FileSystemEntityType.notFound) + s2.type == FileSystemEntityType.notFound) { return false; + } final i1 = await resolvepath(path1); final i2 = await resolvepath(path2); @@ -290,31 +235,20 @@ class WebFileSystem extends FileSystem { } @override - bool identicalSync(String path1, String path2) { - try { - final r1 = resolveSymbolicLinksSync(path1); - final r2 = resolveSymbolicLinksSync(path2); - return r1 == r2; - } catch (_) { - return false; - } - } + @override + bool identicalSync(String path1, String path2) => + throw UnsupportedError('Sync not supported'); - Future resolveSymbolicLinks(String pathStr) async { - final inode = await resolvepath(pathStr, followLinks: true); - final List segments = []; - Inode current = inode; - while (current.id != IdbInodeService.rootId) { - segments.add(current.name); - current = await _idb.getInode(current.parentId); - } - if (segments.isEmpty) return '/'; - return '/' + segments.reversed.join('/'); - } + Future getAsBlob(String path) async { + final inode = await resolvepath(path); + if (inode.nodeType != 0) throw FileSystemException('Not a file', path); + if (inode.blobId == null) return web.Blob([].toJS); - String resolveSymbolicLinksSync(String pathStr) { - final respBytes = makeSyncCall(11, utf8.encode(pathStr)); - return utf8.decode(respBytes); + if (inode.storageType == 1) { + return _idbStore.getBlob(inode.blobId!); + } else { + return _opfs.getBlob(inode.blobId!); + } } } diff --git a/experimental/web_file_system/lib/web_file_system.dart b/experimental/web_file_system/lib/web_file_system.dart index 74644a9..93fc5e1 100644 --- a/experimental/web_file_system/lib/web_file_system.dart +++ b/experimental/web_file_system/lib/web_file_system.dart @@ -1,5 +1,5 @@ /// A high-performance, asynchronous file system for the web. -library web_file_system; +library; export 'package:file/file.dart'; export 'src/web_file_system.dart'; diff --git a/experimental/web_file_system/pubspec.lock b/experimental/web_file_system/pubspec.lock index 9eec6e3..2ff4e7f 100644 --- a/experimental/web_file_system/pubspec.lock +++ b/experimental/web_file_system/pubspec.lock @@ -141,10 +141,10 @@ packages: dependency: "direct dev" description: name: lints - sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "3.0.0" logging: dependency: transitive description: @@ -403,4 +403,3 @@ packages: version: "3.1.3" sdks: dart: ">=3.11.0 <4.0.0" - flutter: ">=3.38.5" diff --git a/experimental/web_file_system/pubspec.yaml b/experimental/web_file_system/pubspec.yaml index 1961a9c..93f1002 100644 --- a/experimental/web_file_system/pubspec.yaml +++ b/experimental/web_file_system/pubspec.yaml @@ -1,20 +1,16 @@ name: web_file_system description: A high-performance, asynchronous file system for the web using IDB and OPFS. version: 0.0.1 -publish_to: 'none' -homepage: https://github.com/fluttercommunity/flutter_whatsnew -maintainer: Rody Davis (@rodydavis) - environment: sdk: ^3.10.0 - flutter: ^3.38.5 dependencies: file: ^7.0.0 path: ^1.9.0 web: ^1.1.1 uuid: ^4.0.0 + mime: ^1.0.0 dev_dependencies: - lints: ^6.0.0 + lints: ^3.0.0 test: ^1.25.0 diff --git a/experimental/web_file_system/test/cleanup_troubleshoot_test.dart b/experimental/web_file_system/test/cleanup_troubleshoot_test.dart new file mode 100644 index 0000000..382fa8d --- /dev/null +++ b/experimental/web_file_system/test/cleanup_troubleshoot_test.dart @@ -0,0 +1,94 @@ +@TestOn('browser') +import 'dart:convert'; +import 'package:test/test.dart'; +import 'package:web_file_system/web_file_system.dart'; + +void main() { + late WebFileSystem fs; + + setUp(() async { + // Tests run in parallel in browser often sharing context, so use unique paths. + fs = WebFileSystem(); + }); + + group('Troubleshooting NotFoundError', () { + test('Temp directory creation and persistence', () async { + print('DEBUG: creating temp dir'); + final tempDir = await fs.systemTempDirectory.createTemp('debug_test_'); + print('DEBUG: Created temp dir at ${tempDir.path}'); + + expect(await tempDir.exists(), isTrue); + + final file = tempDir.childFile('test.txt'); + await file.writeAsString('Persistence Check'); + + print('DEBUG: checking existence immediately'); + expect(await file.exists(), isTrue); + expect(await file.readAsString(), equals('Persistence Check')); + + // Wait a bit to simulate pipeline delays + await Future.delayed(const Duration(milliseconds: 500)); + + print('DEBUG: checking existence after delay'); + expect(await file.readAsString(), equals('Persistence Check')); + + // Cleanup + await tempDir.delete(recursive: true); + print('DEBUG: deleted temp dir'); + expect(await file.exists(), isFalse); + }); + + test('Deep copy behavior verification', () async { + final srcDir = await fs.systemTempDirectory.createTemp('src_'); + final destDir = await fs.systemTempDirectory.createTemp('dest_'); + + final srcFile = srcDir.childFile('source.dat'); + // Write some substantial data + final data = utf8.encode('Essential Data for Export'); + await srcFile.writeAsBytes(data); + + print('DEBUG: Source file written to ${srcFile.path}'); + + final destPath = destDir.childFile('copy.dat').path; + print('DEBUG: Copying to $destPath'); + + final copiedFile = await srcFile.copy(destPath); + + print('DEBUG: Copy complete'); + expect(await copiedFile.exists(), isTrue); + expect(await copiedFile.readAsBytes(), equals(data)); + + // Now delete source + print('DEBUG: Deleting source file'); + await srcFile.delete(); + expect(await srcFile.exists(), isFalse); + + // Verify copy still exists and is readable (Deep Copy Check) + print('DEBUG: Verifying copy after source deletion'); + expect(await copiedFile.exists(), isTrue); + try { + final copyData = await copiedFile.readAsBytes(); + expect(copyData, equals(data)); + print('DEBUG: Deep copy verified!'); + } catch (e) { + print('ERROR: Deep copy failed! Reading copy caused error: $e'); + rethrow; + } + }); + + test('Concurrent Write/Read stress', () async { + final dir = await fs.systemTempDirectory.createTemp('stress_'); + final file = dir.childFile('stress.txt'); + + // Write + await file.writeAsString('Initial'); + + // Rapidly update + for (int i = 0; i < 10; i++) { + await file.writeAsString('Update $i'); + final content = await file.readAsString(); + expect(content, equals('Update $i')); + } + }); + }); +} diff --git a/experimental/web_file_system/test/robust_correctness_test.dart b/experimental/web_file_system/test/robust_correctness_test.dart new file mode 100644 index 0000000..68198b3 --- /dev/null +++ b/experimental/web_file_system/test/robust_correctness_test.dart @@ -0,0 +1,113 @@ +@TestOn('browser') +import 'dart:async'; +import 'dart:typed_data'; +import 'package:test/test.dart'; +import 'package:web_file_system/web_file_system.dart'; + +void main() { + late WebFileSystem fs; + + setUp(() async { + fs = WebFileSystem(); + }); + + group('Robust Correctness Tests', () { + test('Multiple files in a directory stress test', () async { + final uniqueId = DateTime.now().millisecondsSinceEpoch; + final dirPath = '/stress_$uniqueId'; + final dir = fs.directory(dirPath); + await dir.create(); + + final fileCount = 50; + final futures = []; + + for (var i = 0; i < fileCount; i++) { + futures.add( + fs.file('$dirPath/file_$i.txt').writeAsString('Content of file $i'), + ); + } + await Future.wait(futures); + + final entities = await dir.list().toList(); + expect(entities.length, equals(fileCount)); + + final names = entities.map((e) => fs.path.basename(e.path)).toSet(); + for (var i = 0; i < fileCount; i++) { + expect(names, contains('file_$i.txt')); + final content = await fs.file('$dirPath/file_$i.txt').readAsString(); + expect(content, equals('Content of file $i')); + } + }); + + test('Temporary directory usage', () async { + final tempDir = await fs.systemTempDirectory.createTemp('my_prefix_'); + expect(tempDir.path, startsWith('/tmp/my_prefix_')); + expect(await tempDir.exists(), isTrue); + + final file = fs.file(fs.path.join(tempDir.path, 'test.txt')); + await file.writeAsString('temp content'); + expect(await file.readAsString(), equals('temp content')); + + await tempDir.delete(recursive: true); + expect(await tempDir.exists(), isFalse); + expect(await file.exists(), isFalse); + }); + + test('Different files with the same bytes (deduplication/collision check)', () async { + final uniqueId = DateTime.now().millisecondsSinceEpoch; + final data = Uint8List.fromList([1, 2, 3, 4, 5]); + + final file1 = fs.file('/file1_$uniqueId.bin'); + final file2 = fs.file('/file2_$uniqueId.bin'); + + await file1.writeAsBytes(data); + await file2.writeAsBytes(data); + + expect(await file1.readAsBytes(), equals(data)); + expect(await file2.readAsBytes(), equals(data)); + + // Update one, ensure other is unchanged + final newData = Uint8List.fromList([6, 7, 8]); + await file1.writeAsBytes(newData); + + expect(await file1.readAsBytes(), equals(newData)); + expect(await file2.readAsBytes(), equals(data)); + + // Cleanup + await file1.delete(); + await file2.delete(); + + expect(await file1.exists(), isFalse); + expect(await file2.exists(), isFalse); + }); + + test('Resource cleanup verification (Blob leak check)', () async { + final uniqueId = DateTime.now().millisecondsSinceEpoch; + final filePath = '/leak_test_$uniqueId.bin'; + final data = Uint8List.fromList(List.generate(100, (i) => i)); + + final file = fs.file(filePath); + await file.writeAsBytes(data); + + final inode = await fs.resolvepath(filePath); + final blobId = inode.blobId; + expect(blobId, isNotNull); + + // Verify blob exists in store + if (inode.storageType == 1) { + expect(await fs.idbStore.getBlob(blobId!), isNotNull); + } else { + expect(await fs.opfs.getBlob(blobId!), isNotNull); + } + + await file.delete(); + + // Verify blob is gone + if (inode.storageType == 1) { + expect(() => fs.idbStore.getBlob(blobId!), throwsA(anything)); + } else { + expect(() => fs.opfs.getBlob(blobId!), throwsA(anything)); + } + }); + }); +} diff --git a/experimental/web_file_system/test/web_file_system_test.dart b/experimental/web_file_system/test/web_file_system_test.dart index 5e8f286..a68f1aa 100644 --- a/experimental/web_file_system/test/web_file_system_test.dart +++ b/experimental/web_file_system/test/web_file_system_test.dart @@ -1,13 +1,8 @@ @TestOn('browser') import 'dart:async'; -import 'dart:convert'; import 'dart:typed_data'; -import 'dart:js_interop'; import 'package:test/test.dart'; import 'package:web_file_system/web_file_system.dart'; -import 'package:web_file_system/src/backend/idb_inode_service.dart'; -import 'package:web_file_system/src/backend/opfs_block_store.dart'; - void main() { late WebFileSystem fs; @@ -109,235 +104,6 @@ void main() { equals('child-content'), ); }); - - test('Streamed write records correct file size in inode', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final file = fs.file('/stream_size_$uniqueId.txt'); - final sink = file.openWrite(); - sink.add([1, 2, 3, 4, 5]); - await sink.close(); - - expect(await file.length(), equals(5)); - }); - - test('Writing to file with nonexistent parent throws FileSystemException', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final file = fs.file('/nonexistent_$uniqueId/file.txt'); - - expect( - () => file.writeAsBytes([1, 2, 3]), - throwsA(isA()), - ); - - final sink = file.openWrite(); - expect( - () => sink.addStream(Stream.value([1, 2, 3])).then((_) => sink.close()), - throwsA(isA()), - ); - }); - - test('Directory list yields correct entity types based on followLinks', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final dir = '/list_test_$uniqueId'; - final file = '$dir/file.txt'; - final link = '$dir/link_to_file'; - - await fs.directory(dir).create(); - await fs.file(file).writeAsString('data'); - await fs.link(link).create(file); - - // Listing with followLinks = false yields Link - final listNoFollow = await fs.directory(dir).list(followLinks: false).toList(); - final linkEntity = listNoFollow.firstWhere((e) => e.path == link); - expect(linkEntity, isA()); - - // Listing with followLinks = true yields File - final listFollow = await fs.directory(dir).list(followLinks: true).toList(); - final fileEntity = listFollow.firstWhere((e) => e.path == link); - expect(fileEntity, isA()); - }); - - test('Listing a non-existent directory path throws FileSystemException', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final dir = fs.directory('/nonexistent_dir_$uniqueId'); - expect( - () => dir.list().toList(), - throwsA(isA().having((e) => e.message, 'message', contains('Directory not found'))), - ); - }); - - test('Recursive listing of a directory containing a symlink pointing to another directory with followLinks: true', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final root = '/root_$uniqueId'; - final targetDir = '/target_dir_$uniqueId'; - final file = '$targetDir/file.txt'; - final link = '$root/link_to_dir'; - - await fs.directory(root).create(); - await fs.directory(targetDir).create(); - await fs.file(file).writeAsString('some data'); - await fs.link(link).create(targetDir); - - final list = await fs.directory(root).list(recursive: true, followLinks: true).toList(); - final paths = list.map((e) => e.path).toList(); - - expect(paths, contains(link)); - expect(paths, contains('$link/file.txt')); - - final linkDirEntity = list.firstWhere((e) => e.path == link); - expect(linkDirEntity, isA()); - }); - - test('Listing directory containing a broken symlink with followLinks: true yields WebLink', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final dir = '/broken_link_test_$uniqueId'; - final link = '$dir/broken_link'; - final nonexistentTarget = '/nonexistent_target_$uniqueId'; - - await fs.directory(dir).create(); - await fs.link(link).create(nonexistentTarget); - - final list = await fs.directory(dir).list(followLinks: true).toList(); - expect(list.length, equals(1)); - expect(list.first.path, equals(link)); - expect(list.first, isA()); - }); - - test('resolveSymbolicLinks resolves canonical path', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final dir = '/resolve_test_$uniqueId'; - final subDir = '$dir/subdir'; - final target = '$subDir/file.txt'; - final link = '$dir/link_to_file'; - - await fs.directory(subDir).create(recursive: true); - await fs.file(target).writeAsString('target'); - await fs.link(link).create(target); - - // Verify canonical link resolution - expect(await fs.link(link).resolveSymbolicLinks(), equals(target)); - expect(await fs.file(link).resolveSymbolicLinks(), equals(target)); - - // Verify canonical directory resolution - final dirLink = '$dir/link_to_subdir'; - await fs.link(dirLink).create(subDir); - expect(await fs.directory(dirLink).resolveSymbolicLinks(), equals(subDir)); - }); - - test('IndexedDB concurrent database initialization does not fail or race', () async { - final futures = >[]; - for (int i = 0; i < 15; i++) { - futures.add(WebFileSystem().file('/concurrent_$i.txt').exists()); - } - final results = await Future.wait(futures); - expect(results, hasLength(15)); - for (final result in results) { - expect(result, isFalse); - } - }); - - test('WebFile.create on existing file returns the file', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final file = fs.file('/create_existing_$uniqueId.txt'); - await file.create(); - expect(await file.exists(), isTrue); - final sameFile = await file.create(); - expect(sameFile.path, equals(file.path)); - }); - - test('WebFile.create recursive in nested directory structure', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final file = fs.file('/nested_$uniqueId/sub_$uniqueId/file.txt'); - await file.create(recursive: true); - expect(await file.exists(), isTrue); - }); - - test('WebFile.writeAsBytes with FileMode.append throws UnsupportedError', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final file = fs.file('/append_test_$uniqueId.txt'); - await file.create(); - expect( - () => file.writeAsBytes([1, 2], mode: FileMode.append), - throwsA(isA()), - ); - }); - - test('WebLink.create on existing link throws FileSystemException', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final targetPath = '/target_$uniqueId.txt'; - final linkPath = '/link_$uniqueId'; - - await fs.file(targetPath).writeAsString('target-content'); - final link = fs.link(linkPath); - await link.create(targetPath); - - expect( - () => link.create(targetPath), - throwsA(isA().having((e) => e.message, 'message', contains('Link already exists'))), - ); - }); - - test('WebLink.target on a non-link entity throws FileSystemException', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final filePath = '/file_$uniqueId.txt'; - await fs.file(filePath).writeAsString('some content'); - - final link = fs.link(filePath); - expect( - () => link.target(), - throwsA(isA().having((e) => e.message, 'message', contains('Not a link'))), - ); - }); - - test('Deep symbolic link chain throws ELOOP', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - await fs.directory('/dir0_$uniqueId').create(); - for (int i = 1; i <= 25; i++) { - await fs.directory('/dir${i}_$uniqueId').create(); - await fs.link('/dir${i-1}_$uniqueId/link$i').create('/dir${i}_$uniqueId'); - } - - final pathParts = ['/dir0_$uniqueId']; - for (int i = 1; i <= 25; i++) { - pathParts.add('link$i'); - } - final longPath = pathParts.join('/'); - - expect( - () => fs.file(longPath).readAsBytes(), - throwsA(isA().having((e) => e.osError?.errorCode, 'errorCode', 40)), - ); - }); - - test('Relative symbolic link target resolution', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - await fs.directory('/dir_$uniqueId').create(); - await fs.file('/target_$uniqueId.txt').writeAsString('relative-target'); - await fs.link('/dir_$uniqueId/link_$uniqueId').create('../target_$uniqueId.txt'); - - expect( - await fs.file('/dir_$uniqueId/link_$uniqueId').readAsString(), - equals('relative-target'), - ); - }); - - test('Treating file symlink as directory throws ENOTDIR', () async { - final uniqueId = DateTime.now().millisecondsSinceEpoch; - final filePath = '/file_$uniqueId.txt'; - final linkPath = '/link_$uniqueId'; - - await fs.file(filePath).writeAsString('file-content'); - await fs.link(linkPath).create(filePath); - - expect( - () => fs.file('$linkPath/child.txt').readAsBytes(), - throwsA(isA().having((e) => e.osError?.errorCode, 'errorCode', 20)), - ); - }); - - test('resolveSymbolicLinks on root directory', () async { - expect(await fs.resolveSymbolicLinks('/'), equals('/')); - }); }); group('Benchmarks', () { @@ -373,7 +139,9 @@ void main() { final uniqueId = DateTime.now().millisecondsSinceEpoch; final chunkSize = 1024 * 1024; final chunk = Uint8List(chunkSize); - for (int i = 0; i < chunkSize; i++) chunk[i] = i % 256; + for (int i = 0; i < chunkSize; i++) { + chunk[i] = i % 256; + } final file = fs.file('/video_$uniqueId.mp4'); final sink = file.openWrite(); @@ -396,747 +164,4 @@ void main() { expect(totalBytes, equals(10 * chunkSize)); }); }); - - group('IdbInodeService Coverage', () { - setUp(() { - injectMockJS(); - }); - - tearDown(() { - setMockIDBOpenShouldFail(false); - setMockIDBGetShouldFail(false); - setMockIDBPutShouldFail(false); - setMockIDBIndexGetShouldFail(false); - }); - - test('unique index violation triggers request.onerror', () async { - final idb = fs.idb; - // Ensure DB is ready - await idb.getInode(IdbInodeService.rootId); - - // Create two different inodes with same parentId and name - final inode1 = Inode( - id: 'inode-1', - parentId: IdbInodeService.rootId, - name: 'duplicate-name', - nodeType: 0, - modified: DateTime.now().millisecondsSinceEpoch, - ); - final inode2 = Inode( - id: 'inode-2', - parentId: IdbInodeService.rootId, - name: 'duplicate-name', - nodeType: 0, - modified: DateTime.now().millisecondsSinceEpoch, - ); - - await idb.createInode(inode1); - - // The second one must throw Exception('IDB Error') due to unique index on (parentId, name) - expect( - () => idb.createInode(inode2), - throwsA(isA().having((e) => e.toString(), 'message', contains('IDB Error'))), - ); - }); - - test('database open error triggers request.onerror', () async { - setMockIDBOpenShouldFail(true); - // Instantiate a new service so we call _init() again. - final newService = IdbInodeService(); - expect( - () => newService.getInode('some-id'), - throwsA(isA().having((e) => e.toString(), 'message', contains('Failed to open IDB'))), - ); - }); - - test('_ensureRootExists failure triggers open request catchError', () async { - setMockIDBGetShouldFail(true); - setMockIDBPutShouldFail(true); - final newService = IdbInodeService(); - expect( - () => newService.getInode('some-id'), - throwsA(isA().having((e) => e.toString(), 'message', contains('IDB Error'))), - ); - }); - - test('getChild catches request.onerror and returns null', () async { - final idb = fs.idb; - // Ensure DB ready - await idb.getInode(IdbInodeService.rootId); - - setMockIDBIndexGetShouldFail(true); - final child = await idb.getChild(IdbInodeService.rootId, 'any-name'); - expect(child, isNull); - }); - }); - - group('OpfsBlockStore Coverage', () { - test('writeBlob handles stream error and cleanup', () async { - final store = OpfsBlockStore(); - final stream = Stream>.error(Exception('Stream error')); - expect( - () => store.writeBlob(stream), - throwsA(isA().having((e) => e.toString(), 'message', contains('Stream error'))), - ); - }); - - test('readBlob rethrows exception on non-existent block', () async { - final store = OpfsBlockStore(); - final nonExistentBlockId = '00000000-0000-0000-0000-000000000000'; - expect( - () => store.readBlob(nonExistentBlockId).drain(), - throwsA(anything), - ); - }); - }); - - group('Sync API Mocking & Error Verification', () { - test('Synchronous APIs on main thread throw UnsupportedError', () { - final file = fs.file('/sync_main.txt'); - expect(() => fs.typeSync('/sync_main.txt'), throwsA(isA())); - expect(() => fs.statSync('/sync_main.txt'), throwsA(isA())); - expect(() => fs.resolveSymbolicLinksSync('/sync_main.txt'), throwsA(isA())); - expect(() => file.createSync(), throwsA(isA())); - expect(() => file.writeAsBytesSync([1]), throwsA(isA())); - expect(() => file.writeAsStringSync('a'), throwsA(isA())); - expect(() => file.readAsBytesSync(), throwsA(isA())); - expect(() => file.readAsStringSync(), throwsA(isA())); - expect(() => file.readAsLinesSync(), throwsA(isA())); - expect(() => file.existsSync(), throwsA(isA())); - expect(() => file.renameSync('/sync_main_renamed.txt'), throwsA(isA())); - expect(() => file.deleteSync(), throwsA(isA())); - expect(() => file.statSync(), throwsA(isA())); - - final dir = fs.directory('/sync_dir'); - expect(() => dir.createSync(), throwsA(isA())); - expect(() => dir.createTempSync(), throwsA(isA())); - expect(() => dir.deleteSync(), throwsA(isA())); - expect(() => dir.existsSync(), throwsA(isA())); - expect(() => dir.listSync(), throwsA(isA())); - expect(() => dir.renameSync('/sync_dir_renamed'), throwsA(isA())); - expect(() => dir.statSync(), throwsA(isA())); - - final link = fs.link('/sync_link'); - expect(() => link.createSync('/target'), throwsA(isA())); - expect(() => link.updateSync('/new_target'), throwsA(isA())); - expect(() => link.targetSync(), throwsA(isA())); - expect(() => link.renameSync('/sync_link_renamed'), throwsA(isA())); - expect(() => link.deleteSync(), throwsA(isA())); - expect(() => link.existsSync(), throwsA(isA())); - expect(() => link.statSync(), throwsA(isA())); - }); - - group('Mock Worker Environment', () { - setUp(() { - setupSyncMockJS(); - }); - - tearDown(() { - clearSyncMockJS(); - }); - - test('typeSync calls cmd 2 and returns correct types', () { - setMockSyncResponse(utf8.encode('file')); - expect(fs.typeSync('/test_file'), equals(FileSystemEntityType.file)); - expect(getLastSyncCmd(), equals(2)); - - setMockSyncResponse(utf8.encode('directory')); - expect(fs.typeSync('/test_dir'), equals(FileSystemEntityType.directory)); - - setMockSyncResponse(utf8.encode('link')); - expect(fs.typeSync('/test_link'), equals(FileSystemEntityType.link)); - - setMockSyncResponse(utf8.encode('notFound')); - expect(fs.typeSync('/not_found'), equals(FileSystemEntityType.notFound)); - }); - - test('file read and write sync APIs', () { - // existsSync - setMockSyncResponse(utf8.encode('file')); - final file = fs.file('/sync_worker.txt'); - expect(file.existsSync(), isTrue); - expect(getLastSyncCmd(), equals(2)); - - // writeAsBytesSync - setMockSyncResponse(utf8.encode('directory')); // parent typeSync - setMockSyncResponse([]); // writeBytes - file.writeAsBytesSync([10, 20, 30]); - expect(getLastSyncCmd(), equals(4)); - final payload = getLastSyncPayload(); - expect(payload, isNotNull); - // Request format: [pathLen (4)] + [pathBytes] + [bytes] - // path is '/sync_worker.txt' (length 16) - expect(payload![0], equals(16)); - expect(payload.sublist(4, 20), equals(utf8.encode('/sync_worker.txt'))); - expect(payload.sublist(20), equals([10, 20, 30])); - - // writeAsStringSync - setMockSyncResponse(utf8.encode('directory')); // parent typeSync - setMockSyncResponse([]); // writeBytes - file.writeAsStringSync('abc'); - expect(getLastSyncCmd(), equals(4)); - - // readAsBytesSync - setMockSyncResponse(utf8.encode('file')); // typeSync(path) - setMockSyncResponse([12, 34, 56]); // readBytes - expect(file.readAsBytesSync(), equals([12, 34, 56])); - expect(getLastSyncCmd(), equals(3)); - - // readAsStringSync - setMockSyncResponse(utf8.encode('file')); // typeSync(path) - setMockSyncResponse(utf8.encode('hello sync')); // readBytes - expect(file.readAsStringSync(), equals('hello sync')); - - // readAsLinesSync - setMockSyncResponse(utf8.encode('file')); // typeSync(path) - setMockSyncResponse(utf8.encode('line1\nline2\r\nline3')); // readBytes - expect(file.readAsLinesSync(), equals(['line1', 'line2', 'line3'])); - - // deleteSync - setMockSyncResponse(utf8.encode('file')); // typeSync(path) - setMockSyncResponse([]); // delete - file.deleteSync(); - expect(getLastSyncCmd(), equals(6)); - - // statSync - final statJson = json.encode({ - 'type': 'file', - 'size': 1500, - 'modified': 1718100000000, - }); - setMockSyncResponse(utf8.encode(statJson)); - final stat = file.statSync(); - expect(getLastSyncCmd(), equals(9)); - expect(stat.type, equals(FileSystemEntityType.file)); - expect(stat.size, equals(1500)); - expect(stat.modified.millisecondsSinceEpoch, equals(1718100000000)); - }); - - test('directory sync APIs', () { - final dir = fs.directory('/sync_dir'); - - // createSync - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('directory')); // parent typeSync - setMockSyncResponse([]); // createDir - dir.createSync(); - expect(getLastSyncCmd(), equals(5)); - - // deleteSync - setMockSyncResponse(utf8.encode('directory')); // typeSync(path) inside deleteSync - setMockSyncResponse(utf8.encode('directory')); // typeSync(path) inside listSync -> existsSync - setMockSyncResponse(utf8.encode('[]')); // listSync - setMockSyncResponse([]); // delete - dir.deleteSync(); - expect(getLastSyncCmd(), equals(6)); - - // listSync - final listData = json.encode([ - {'path': '/sync_dir/file.txt', 'type': 'file'}, - {'path': '/sync_dir/sub', 'type': 'directory'}, - {'path': '/sync_dir/link', 'type': 'link'}, - ]); - setMockSyncResponse(utf8.encode('directory')); // listSync -> existsSync - setMockSyncResponse(utf8.encode(listData)); // listSync - final list = dir.listSync(followLinks: false); - expect(getLastSyncCmd(), equals(10)); - expect(list.length, equals(3)); - expect(list[0], isA()); - expect(list[0].path, equals('/sync_dir/file.txt')); - expect(list[1], isA()); - expect(list[2], isA()); - }); - - test('link sync APIs', () { - final link = fs.link('/sync_link'); - - // createSync - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('directory')); // parent typeSync - setMockSyncResponse([]); // createLink - link.createSync('/target_path'); - expect(getLastSyncCmd(), equals(7)); - - // updateSync - setMockSyncResponse(utf8.encode('link')); // typeSync check - setMockSyncResponse([]); // updateLink - link.updateSync('/new_target'); - expect(getLastSyncCmd(), equals(13)); - - // targetSync - setMockSyncResponse(utf8.encode('link')); // typeSync check - setMockSyncResponse(utf8.encode('/resolved_target')); // readLink - expect(link.targetSync(), equals('/resolved_target')); - expect(getLastSyncCmd(), equals(8)); - - // deleteSync - setMockSyncResponse(utf8.encode('link')); // typeSync check - setMockSyncResponse([]); // delete - link.deleteSync(); - expect(getLastSyncCmd(), equals(6)); - }); - - test('sync error paths and branch coverage', () { - // 1. SharedArrayBuffer unsupported - jsEval('window.SharedArrayBuffer = undefined;'); - expect(() => fs.typeSync('/'), throwsStateError); - expect(() => fs.statSync('/'), throwsStateError); - jsEval('window.SharedArrayBuffer = function() {};'); // restore - - // 2. typeSync catch block - jsEval(''' - window.originalSendVFSSyncRequest = window.sendVFSSyncRequest; - window.sendVFSSyncRequest = function() { throw new Error("mock error"); }; - '''); - expect(fs.typeSync('/'), equals(FileSystemEntityType.notFound)); - expect(fs.statSync('/').type, equals(FileSystemEntityType.notFound)); - jsEval('window.sendVFSSyncRequest = window.originalSendVFSSyncRequest;'); // restore - - // 3. statSync type branches (directory, link, other/notFound) - // directory - setMockSyncResponse(utf8.encode(json.encode({'type': 'directory', 'size': 0, 'modified': 123}))); - expect(fs.statSync('/dir').type, equals(FileSystemEntityType.directory)); - // link - setMockSyncResponse(utf8.encode(json.encode({'type': 'link', 'size': 0, 'modified': 123}))); - expect(fs.statSync('/link').type, equals(FileSystemEntityType.link)); - // other - setMockSyncResponse(utf8.encode(json.encode({'type': 'unknown', 'size': 0, 'modified': 123}))); - expect(fs.statSync('/unknown').type, equals(FileSystemEntityType.notFound)); - - // 4. WebDirectory createSync branches - final dir = fs.directory('/sync_dir_cov'); - // Already exists - setMockSyncResponse(utf8.encode('directory')); // existsSync - dir.createSync(); // Should return early without exception - - // Parent notFound, recursive: true - setMockSyncResponse(utf8.encode('notFound')); // child existsSync - setMockSyncResponse(utf8.encode('notFound')); // parent typeSync - setMockSyncResponse(utf8.encode('notFound')); // parent existsSync - setMockSyncResponse(utf8.encode('directory')); // parent-parent typeSync - setMockSyncResponse([]); // parent createDir - setMockSyncResponse([]); // child createDir - dir.createSync(recursive: true); - - // Parent notFound, recursive: false throws - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('notFound')); // parent typeSync - expect(() => dir.createSync(recursive: false), throwsA(isA())); - - // Parent is file throws - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('file')); // parent typeSync - expect(() => dir.createSync(), throwsA(isA())); - - // createTempSync when directory notFound throws - setMockSyncResponse(utf8.encode('notFound')); // existsSync - expect(() => dir.createTempSync(), throwsA(isA())); - - // createTempSync successful path - setMockSyncResponse(utf8.encode('directory')); // existsSync - setMockSyncResponse(utf8.encode('notFound')); // temp child existsSync - setMockSyncResponse(utf8.encode('directory')); // temp parent typeSync - setMockSyncResponse([]); // temp createDir - final tempDir = dir.createTempSync('foo'); - expect(tempDir.path, contains('foo')); - - // deleteSync path notFound throws - setMockSyncResponse(utf8.encode('notFound')); // typeSync - expect(() => dir.deleteSync(), throwsA(isA())); - - // deleteSync path is file throws - setMockSyncResponse(utf8.encode('file')); // typeSync - expect(() => dir.deleteSync(), throwsA(isA())); - - // deleteSync not empty throws - setMockSyncResponse(utf8.encode('directory')); // typeSync - setMockSyncResponse(utf8.encode('directory')); // existsSync inside listSync - setMockSyncResponse(utf8.encode(json.encode([{'path': '/sub/a', 'type': 'file'}]))); // listSync - expect(() => dir.deleteSync(), throwsA(isA())); - - // listSync path notFound throws - setMockSyncResponse(utf8.encode('notFound')); // existsSync - expect(() => dir.listSync(), throwsA(isA())); - - // listSync recursive containing a directory - setMockSyncResponse(utf8.encode('directory')); // existsSync - setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/sub', 'type': 'directory'}]))); // listSync level 1 - setMockSyncResponse(utf8.encode('directory')); // existsSync recursive - setMockSyncResponse(utf8.encode('[]')); // listSync level 2 - final listRec = dir.listSync(recursive: true); - expect(listRec.length, equals(1)); - - // listSync following link target to directory - setMockSyncResponse(utf8.encode('directory')); // existsSync - setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync - setMockSyncResponse(utf8.encode('directory')); // typeSync for link target - final listLnkDir = dir.listSync(followLinks: true); - expect(listLnkDir.length, equals(1)); - expect(listLnkDir[0], isA()); - - // listSync following link target to file (non-directory) - setMockSyncResponse(utf8.encode('directory')); // existsSync - setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync - setMockSyncResponse(utf8.encode('file')); // typeSync for link target - final listLnkFile = dir.listSync(followLinks: true); - expect(listLnkFile.length, equals(1)); - expect(listLnkFile[0], isA()); - - // listSync following link target throws/notFound - setMockSyncResponse(utf8.encode('directory')); // existsSync - setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync - setMockSyncResponse(utf8.encode('notFound')); // typeSync for link target - final listLnkNotFound = dir.listSync(followLinks: true); - expect(listLnkNotFound.length, equals(1)); - expect(listLnkNotFound[0], isA()); - - // listSync recursive following link target to directory - setMockSyncResponse(utf8.encode('directory')); // existsSync - setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync level 1 - setMockSyncResponse(utf8.encode('directory')); // typeSync for link target - setMockSyncResponse(utf8.encode('directory')); // existsSync recursive - setMockSyncResponse(utf8.encode('[]')); // listSync level 2 - final listRecLnkDir = dir.listSync(recursive: true, followLinks: true); - expect(listRecLnkDir.length, equals(1)); - expect(listRecLnkDir[0], isA()); - - // listSync following link target where typeSync throws (covered via SAB disabled mid-run) - jsEval(''' - window.originalSendVFSSyncRequest = window.sendVFSSyncRequest; - window.sendVFSSyncRequest = function(cmd, req) { - if (cmd === 10) { - window.SharedArrayBuffer = undefined; // disable SAB - const listData = JSON.stringify([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]); - const bytes = new TextEncoder().encode(listData); - return bytes; - } - return window.originalSendVFSSyncRequest(cmd, req); - }; - '''); - setMockSyncResponse(utf8.encode('directory')); // existsSync - // listSync response is provided by custom sendVFSSyncRequest function - final listLnkThrow = dir.listSync(followLinks: true); - expect(listLnkThrow.length, equals(1)); - expect(listLnkThrow[0], isA()); - jsEval(''' - window.sendVFSSyncRequest = window.originalSendVFSSyncRequest; - window.SharedArrayBuffer = function() {}; // restore SAB - '''); - - // makeSyncCall SharedArrayBuffer unsupported - jsEval('window.SharedArrayBuffer = undefined;'); - expect(() => dir.resolveSymbolicLinksSync(), throwsStateError); - jsEval('window.SharedArrayBuffer = function() {};'); // restore - - // renameSync original path not found throws - setMockSyncResponse(utf8.encode('notFound')); // typeSync - expect(() => dir.renameSync('/new'), throwsA(isA())); - - // renameSync new parent not directory throws - setMockSyncResponse(utf8.encode('directory')); // typeSync self - setMockSyncResponse(utf8.encode('file')); // typeSync parent - expect(() => dir.renameSync('/parent_file/new'), throwsA(isA())); - - // renameSync successful - setMockSyncResponse(utf8.encode('directory')); // typeSync self - setMockSyncResponse(utf8.encode('directory')); // typeSync parent - setMockSyncResponse([]); // rename - final renamedDir = dir.renameSync('/new_parent/new_dir'); - expect(renamedDir.path, equals('/new_parent/new_dir')); - - // resolveSymbolicLinksSync - setMockSyncResponse(utf8.encode('/resolved/path')); - expect(dir.resolveSymbolicLinksSync(), equals('/resolved/path')); - - // 5. WebFile sync branches - final file = fs.file('/sync_file_cov'); - // createSync already exists, exclusive: true throws - setMockSyncResponse(utf8.encode('file')); // existsSync - expect(() => file.createSync(exclusive: true), throwsA(isA())); - - // createSync already exists, exclusive: false returns early - setMockSyncResponse(utf8.encode('file')); // existsSync - file.createSync(exclusive: false); // returns early - - // createSync parent notFound, recursive: false throws - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('notFound')); // parent typeSync - expect(() => file.createSync(), throwsA(isA())); - - // createSync parent notFound, recursive: true - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('notFound')); // parent typeSync - setMockSyncResponse(utf8.encode('notFound')); // parent existsSync - setMockSyncResponse(utf8.encode('directory')); // parent-parent typeSync - setMockSyncResponse([]); // parent createDir - setMockSyncResponse(utf8.encode('directory')); // parent check for writeBytes - setMockSyncResponse([]); // writeBytes (create file) - file.createSync(recursive: true); - - // createSync parent is file throws - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('file')); // parent typeSync - expect(() => file.createSync(), throwsA(isA())); - - // writeAsBytesSync FileMode.append throws UnsupportedError - expect(() => file.writeAsBytesSync([], mode: FileMode.append), throwsUnsupportedError); - - // writeAsBytesSync parent not directory throws - setMockSyncResponse(utf8.encode('file')); // parent typeSync - expect(() => file.writeAsBytesSync([]), throwsA(isA())); - - // readAsBytesSync path notFound throws - setMockSyncResponse(utf8.encode('notFound')); // typeSync - expect(() => file.readAsBytesSync(), throwsA(isA())); - - // readAsBytesSync path is directory throws - setMockSyncResponse(utf8.encode('directory')); // typeSync - expect(() => file.readAsBytesSync(), throwsA(isA())); - - // renameSync path notFound throws - setMockSyncResponse(utf8.encode('notFound')); // typeSync self - expect(() => file.renameSync('/new'), throwsA(isA())); - - // renameSync parent not directory throws - setMockSyncResponse(utf8.encode('file')); // typeSync self - setMockSyncResponse(utf8.encode('file')); // typeSync parent - expect(() => file.renameSync('/parent_file/new'), throwsA(isA())); - - // renameSync successful - setMockSyncResponse(utf8.encode('file')); // typeSync self - setMockSyncResponse(utf8.encode('directory')); // typeSync parent - setMockSyncResponse([]); // rename - final renamedFile = file.renameSync('/new_parent/new_file'); - expect(renamedFile.path, equals('/new_parent/new_file')); - - // deleteSync path notFound throws - setMockSyncResponse(utf8.encode('notFound')); // typeSync self - expect(() => file.deleteSync(), throwsA(isA())); - - // 6. WebLink sync branches - final link = fs.link('/sync_link_cov'); - // createSync already exists throws - setMockSyncResponse(utf8.encode('link')); // existsSync - expect(() => link.createSync('/target'), throwsA(isA())); - - // createSync parent notFound, recursive: false throws - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('notFound')); // parent typeSync - expect(() => link.createSync('/target'), throwsA(isA())); - - // createSync parent notFound, recursive: true - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('notFound')); // parent typeSync - setMockSyncResponse(utf8.encode('notFound')); // parent existsSync - setMockSyncResponse(utf8.encode('directory')); // parent-parent typeSync - setMockSyncResponse([]); // parent createDir - setMockSyncResponse([]); // createLink - link.createSync('/target', recursive: true); - - // createSync parent is file throws - setMockSyncResponse(utf8.encode('notFound')); // existsSync - setMockSyncResponse(utf8.encode('file')); // parent typeSync - expect(() => link.createSync('/target'), throwsA(isA())); - - // updateSync path notFound throws - setMockSyncResponse(utf8.encode('notFound')); // typeSync self - expect(() => link.updateSync('/new'), throwsA(isA())); - - // updateSync path is not link throws - setMockSyncResponse(utf8.encode('file')); // typeSync self - expect(() => link.updateSync('/new'), throwsA(isA())); - - // targetSync path notFound throws - setMockSyncResponse(utf8.encode('notFound')); // typeSync self - expect(() => link.targetSync(), throwsA(isA())); - - // targetSync path is not link throws - setMockSyncResponse(utf8.encode('file')); // typeSync self - expect(() => link.targetSync(), throwsA(isA())); - - // renameSync path notFound throws - setMockSyncResponse(utf8.encode('notFound')); // typeSync self - expect(() => link.renameSync('/new'), throwsA(isA())); - - // renameSync parent not directory throws - setMockSyncResponse(utf8.encode('link')); // typeSync self - setMockSyncResponse(utf8.encode('file')); // typeSync parent - expect(() => link.renameSync('/parent_file/new'), throwsA(isA())); - - // renameSync successful - setMockSyncResponse(utf8.encode('link')); // typeSync self - setMockSyncResponse(utf8.encode('directory')); // typeSync parent - setMockSyncResponse([]); // rename - final renamedLink = link.renameSync('/new_parent/new_link'); - expect(renamedLink.path, equals('/new_parent/new_link')); - - // deleteSync path notFound throws - setMockSyncResponse(utf8.encode('notFound')); // typeSync self - expect(() => link.deleteSync(), throwsA(isA())); - }); - }); - }); -} - -@JS('eval') -external JSAny? jsEval(String code); - -void injectMockJS() { - jsEval(''' - window.mockIDBOpenShouldFail = false; - window.mockIDBGetShouldFail = false; - window.mockIDBPutShouldFail = false; - window.mockIDBIndexGetShouldFail = false; - - if (!window.hasInjectedIDBMocks) { - window.hasInjectedIDBMocks = true; - - const originalOpen = IDBFactory.prototype.open; - IDBFactory.prototype.open = function(name, version) { - if (window.mockIDBOpenShouldFail) { - const mockRequest = { - set onsuccess(fn) { this._onsuccess = fn; }, - set onerror(fn) { this._onerror = fn; }, - set onupgradeneeded(fn) { this._onupgradeneeded = fn; }, - get target() { return this; } - }; - setTimeout(() => { - if (mockRequest._onerror) { - mockRequest._onerror({ target: mockRequest }); - } - }, 0); - return mockRequest; - } - return originalOpen.apply(this, arguments); - }; - - const originalGet = IDBObjectStore.prototype.get; - IDBObjectStore.prototype.get = function(key) { - if (window.mockIDBGetShouldFail) { - const mockRequest = { - set onsuccess(fn) { this._onsuccess = fn; }, - set onerror(fn) { this._onerror = fn; }, - get target() { return this; } - }; - setTimeout(() => { - if (mockRequest._onerror) { - mockRequest._onerror({ target: mockRequest }); - } - }, 0); - return mockRequest; - } - return originalGet.apply(this, arguments); - }; - - const originalPut = IDBObjectStore.prototype.put; - IDBObjectStore.prototype.put = function(value) { - if (window.mockIDBPutShouldFail) { - const mockRequest = { - set onsuccess(fn) { this._onsuccess = fn; }, - set onerror(fn) { this._onerror = fn; }, - get target() { return this; } - }; - setTimeout(() => { - if (mockRequest._onerror) { - mockRequest._onerror({ target: mockRequest }); - } - }, 0); - return mockRequest; - } - return originalPut.apply(this, arguments); - }; - - const originalIndexGet = IDBIndex.prototype.get; - IDBIndex.prototype.get = function(key) { - if (window.mockIDBIndexGetShouldFail) { - const mockRequest = { - set onsuccess(fn) { this._onsuccess = fn; }, - set onerror(fn) { this._onerror = fn; }, - get target() { return this; } - }; - setTimeout(() => { - if (mockRequest._onerror) { - mockRequest._onerror({ target: mockRequest }); - } - }, 0); - return mockRequest; - } - return originalIndexGet.apply(this, arguments); - }; - } - '''); -} - -void setMockIDBOpenShouldFail(bool value) { - jsEval('window.mockIDBOpenShouldFail = $value;'); -} -void setMockIDBGetShouldFail(bool value) { - jsEval('window.mockIDBGetShouldFail = $value;'); -} -void setMockIDBPutShouldFail(bool value) { - jsEval('window.mockIDBPutShouldFail = $value;'); -} -void setMockIDBIndexGetShouldFail(bool value) { - jsEval('window.mockIDBIndexGetShouldFail = $value;'); -} - -void setupSyncMockJS() { - jsEval(''' - window.originalImportScripts = window.importScripts; - window.importScripts = function() {}; - window.originalSharedArrayBuffer = window.SharedArrayBuffer; - window.SharedArrayBuffer = function() {}; - window.isVFSSyncWorkerInitialized = true; - window.initVFSSyncWorker = function() {}; - - window.lastSyncCmd = null; - window.lastSyncPayload = null; - window.mockSyncResponseArray = []; - window.syncCmdsHistory = []; - - window.sendVFSSyncRequest = function(cmd, requestBytes) { - window.lastSyncCmd = cmd; - window.syncCmdsHistory.push(cmd); - window.lastSyncPayload = Array.from(requestBytes); - const resp = window.mockSyncResponseArray.shift() || new Uint8Array(0); - return resp; - }; - '''); -} - -void clearSyncMockJS() { - jsEval(''' - window.importScripts = window.originalImportScripts; - window.SharedArrayBuffer = window.originalSharedArrayBuffer; - delete window.sendVFSSyncRequest; - delete window.lastSyncCmd; - delete window.lastSyncPayload; - delete window.mockSyncResponseArray; - delete window.syncCmdsHistory; - delete window.isVFSSyncWorkerInitialized; - delete window.initVFSSyncWorker; - '''); -} - -List getSyncCmdsHistory() { - final jsVal = jsEval('JSON.stringify(window.syncCmdsHistory || [])'); - if (jsVal == null) return []; - return List.from(json.decode((jsVal as JSString).toDart)); -} - -int? getLastSyncCmd() { - final jsVal = jsEval('window.lastSyncCmd'); - if (jsVal == null) return null; - return (jsVal as JSNumber).toDartInt; -} - -List? getLastSyncPayload() { - final jsVal = jsEval('window.lastSyncPayload ? JSON.stringify(window.lastSyncPayload) : null'); - if (jsVal == null) return null; - return List.from(json.decode((jsVal as JSString).toDart)); -} - -void setMockSyncResponse(List bytes) { - final jsonBytes = json.encode(bytes); - jsEval('window.mockSyncResponseArray.push(new Uint8Array($jsonBytes));'); }