Skip to content

Commit d1a158f

Browse files
authored
fix: separate createWritable polyfill (#74)
1 parent 333fd33 commit d1a158f

File tree

2 files changed

+183
-182
lines changed

2 files changed

+183
-182
lines changed

src/FileSystemFileHandle.js

Lines changed: 1 addition & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import FileSystemHandle from './FileSystemHandle.js'
22
import FileSystemWritableFileStream from './FileSystemWritableFileStream.js'
3-
import { errors } from './util.js'
4-
5-
const { INVALID, SYNTAX, GONE } = errors
3+
import './createWritable.js'
64

75
const kAdapter = Symbol('adapter')
86

@@ -46,184 +44,5 @@ Object.defineProperties(FileSystemFileHandle.prototype, {
4644
getFile: { enumerable: true }
4745
})
4846

49-
// Safari doesn't support async createWritable streams yet.
50-
if (
51-
globalThis.FileSystemFileHandle &&
52-
!globalThis.FileSystemFileHandle.prototype.createWritable
53-
) {
54-
const wm = new WeakMap()
55-
56-
let workerUrl
57-
58-
// Worker code that should be inlined (can't use any external functions)
59-
const code = () => {
60-
let fileHandle, handle
61-
62-
onmessage = async evt => {
63-
const port = evt.ports[0]
64-
const cmd = evt.data
65-
switch (cmd.type) {
66-
case 'open':
67-
const file = cmd.name
68-
69-
let dir = await navigator.storage.getDirectory()
70-
71-
for (const folder of cmd.path) {
72-
dir = await dir.getDirectoryHandle(folder)
73-
}
74-
75-
fileHandle = await dir.getFileHandle(file)
76-
handle = await fileHandle.createSyncAccessHandle()
77-
break
78-
case 'write':
79-
handle.write(cmd.data, { at: cmd.position })
80-
handle.flush()
81-
break
82-
case 'truncate':
83-
handle.truncate(cmd.size)
84-
break
85-
case 'abort':
86-
case 'close':
87-
handle.close()
88-
break
89-
}
90-
91-
port.postMessage(0)
92-
}
93-
}
94-
95-
96-
globalThis.FileSystemFileHandle.prototype.createWritable = async function (options) {
97-
// Safari only support writing data in a worker with sync access handle.
98-
if (!workerUrl) {
99-
const stringCode = `(${code.toString()})()`
100-
const blob = new Blob([stringCode], {
101-
type: 'text/javascript'
102-
})
103-
workerUrl = URL.createObjectURL(blob)
104-
}
105-
const worker = new Worker(workerUrl, { type: 'module' })
106-
107-
let position = 0
108-
const textEncoder = new TextEncoder()
109-
let size = await this.getFile().then(file => file.size)
110-
111-
const send = message => new Promise((resolve, reject) => {
112-
const mc = new MessageChannel()
113-
mc.port1.onmessage = evt => {
114-
if (evt.data instanceof Error) reject(evt.data)
115-
else resolve(evt.data)
116-
mc.port1.close()
117-
mc.port2.close()
118-
mc.port1.onmessage = null
119-
}
120-
worker.postMessage(message, [mc.port2])
121-
})
122-
123-
// Safari also don't support transferable file system handles.
124-
// So we need to pass the path to the worker. This is a bit hacky and ugly.
125-
const root = await navigator.storage.getDirectory()
126-
const parent = await wm.get(this)
127-
const path = await root.resolve(parent)
128-
129-
// Should likely never happen, but just in case...
130-
if (path === null) throw new DOMException(...GONE)
131-
132-
let controller
133-
await send({ type: 'open', path, name: this.name })
134-
135-
if (options?.keepExistingData === false) {
136-
await send({ type: 'truncate', size: 0 })
137-
size = 0
138-
}
139-
140-
const ws = new FileSystemWritableFileStream({
141-
start: ctrl => {
142-
controller = ctrl
143-
},
144-
async write(chunk) {
145-
const isPlainObject = chunk?.constructor === Object
146-
147-
if (isPlainObject) {
148-
chunk = { ...chunk }
149-
} else {
150-
chunk = { type: 'write', data: chunk, position }
151-
}
152-
153-
if (chunk.type === 'write') {
154-
if (!('data' in chunk)) {
155-
await send({ type: 'close' })
156-
throw new DOMException(...SYNTAX('write requires a data argument'))
157-
}
158-
159-
chunk.position ??= position
160-
161-
if (typeof chunk.data === 'string') {
162-
chunk.data = textEncoder.encode(chunk.data)
163-
}
164-
165-
else if (chunk.data instanceof ArrayBuffer) {
166-
chunk.data = new Uint8Array(chunk.data)
167-
}
168-
169-
else if (!(chunk.data instanceof Uint8Array) && ArrayBuffer.isView(chunk.data)) {
170-
chunk.data = new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength)
171-
}
172-
173-
else if (!(chunk.data instanceof Uint8Array)) {
174-
const ab = await new Response(chunk.data).arrayBuffer()
175-
chunk.data = new Uint8Array(ab)
176-
}
177-
178-
if (Number.isInteger(chunk.position) && chunk.position >= 0) {
179-
position = chunk.position
180-
}
181-
position += chunk.data.byteLength
182-
size += chunk.data.byteLength
183-
} else if (chunk.type === 'seek') {
184-
if (Number.isInteger(chunk.position) && chunk.position >= 0) {
185-
if (size < chunk.position) {
186-
throw new DOMException(...INVALID)
187-
}
188-
console.log('seeking', chunk)
189-
position = chunk.position
190-
return // Don't need to enqueue seek...
191-
} else {
192-
await send({ type: 'close' })
193-
throw new DOMException(...SYNTAX('seek requires a position argument'))
194-
}
195-
} else if (chunk.type === 'truncate') {
196-
if (Number.isInteger(chunk.size) && chunk.size >= 0) {
197-
size = chunk.size
198-
if (position > size) { position = size }
199-
} else {
200-
await send({ type: 'close' })
201-
throw new DOMException(...SYNTAX('truncate requires a size argument'))
202-
}
203-
}
204-
205-
await send(chunk)
206-
},
207-
async close () {
208-
await send({ type: 'close' })
209-
worker.terminate()
210-
},
211-
async abort (reason) {
212-
await send({ type: 'abort', reason })
213-
worker.terminate()
214-
},
215-
})
216-
217-
return ws
218-
}
219-
220-
const orig = FileSystemDirectoryHandle.prototype.getFileHandle
221-
FileSystemDirectoryHandle.prototype.getFileHandle = async function (...args) {
222-
const handle = await orig.call(this, ...args)
223-
wm.set(handle, this)
224-
return handle
225-
}
226-
}
227-
22847
export default FileSystemFileHandle
22948
export { FileSystemFileHandle }

src/createWritable.js

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { errors } from './util.js'
2+
3+
const { INVALID, SYNTAX, GONE } = errors
4+
5+
// Safari doesn't support async createWritable streams yet.
6+
if (
7+
globalThis.FileSystemFileHandle &&
8+
!globalThis.FileSystemFileHandle.prototype.createWritable
9+
) {
10+
const wm = new WeakMap()
11+
12+
let workerUrl
13+
14+
// Worker code that should be inlined (can't use any external functions)
15+
const code = () => {
16+
let fileHandle, handle
17+
18+
onmessage = async evt => {
19+
const port = evt.ports[0]
20+
const cmd = evt.data
21+
switch (cmd.type) {
22+
case 'open':
23+
const file = cmd.name
24+
25+
let dir = await navigator.storage.getDirectory()
26+
27+
for (const folder of cmd.path) {
28+
dir = await dir.getDirectoryHandle(folder)
29+
}
30+
31+
fileHandle = await dir.getFileHandle(file)
32+
handle = await fileHandle.createSyncAccessHandle()
33+
break
34+
case 'write':
35+
handle.write(cmd.data, { at: cmd.position })
36+
handle.flush()
37+
break
38+
case 'truncate':
39+
handle.truncate(cmd.size)
40+
break
41+
case 'abort':
42+
case 'close':
43+
handle.close()
44+
break
45+
}
46+
47+
port.postMessage(0)
48+
}
49+
}
50+
51+
52+
globalThis.FileSystemFileHandle.prototype.createWritable = async function (options) {
53+
// Safari only support writing data in a worker with sync access handle.
54+
if (!workerUrl) {
55+
const stringCode = `(${code.toString()})()`
56+
const blob = new Blob([stringCode], {
57+
type: 'text/javascript'
58+
})
59+
workerUrl = URL.createObjectURL(blob)
60+
}
61+
const worker = new Worker(workerUrl, { type: 'module' })
62+
63+
let position = 0
64+
const textEncoder = new TextEncoder()
65+
let size = await this.getFile().then(file => file.size)
66+
67+
const send = message => new Promise((resolve, reject) => {
68+
const mc = new MessageChannel()
69+
mc.port1.onmessage = evt => {
70+
if (evt.data instanceof Error) reject(evt.data)
71+
else resolve(evt.data)
72+
mc.port1.close()
73+
mc.port2.close()
74+
mc.port1.onmessage = null
75+
}
76+
worker.postMessage(message, [mc.port2])
77+
})
78+
79+
// Safari also don't support transferable file system handles.
80+
// So we need to pass the path to the worker. This is a bit hacky and ugly.
81+
const root = await navigator.storage.getDirectory()
82+
const parent = await wm.get(this)
83+
const path = await root.resolve(parent)
84+
85+
// Should likely never happen, but just in case...
86+
if (path === null) throw new DOMException(...GONE)
87+
88+
let controller
89+
await send({ type: 'open', path, name: this.name })
90+
91+
if (options?.keepExistingData === false) {
92+
await send({ type: 'truncate', size: 0 })
93+
size = 0
94+
}
95+
96+
const ws = new FileSystemWritableFileStream({
97+
start: ctrl => {
98+
controller = ctrl
99+
},
100+
async write(chunk) {
101+
const isPlainObject = chunk?.constructor === Object
102+
103+
if (isPlainObject) {
104+
chunk = { ...chunk }
105+
} else {
106+
chunk = { type: 'write', data: chunk, position }
107+
}
108+
109+
if (chunk.type === 'write') {
110+
if (!('data' in chunk)) {
111+
await send({ type: 'close' })
112+
throw new DOMException(...SYNTAX('write requires a data argument'))
113+
}
114+
115+
chunk.position ??= position
116+
117+
if (typeof chunk.data === 'string') {
118+
chunk.data = textEncoder.encode(chunk.data)
119+
}
120+
121+
else if (chunk.data instanceof ArrayBuffer) {
122+
chunk.data = new Uint8Array(chunk.data)
123+
}
124+
125+
else if (!(chunk.data instanceof Uint8Array) && ArrayBuffer.isView(chunk.data)) {
126+
chunk.data = new Uint8Array(chunk.data.buffer, chunk.data.byteOffset, chunk.data.byteLength)
127+
}
128+
129+
else if (!(chunk.data instanceof Uint8Array)) {
130+
const ab = await new Response(chunk.data).arrayBuffer()
131+
chunk.data = new Uint8Array(ab)
132+
}
133+
134+
if (Number.isInteger(chunk.position) && chunk.position >= 0) {
135+
position = chunk.position
136+
}
137+
position += chunk.data.byteLength
138+
size += chunk.data.byteLength
139+
} else if (chunk.type === 'seek') {
140+
if (Number.isInteger(chunk.position) && chunk.position >= 0) {
141+
if (size < chunk.position) {
142+
throw new DOMException(...INVALID)
143+
}
144+
console.log('seeking', chunk)
145+
position = chunk.position
146+
return // Don't need to enqueue seek...
147+
} else {
148+
await send({ type: 'close' })
149+
throw new DOMException(...SYNTAX('seek requires a position argument'))
150+
}
151+
} else if (chunk.type === 'truncate') {
152+
if (Number.isInteger(chunk.size) && chunk.size >= 0) {
153+
size = chunk.size
154+
if (position > size) { position = size }
155+
} else {
156+
await send({ type: 'close' })
157+
throw new DOMException(...SYNTAX('truncate requires a size argument'))
158+
}
159+
}
160+
161+
await send(chunk)
162+
},
163+
async close () {
164+
await send({ type: 'close' })
165+
worker.terminate()
166+
},
167+
async abort (reason) {
168+
await send({ type: 'abort', reason })
169+
worker.terminate()
170+
},
171+
})
172+
173+
return ws
174+
}
175+
176+
const orig = FileSystemDirectoryHandle.prototype.getFileHandle
177+
FileSystemDirectoryHandle.prototype.getFileHandle = async function (...args) {
178+
const handle = await orig.call(this, ...args)
179+
wm.set(handle, this)
180+
return handle
181+
}
182+
}

0 commit comments

Comments
 (0)