collecting-file-system-observer.js (5013B)
1 // Wraps a FileSystemObserver to collect its records until it stops receiving 2 // them. 3 // 4 // To collect records, it sets up a directory to observe and periodically create 5 // files in it. If no new changes occur (outside of these file creations) 6 // between two file changes, then it resolves the promise returned by 7 // getRecords() with the records it collected. 8 class CollectingFileSystemObserver { 9 #observer = new FileSystemObserver(this.#collectRecordsCallback.bind(this)); 10 #notificationObserver = 11 new FileSystemObserver(this.#notificationCallback.bind(this)); 12 13 #callback; 14 15 #records_promise_and_resolvers = Promise.withResolvers(); 16 #collected_records = []; 17 18 #notification_dir_handle; 19 #notification_file_count = 0; 20 #received_changes_since_last_notification = true; 21 22 constructor(test, root_dir, callback) { 23 test.add_cleanup(() => { 24 this.disconnect(); 25 this.#notificationObserver.disconnect(); 26 }); 27 28 this.#setupCollectNotification(root_dir); 29 this.#callback = callback ?? (() => {return {}}); 30 } 31 32 #getCollectNotificationName() { 33 return `notification_file_${this.#notification_file_count}`; 34 } 35 36 async #setupCollectNotification(root_dir) { 37 this.#notification_dir_handle = 38 await root_dir.getDirectoryHandle(getUniqueName(), {create: true}); 39 await this.#notificationObserver.observe(this.#notification_dir_handle); 40 await this.#createCollectNotification(); 41 } 42 43 #createCollectNotification() { 44 this.#notification_file_count++; 45 return this.#notification_dir_handle.getFileHandle( 46 this.#getCollectNotificationName(), {create: true}); 47 } 48 49 #finishCollectingIfReady() { 50 // `records` contains the notification for collecting records. Determine 51 // if we should finish collecting or create the next notification. 52 if (this.#received_changes_since_last_notification) { 53 this.#received_changes_since_last_notification = false; 54 this.#createCollectNotification(); 55 } else { 56 this.#records_promise_and_resolvers.resolve(this.#collected_records); 57 } 58 } 59 60 #notificationCallback(records) { 61 this.#finishCollectingIfReady(records); 62 } 63 64 #collectRecordsCallback(records, observer) { 65 this.#collected_records.push({ 66 ...this.#callback(records, observer), 67 records, 68 }); 69 70 this.#received_changes_since_last_notification = true; 71 } 72 73 async getRecords() { 74 return (await this.#records_promise_and_resolvers.promise) 75 .map(record => record.records) 76 .flat(); 77 } 78 79 getRecordsWithCallbackInfo() { 80 return this.#records_promise_and_resolvers.promise; 81 } 82 83 observe(handles, options) { 84 return Promise.all( 85 handles.map(handle => this.#observer.observe(handle, options))); 86 } 87 88 disconnect() { 89 this.#observer.disconnect(); 90 } 91 } 92 93 async function assert_records_equal(root, actual, expected) { 94 assert_equals( 95 actual.length, expected.length, 96 'Received an unexpected number of events'); 97 98 for (let i = 0; i < actual.length; i++) { 99 const actual_record = actual[i]; 100 const expected_record = expected[i]; 101 102 assert_equals( 103 actual_record.type, expected_record.type, 104 'A record\'s type didn\'t match the expected type'); 105 106 assert_array_equals( 107 actual_record.relativePathComponents, 108 expected_record.relativePathComponents, 109 'A record\'s relativePathComponents didn\'t match the expected relativePathComponents'); 110 111 if (expected_record.relativePathMovedFrom) { 112 assert_array_equals( 113 actual_record.relativePathMovedFrom, 114 expected_record.relativePathMovedFrom, 115 'A record\'s relativePathMovedFrom didn\'t match the expected relativePathMovedFrom'); 116 } else { 117 assert_equals( 118 actual_record.relativePathMovedFrom, null, 119 'A record\'s relativePathMovedFrom was set when it shouldn\'t be'); 120 } 121 122 if (expected_record.changedHandle) { 123 assert_true( 124 await actual_record.changedHandle.isSameEntry( 125 expected_record.changedHandle), 126 'A record\'s changedHandle didn\'t match the expected changedHandle'); 127 } else { 128 assert_equals( 129 actual_record.changedHandle, null, 130 'A record\'s changedHandle was set when it shouldn\'t be'); 131 } 132 133 assert_true( 134 await actual_record.root.isSameEntry(root), 135 'A record\'s root didn\'t match the expected root'); 136 } 137 } 138 139 function modifiedEvent(changedHandle, relativePathComponents) { 140 return {type: 'modified', changedHandle, relativePathComponents}; 141 } 142 143 function appearedEvent(changedHandle, relativePathComponents) { 144 return {type: 'appeared', changedHandle, relativePathComponents}; 145 } 146 147 function disappearedEvent(relativePathComponents) { 148 return {type: 'disappeared', changedHandle: null, relativePathComponents}; 149 } 150 151 function movedEvent( 152 changedHandle, relativePathComponents, relativePathMovedFrom) { 153 return { 154 type: 'moved', 155 changedHandle, 156 relativePathComponents, 157 relativePathMovedFrom 158 }; 159 }