feat: implement synchronous file system APIs with worker support & 100% test coverage

This commit is contained in:
2026-06-07 17:32:22 -07:00
parent fcc1a194c4
commit afb6302f0d
8 changed files with 1715 additions and 55 deletions
@@ -1,10 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'dart:js_interop';
import 'package:file/file.dart';
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 'entities/web_directory.dart';
import 'entities/web_file.dart';
import 'entities/web_link.dart';
@@ -60,9 +63,43 @@ 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}) {
throw UnsupportedError('Sync type not supported');
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;
}
}
// Internal Resolution Logic
@@ -176,7 +213,34 @@ class WebFileSystem extends FileSystem {
@override
FileStat statSync(String path) {
throw UnsupportedError('Sync stat not supported');
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<String, dynamic>;
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);
}
}
FileSystemEntityType _getType(int nodeType) {
@@ -188,15 +252,15 @@ class WebFileSystem extends FileSystem {
@override
bool isFileSync(String path) =>
throw UnsupportedError('Sync isFile not supported');
typeSync(path) == FileSystemEntityType.file;
@override
bool isDirectorySync(String path) =>
throw UnsupportedError('Sync isDirectory not supported');
typeSync(path) == FileSystemEntityType.directory;
@override
bool isLinkSync(String path) =>
throw UnsupportedError('Sync isLink not supported');
typeSync(path, followLinks: false) == FileSystemEntityType.link;
@override
Future<bool> isFile(String path) async =>
@@ -226,8 +290,15 @@ class WebFileSystem extends FileSystem {
}
@override
bool identicalSync(String path1, String path2) =>
throw UnsupportedError('Sync not supported');
bool identicalSync(String path1, String path2) {
try {
final r1 = resolveSymbolicLinksSync(path1);
final r2 = resolveSymbolicLinksSync(path2);
return r1 == r2;
} catch (_) {
return false;
}
}
Future<String> resolveSymbolicLinks(String pathStr) async {
final inode = await resolvepath(pathStr, followLinks: true);
@@ -240,6 +311,11 @@ class WebFileSystem extends FileSystem {
if (segments.isEmpty) return '/';
return '/' + segments.reversed.join('/');
}
String resolveSymbolicLinksSync(String pathStr) {
final respBytes = makeSyncCall(11, utf8.encode(pathStr));
return utf8.decode(respBytes);
}
}
class FileStatImpl implements FileStat {