└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Problems with this proposal 2 | 3 | * The locking in this proposal is a problem in that it's mandatory, and locks don't work across multiple reads & writes. Locking should be optional and independent. Whether they should be opt-in or opt-out is another issue. [#8](https://github.com/jakearchibald/byte-storage/issues/8). 4 | * A lower-level may be sync access in a worker, similar to mmap. [#4](https://github.com/jakearchibald/byte-storage/issues/4). 5 | 6 | ---- 7 | 8 | # Byte storage 9 | 10 | The aim is to provide a low-level disk-backed storage system. 11 | 12 | A measure of success would be being able to use this API to create a custom disk-backed data store, such as sqlite. 13 | 14 | ## API 15 | 16 | ```ts 17 | self.byteStorage:ByteStorage; 18 | ``` 19 | 20 | The entry point for the API. 21 | 22 | ### Reading from a byte store 23 | 24 | ```ts 25 | const readable:ReadableStream = await byteStorage.read(name:String, { 26 | start:Number = 0, 27 | end:Number 28 | }); 29 | ``` 30 | 31 | * `name` - the identifier of the store. This can be any string. `/` has no special meaning. 32 | * `start` - start point within the store in bytes. If negative, treated as an offset from the end of the store. 33 | * `end` - end point within the store in bytes. If negative, treated as an offset from the end of the store. If not provided, treated as the end of the store. 34 | 35 | Resolves once: 36 | 37 | * All write locks within intersecting byte ranges are released. 38 | * A read lock is granted for the start-end. 39 | 40 | Rejects if: 41 | 42 | * `name` is not an existing byte store. 43 | * The computed start point is less than 0. 44 | * The computed start point is greater than the length of the byte store. 45 | * The computed end point is less than 0. 46 | * The computed end point is greater than the length of the byte store. 47 | * The computed end point is less than the start point. 48 | 49 | `readable` is a `ReadableString` with a underlying byte source. Non-BYOB reads produce `UInt8Array`s. 50 | 51 | The read lock is released once the `readable` is closed. 52 | 53 | ### Writing to a byte store 54 | 55 | ```ts 56 | const writable:WritableStream = await byteStorage.write(name:String, { 57 | start:Number = 0, 58 | end:Number 59 | }); 60 | ``` 61 | 62 | * `name` - the identifier of the store. 63 | * `start` - start point within the store in bytes. If negative, treated as an offset from the end of the store. 64 | * `end` - end point within the store in bytes. If negative, treated as an offset from the end of the store. If not provided, the store may continue to write beyond its current length, increasing its size. 65 | 66 | Resolves once: 67 | 68 | * The byte store entry is created, if not already. 69 | * The space is allocated (zeroed), if `end` is provided and its computed value is greater than the length of the current store. 70 | * The space is allocated (zeroed), if the computed start is greater than the length of the current store. 71 | * All read and write locks for intersecting byte ranges are released. 72 | * A write lock for the start-end (or end of the store) is granted. 73 | 74 | Rejects if: 75 | 76 | * The computed start point is less than 0. 77 | * The computed end point is less than 0. 78 | * The space cannot be allocated. 79 | 80 | `writable` accepts chunks of `ArrayBuffer` or `ArrayBufferView`. Writing will error if additional allocation fails (this can only happen if `end` was not provided). 81 | 82 | The `writable` will close once if `end` is provided, and `end - start` bytes have been queued. 83 | 84 | If more than `end - start` bytes are queued, the writable errors. 85 | 86 | The write lock is released once the `writable` is closed. 87 | 88 | ### Transforming a byte store 89 | 90 | ```ts 91 | byteStorage.transform(name:String, { 92 | readable:ReadableStream, 93 | writable:WritableStream 94 | }, { 95 | start:Number = 0, 96 | end:Number 97 | }); 98 | ``` 99 | 100 | This functions the same as `.write` except: 101 | 102 | * Writes to the writable are buffered if they're beyond the current read point (unless it's the end of the store). This means if you read 1 byte, and write 2, the next read in the transform will not include your written byte. 103 | * Rejects if store `name` doesn't exist. 104 | * If the readable closes before `end - start` bytes are queued, the writable closes. TODO: should we leave untouched bytes alone in this case? 105 | * If the readable errors, then any already-written bytes are retained, as in this is not a transactional system, it won't undo changes so far. 106 | 107 | ### Retrieving metadata on a byte store 108 | 109 | ```ts 110 | const data = await byteStorage.status(name:String); 111 | 112 | const { 113 | size:Number, 114 | created:Date, 115 | modified:Date 116 | } = data; 117 | ``` 118 | 119 | * `name` - the identifier of the store. 120 | 121 | Resolves once: 122 | 123 | * All write locks for the store are released. TODO: or shall we just return the information we have, which may include half-written data? 124 | 125 | `data` is `null` if the store does not exist. 126 | 127 | ### Resizing a byte store 128 | 129 | ```ts 130 | await byteStorage.resize(name:String, end:Number); 131 | ``` 132 | 133 | * `name` - the identifier of the store. 134 | * `end` - end point within the store in bytes. If negative, treated as an offset from the end of the store. 135 | 136 | Resolves once: 137 | 138 | * The space is allocated (zeroed), if `end` is greater than the length of the current store. 139 | * The space is allocated (zeroed), if the computed start is greater than the length of the current store. 140 | * All read and write locks for `end` until the end of the resource are released. 141 | * A write lock for `end` until the end of the resource is granted. 142 | * The space is allocated/deallocated. 143 | 144 | Rejects if: 145 | 146 | * `name` is not an existing byte store. 147 | * The computed end point is less than 0. 148 | * The space cannot be allocated. 149 | 150 | ### Deleting a byte store 151 | 152 | ```ts 153 | const existed:Boolean = await byteStorage.delete(name:String); 154 | ``` 155 | 156 | Resolves once: 157 | 158 | * All read and write locks for intersecting byte ranges are released. 159 | * A write lock for the start-end (or end of the store) is granted. 160 | * The store is unlinked. 161 | 162 | TODO: or should we be more agressive here, and error current reads & writes? 163 | 164 | ### Getting all store names 165 | 166 | We could add a `.keys()` method, or just use async iterators. 167 | 168 | ### Helpers for simple reads and writes? 169 | 170 | ```js 171 | const data:UInt8Array = await byteStorage.readAll(name:String, { 172 | start:Number = 0, 173 | end:Number 174 | }); 175 | 176 | await byteStorage.writeAll(name:String, data, { 177 | start:Number = 0 178 | }); 179 | ``` 180 | 181 | TODO: Do we need methods like above for making simple reads & writes? 182 | 183 | ## Issues 184 | 185 | ### Permalock 186 | 187 | ```js 188 | await byteStorage.write('foo'); 189 | ``` 190 | 191 | The above locks the whole of `"foo"` until the client closes. We could work around this by: 192 | 193 | * Adding timeouts. 194 | * Provide a method to discard existing locks (erroring the related open streams). 195 | 196 | ### Multi-action locking 197 | 198 | Do we need an API to create locks independent of particular actions. Eg: 199 | 200 | * I have a 500 byte store containing PNG data. I want to lock the whole store while I compress the data, which includes reading, writing, and hopefully truncating. 201 | * I am transforming some data, but I'm also buffering what I read. If my write errors, I want to write back what I originally read within the same lock, effectively undoing the partial transform. 202 | 203 | # Examples 204 | 205 | ## Writing a fetch response into byte storage 206 | 207 | ```js 208 | (async function() { 209 | const response = await fetch(url); 210 | const opts = {}; 211 | 212 | if (response.headers.get('Content-Length')) { 213 | opts.end = Number(response.headers.get('Content-Length')); 214 | } 215 | 216 | await response.body.pipeTo( 217 | await byteStorage.write('some-data', opts) 218 | ); 219 | })(); 220 | ``` 221 | 222 | ## Reading number of data chunks in a custom structure 223 | 224 | Imagine a data structure that was an unsigned long, and then a set of data of length specified by that long (in bytes). The sequence ends with a unsigned long equalling zero. 225 | 226 | ```js 227 | async function itemsInStructure() { 228 | let start = 0; 229 | let num = 0; 230 | 231 | while (true) { 232 | const data = await byteStorage.readAll('data-structure', {start, end: start + 4}); 233 | const nextChunkLen = new Uint32Array(data.buffer)[0]; 234 | if (nextChunkLen === 0) return num; 235 | num++; 236 | start += 4 + nextChunkLen; 237 | } 238 | } 239 | ``` 240 | 241 | This example is one that would benefit from a single lock across multiple reads. 242 | --------------------------------------------------------------------------------