diff --git a/fix-sw.bash b/fix-sw.bash new file mode 100755 index 0000000..a718e34 --- /dev/null +++ b/fix-sw.bash @@ -0,0 +1,6 @@ +importFile=$(grep -Po '(?<=await import\()(.+)\"' dist/sw.js) +echo "Captured ${importFile}" +ESCAPED_REPLACE=$(printf '%s\n' "$importFile" | sed -e 's/[\/&]/\\&/g') +sed -i -e '1s/^/import { FileSystemWritableFileStream } from '${ESCAPED_REPLACE}';\n/' \ + -e 's/.*await import.*/\/\//' \ + -e 's/return new t(await this\[ee\]\.createWritable(e));/return new FileSystemWritableFileStream(await this\[ee\]\.createWritable(e));/' dist/sw.js diff --git a/index.html b/index.html index 3b8762c..4e34c04 100644 --- a/index.html +++ b/index.html @@ -1,21 +1,23 @@ - + + + + + + + + + + NDN Workspace + + + - - - - - - - - - NDN Workspace - - - - -
- - - - \ No newline at end of file + +
+ + + diff --git a/package.json b/package.json index b87abc3..1bbcdc9 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc && vite build && ./fix-sw.bash", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "peer-server": "peerjs --port 8000 --key peerjs --path /aincraft --allow_discovery true", @@ -50,6 +50,7 @@ "diff": "^5.1.0", "event-iterator": "^2.0.0", "eventemitter3": "^5.0.1", + "file-system-access": "^1.0.4", "jszip": "^3.10.1", "peerjs": "^1.5.1", "qr-scanner": "^1.4.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eba6ff1..e2852b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -119,6 +119,9 @@ dependencies: eventemitter3: specifier: ^5.0.1 version: 5.0.1 + file-system-access: + specifier: ^1.0.4 + version: 1.0.4 jszip: specifier: ^3.10.1 version: 3.10.1 @@ -3030,6 +3033,10 @@ packages: resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==} dev: true + /@types/wicg-file-system-access@2020.9.8: + resolution: {integrity: sha512-ggMz8nOygG7d/stpH40WVaNvBwuyYLnrg5Mbyf6bmsj/8+gb6Ei4ZZ9/4PNpcPNTT8th9Q8sM8wYmWGjMWLX/A==} + dev: false + /@types/wicg-file-system-access@2023.10.4: resolution: {integrity: sha512-ewOj7hWhsUTS2+aY6zY+7BwlgqGBj5ZXxKuHt3TAWpIJH0bDW/6bO1N1SdUDAzV8r0Nc+/ZtpAEETYTwrehBMw==} dev: true @@ -4415,6 +4422,17 @@ packages: flat-cache: 3.2.0 dev: true + /file-system-access@1.0.4: + resolution: {integrity: sha512-JDlhH+gJfZu/oExmtN4/6VX+q1etlrbJbR5uzoBa4BzfTRQbEXGFuGIBRk3ZcPocko3WdEclZSu+d/SByjG6Rg==} + engines: {node: '>=14'} + dependencies: + '@types/wicg-file-system-access': 2020.9.8 + fetch-blob: 3.2.0 + node-domexception: 1.0.0 + optionalDependencies: + web-streams-polyfill: 3.2.1 + dev: false + /filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} dependencies: diff --git a/public/registerSW.js b/public/registerSW.js new file mode 100644 index 0000000..ac25542 --- /dev/null +++ b/public/registerSW.js @@ -0,0 +1,5 @@ +if ("serviceWorker" in navigator) { + window.addEventListener("load", () => { + navigator.serviceWorker.register("/sw.js", { scope: "/", type: "module" }); + }); +} diff --git a/src/backend/main.ts b/src/backend/main.ts index 0d8aa97..6fb9517 100644 --- a/src/backend/main.ts +++ b/src/backend/main.ts @@ -15,7 +15,7 @@ import { FsStorage, InMemoryStorage, type Storage } from "./storage" import { SyncAgent } from './sync-agent' import { Certificate, ECDSA, createSigner } from "@ndn/keychain" import { v4 as uuidv4 } from "uuid" -import { base64ToBytes, encodeKey as encodePath, Signal as BackendSignal } from "../utils" +import { base64ToBytes, encodeKey as encodePath, Signal as BackendSignal, openRoot } from "../utils" import { Decoder } from "@ndn/tlv" export const UseAutoAnnouncement = false @@ -208,7 +208,7 @@ export async function bootstrapWorkspace(opts: { if (opts.inMemory) { persistStore = new InMemoryStorage() } else { - const handle = await navigator.storage.getDirectory() + const handle = await openRoot() const subFolder = await handle.getDirectoryHandle(encodePath(nodeId.toString()), { create: true }) persistStore = new FsStorage(subFolder) } diff --git a/src/backend/models/connections.ts b/src/backend/models/connections.ts index 85dfb96..2b54edf 100644 --- a/src/backend/models/connections.ts +++ b/src/backend/models/connections.ts @@ -1,3 +1,4 @@ +import { openRoot } from "../../utils" import { TypedModel } from "./typed-models" export type ConfigBase = { @@ -50,7 +51,7 @@ export const storageFolder = 'connections' export const connections = new TypedModel('connections', getName) export async function initDefault() { - const rootHandle = await navigator.storage.getDirectory() + const rootHandle = await openRoot() try { await rootHandle.getDirectoryHandle(storageFolder) return diff --git a/src/backend/models/typed-models.ts b/src/backend/models/typed-models.ts index f7117d8..b15a72c 100644 --- a/src/backend/models/typed-models.ts +++ b/src/backend/models/typed-models.ts @@ -1,4 +1,4 @@ -import { encodeKey as encodePath } from "../../utils" +import { encodeKey as encodePath, openRoot } from "../../utils" export class TypedModel { constructor( @@ -7,7 +7,7 @@ export class TypedModel { ) { } async save(object: T) { - const rootHandle = await navigator.storage.getDirectory() + const rootHandle = await openRoot() const connections = await rootHandle.getDirectoryHandle(this.storageFolder, { create: true }) const fileHandle = await connections.getFileHandle(encodePath(this.getName(object)), { create: true }) const textFile = await fileHandle.createWritable() @@ -16,7 +16,7 @@ export class TypedModel { } async remove(connName: string) { - const rootHandle = await navigator.storage.getDirectory() + const rootHandle = await openRoot() const objects = await rootHandle.getDirectoryHandle(this.storageFolder, { create: true }) try { await objects.removeEntry(encodePath(connName), { recursive: true }) @@ -27,7 +27,7 @@ export class TypedModel { } async isExisting(connName: string) { - const rootHandle = await navigator.storage.getDirectory() + const rootHandle = await openRoot() const objects = await rootHandle.getDirectoryHandle(this.storageFolder, { create: true }) try { await objects.getFileHandle(encodePath(connName), { create: false }) @@ -38,7 +38,7 @@ export class TypedModel { } async load(connName: string) { - const rootHandle = await navigator.storage.getDirectory() + const rootHandle = await openRoot() const objects = await rootHandle.getDirectoryHandle(this.storageFolder, { create: true }) try { const file = await objects.getFileHandle(encodePath(connName), { create: false }) @@ -52,12 +52,12 @@ export class TypedModel { } async loadAll() { - const rootHandle = await navigator.storage.getDirectory() + const rootHandle = await openRoot() const objects = await rootHandle.getDirectoryHandle(this.storageFolder, { create: true }) const ret: Array = [] for await (const [, handle] of objects.entries()) { - if (handle instanceof FileSystemFileHandle) { + if (handle.kind === 'file') { const jsonFile = await handle.getFile() const jsonText = await jsonFile.text() const object = JSON.parse(jsonText) as T diff --git a/src/components/connect/ndn-testbed.tsx b/src/components/connect/ndn-testbed.tsx index 2632563..ec36433 100644 --- a/src/components/connect/ndn-testbed.tsx +++ b/src/components/connect/ndn-testbed.tsx @@ -67,21 +67,30 @@ export default function NdnTestbed(props: { setTempFace(nfdWsFace) } - // Request profile - const caProfile = await ndncert.retrieveCaProfile({ - caCertFullName: TestbedAnchorName, - }) - // Probe step - const probeRes = await ndncert.requestProbe({ - profile: caProfile, - parameters: { email: new TextEncoder().encode(curEmail) }, - }) - if (probeRes.entries.length <= 0) { - console.error('No available name to register') - return + let caProfile: ndncert.CaProfile | undefined = undefined + let caFullName = TestbedAnchorName + let probeRes + while (caProfile === undefined) { + // Request profile + caProfile = await ndncert.retrieveCaProfile({ + caCertFullName: caFullName, + }) + // Probe step + probeRes = await ndncert.requestProbe({ + profile: caProfile, + parameters: { email: new TextEncoder().encode(curEmail) }, + }) + if (probeRes.entries.length <= 0) { + console.error('No available name to register') + return + } + if (probeRes.redirects.length > 0) { + caFullName = probeRes.redirects[0].caCertFullName + caProfile = undefined + } } // Generate key pair - const myPrefix = probeRes.entries[0].prefix + const myPrefix = probeRes!.entries[0].prefix const keyName = keychain.CertNaming.makeKeyName(myPrefix) const algo = keychain.ECDSA const gen = await keychain.ECDSA.cryptoGenerate({}, true) diff --git a/src/components/oauth-test/index.tsx b/src/components/oauth-test/index.tsx index 67578b4..cf38d2f 100644 --- a/src/components/oauth-test/index.tsx +++ b/src/components/oauth-test/index.tsx @@ -35,7 +35,7 @@ export default function OauthTest() { access_type: 'offline', }).toString() const url = 'https://accounts.google.com/o/oauth2/v2/auth?' + queryStr - window.open(url) + window.open(url) // TODO: not working on Safari } @@ -48,7 +48,7 @@ export default function OauthTest() { state: requestId(), }).toString() const url = 'https://github.com/login/oauth/authorize?' + queryStr - window.open(url) + window.open(url) // TODO: not working on Safari } diff --git a/src/components/share-latex/share-latex/index.tsx b/src/components/share-latex/share-latex/index.tsx index 089b594..2596157 100644 --- a/src/components/share-latex/share-latex/index.tsx +++ b/src/components/share-latex/share-latex/index.tsx @@ -162,7 +162,7 @@ export default function ShareLatex(props: { const content = await zip.generateAsync({ type: "uint8array" }) const file = new Blob([content], { type: 'application/zip;base64' }) const fileUrl = URL.createObjectURL(file) - window.open(fileUrl) + window.open(fileUrl) // TODO: not working on Safari } const [texEngine, setTexEngine] = createSignal() @@ -202,7 +202,7 @@ export default function ShareLatex(props: { // URL.revokeObjectURL(previewUrl()!); // setPreviewUrl(URL.createObjectURL(blob)) const fileUrl = URL.createObjectURL(blob) - window.open(fileUrl) + window.open(fileUrl) // TODO: not working on Safari } const onCompileRemote = async () => { @@ -242,7 +242,7 @@ export default function ShareLatex(props: { const pdfContent = await segObj.fetch(`/ndn/workspace-compiler/result/${reqId}`) const file = new Blob([pdfContent], { type: 'application/pdf;base64' }) const fileUrl = URL.createObjectURL(file) - window.open(fileUrl) + window.open(fileUrl) // TODO: not working on Safari } } @@ -295,7 +295,7 @@ export default function ShareLatex(props: { if (blob !== undefined) { const file = new Blob([blob], { type: 'application/octet-stream;base64' }) const fileUrl = URL.createObjectURL(file) - window.open(fileUrl) + window.open(fileUrl) // TODO: not working on Safari } } catch (e) { console.error(`Unable to fetch blob file: `, e) diff --git a/src/utils/index.ts b/src/utils/index.ts index a78503f..d1516f9 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,3 +4,4 @@ export * from './reset' export * from './solid-assist' export * from './callcc' export * from './signals' +export * from './opfs-ponyfill' \ No newline at end of file diff --git a/src/utils/opfs-ponyfill.ts b/src/utils/opfs-ponyfill.ts new file mode 100644 index 0000000..dc94aa8 --- /dev/null +++ b/src/utils/opfs-ponyfill.ts @@ -0,0 +1,13 @@ +import { getOriginPrivateDirectory } from 'file-system-access' +import indexedDbAdapter from 'file-system-access/lib/adapters/indexeddb' + +export const openRoot: () => Promise = (() => { + if (FileSystemFileHandle.prototype.createWritable !== undefined) { + // Normal browsers + return getOriginPrivateDirectory + } else { + // Weird Safari + console.log('Safari ponyfill applied') + return () => getOriginPrivateDirectory(indexedDbAdapter) + } +})() diff --git a/src/workers/sw.ts b/src/workers/sw.ts index b71594a..8238d83 100644 --- a/src/workers/sw.ts +++ b/src/workers/sw.ts @@ -3,7 +3,7 @@ import { clientsClaim } from 'workbox-core' import * as navigationPreload from 'workbox-navigation-preload' import { registerRoute, NavigationRoute } from 'workbox-routing' import { DefaultTexliveEndpoint } from '../constants' -import { encodeKey } from '../utils' +import { encodeKey, openRoot } from '../utils' declare let self: ServiceWorkerGlobalScope; @@ -43,8 +43,7 @@ registerRoute(/\/stored\/.*/, async (options) => { newUrl = originalUrl; } - - const opfsRoot = await navigator.storage.getDirectory(); + const opfsRoot = await openRoot(); const stored = await opfsRoot.getDirectoryHandle('stored', { create: true }); const hashStr = encodeKey(newUrl.toString()); try { diff --git a/vite.config.ts b/vite.config.ts index d966c26..45dbea5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -19,6 +19,7 @@ export default defineConfig({ srcDir: 'src/workers', filename: 'sw.ts', registerType: 'autoUpdate', + injectRegister: null, devOptions: { enabled: true // SW and devtools adds > 1 sec to loading time. Enable only when nesessary. },