feat(core): new worker workspace engine (#9257)

This commit is contained in:
EYHN
2025-01-17 00:22:18 +08:00
committed by GitHub
parent 7dc470e7ea
commit a2ffdb4047
219 changed files with 4267 additions and 7194 deletions
@@ -24,6 +24,9 @@
9D90BE2B2CCB9876006677DB /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1F2CCB9876006677DB /* config.xml */; };
9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; };
9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; };
9DEC593B2D3002E70027CEBD /* AffineHttpHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */; };
9DEC593F2D30EFA40027CEBD /* AffineWsHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC593E2D30EFA40027CEBD /* AffineWsHandler.swift */; };
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC59422D323EE00027CEBD /* Mutex.swift */; };
9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */; };
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C6F2D0307B700BC2AD1 /* affine_mobile_native.swift */; };
@@ -52,6 +55,9 @@
9D90BE202CCB9876006677DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
9D90BE212CCB9876006677DB /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
9D90BE232CCB9876006677DB /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineHttpHandler.swift; sourceTree = "<group>"; };
9DEC593E2D30EFA40027CEBD /* AffineWsHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AffineWsHandler.swift; sourceTree = "<group>"; };
9DEC59422D323EE00027CEBD /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = "<group>"; };
9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libaffine_mobile_native.a; sourceTree = "<group>"; };
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
@@ -156,6 +162,9 @@
9D90BE242CCB9876006677DB /* App */ = {
isa = PBXGroup;
children = (
9DEC59422D323EE00027CEBD /* Mutex.swift */,
9DEC593A2D3002C70027CEBD /* AffineHttpHandler.swift */,
9DEC593E2D30EFA40027CEBD /* AffineWsHandler.swift */,
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */,
9D90BE1A2CCB9876006677DB /* Plugins */,
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */,
@@ -331,13 +340,16 @@
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */,
5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */,
5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */,
9DEC593B2D3002E70027CEBD /* AffineHttpHandler.swift in Sources */,
C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */,
C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */,
C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */,
E93B276C2CED92B1001409B8 /* NavigationGesturePlugin.swift in Sources */,
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */,
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */,
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */,
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */,
9DEC593F2D30EFA40027CEBD /* AffineWsHandler.swift in Sources */,
9D90BE272CCB9876006677DB /* AffineViewController.swift in Sources */,
9D90BE282CCB9876006677DB /* AppDelegate.swift in Sources */,
);
@@ -0,0 +1,114 @@
//
// RequestUrlSchemeHandler.swift
// App
//
// Created by EYHN on 2025/1/9.
//
import WebKit
enum AffineHttpError: Error {
case invalidOperation(reason: String), invalidState(reason: String)
}
class AffineHttpHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
urlSchemeTask.stopped = Mutex.init(false)
guard let rawUrl = urlSchemeTask.request.url else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad request"))
return
}
guard let scheme = rawUrl.scheme else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad request"))
return
}
let httpProtocol = scheme == "affine-http" ? "http" : "https"
guard let urlComponents = URLComponents(url: rawUrl, resolvingAgainstBaseURL: true) else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad request"))
return
}
guard let host = urlComponents.host else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad url"))
return
}
let path = urlComponents.path
let query = urlComponents.query != nil ? "?\(urlComponents.query!)" : ""
guard let targetUrl = URL(string: "\(httpProtocol)://\(host)\(path)\(query)") else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidOperation(reason: "bad url"))
return
}
var request = URLRequest(url: targetUrl);
request.httpMethod = urlSchemeTask.request.httpMethod;
request.httpShouldHandleCookies = true
request.httpBody = urlSchemeTask.request.httpBody
urlSchemeTask.request.allHTTPHeaderFields?.filter({
key, value in
let normalizedKey = key.lowercased()
return normalizedKey == "content-type" ||
normalizedKey == "content-length" ||
normalizedKey == "accept"
}).forEach {
key, value in
request.setValue(value, forHTTPHeaderField: key)
}
URLSession.shared.dataTask(with: request) {
rawData, rawResponse, error in
urlSchemeTask.stopped?.withLock({
if $0 {
return
}
if error != nil {
urlSchemeTask.didFailWithError(error!)
} else {
guard let httpResponse = rawResponse as? HTTPURLResponse else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "bad response"))
return
}
let inheritedHeaders = httpResponse.allHeaderFields.filter({
key, value in
let normalizedKey = (key as? String)?.lowercased()
return normalizedKey == "content-type" ||
normalizedKey == "content-length"
}) as? [String: String] ?? [:]
let newHeaders: [String: String] = [
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*"
]
guard let response = HTTPURLResponse.init(url: rawUrl, statusCode: httpResponse.statusCode, httpVersion: nil, headerFields: inheritedHeaders.merging(newHeaders, uniquingKeysWith: { (_, newHeaders) in newHeaders })) else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "failed to create response"))
return
}
urlSchemeTask.didReceive(response)
if rawData != nil {
urlSchemeTask.didReceive(rawData!)
}
urlSchemeTask.didFinish()
}
})
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
urlSchemeTask.stopped?.withLock({
$0 = true
})
}
}
private extension WKURLSchemeTask {
var stopped: Mutex<Bool>? {
get {
return objc_getAssociatedObject(self, &stoppedKey) as? Mutex<Bool> ?? nil
}
set {
objc_setAssociatedObject(self, &stoppedKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}
}
private var stoppedKey = malloc(1)
@@ -13,6 +13,19 @@ class AFFiNEViewController: CAPBridgeViewController {
intelligentsButton.delegate = self
dismissIntelligentsButton()
}
override func webViewConfiguration(for instanceConfiguration: InstanceConfiguration) -> WKWebViewConfiguration {
let configuration = super.webViewConfiguration(for: instanceConfiguration)
return configuration
}
override func webView(with frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView {
configuration.setURLSchemeHandler(AffineHttpHandler(), forURLScheme: "affine-http")
configuration.setURLSchemeHandler(AffineHttpHandler(), forURLScheme: "affine-https")
configuration.setURLSchemeHandler(AffineWsHandler(), forURLScheme: "affine-ws")
configuration.setURLSchemeHandler(AffineWsHandler(), forURLScheme: "affine-wss")
return super.webView(with: frame, configuration: configuration)
}
override func capacitorDidLoad() {
let plugins: [CAPPlugin] = [
@@ -0,0 +1,197 @@
//
// RequestUrlSchemeHandler.swift
// App
//
// Created by EYHN on 2025/1/9.
//
import WebKit
enum AffineWsError: Error {
case invalidOperation(reason: String), invalidState(reason: String)
}
/**
this custom url scheme handler simulates websocket connection through an http request.
frontend open websocket connections and send messages by sending requests to affine-ws:// or affine-wss://
the handler has two endpoints:
`affine-ws:///open?uuid={uuid}&url={wsUrl}`: open a websocket connection and return received data through the SSE protocol. If the front-end closes the http connection, the websocket connection will also be closed.
`affine-ws:///send?uuid={uuid}`: send the request body data to the websocket connection with the specified uuid.
*/
class AffineWsHandler: NSObject, WKURLSchemeHandler {
var wsTasks: [UUID: URLSessionWebSocketTask] = [:]
func webView(_ webView: WKWebView, start urlSchemeTask: any WKURLSchemeTask) {
urlSchemeTask.stopped = Mutex.init(false)
guard let rawUrl = urlSchemeTask.request.url else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "bad request"))
return
}
guard let urlComponents = URLComponents(url: rawUrl, resolvingAgainstBaseURL: true) else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "bad request"))
return
}
let path = urlComponents.path
if path == "/open" {
guard let targetUrlStr = urlComponents.queryItems?.first(where: { $0.name == "url" })?.value else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "url is request"))
return
}
guard let targetUrl = URL(string: targetUrlStr) else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "failed to parse url"))
return
}
guard let uuidStr = urlComponents.queryItems?.first(where: { $0.name == "uuid" })?.value else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "url is request"))
return
}
guard let uuid = UUID(uuidString: uuidStr) else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "invalid uuid"))
return
}
guard let response = HTTPURLResponse.init(url: rawUrl, statusCode: 200, httpVersion: nil, headerFields: [
"X-Accel-Buffering": "no",
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*"
]) else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "failed to create response"))
return
}
urlSchemeTask.didReceive(response)
let jsonEncoder = JSONEncoder()
let json = String(data: try! jsonEncoder.encode(["type": "start"]), encoding: .utf8)!
urlSchemeTask.didReceive("data: \(json)\n\n".data(using: .utf8)!)
var request = URLRequest(url: targetUrl);
request.httpShouldHandleCookies = true
let webSocketTask = URLSession.shared.webSocketTask(with: targetUrl)
self.wsTasks[uuid] = webSocketTask
webSocketTask.resume()
urlSchemeTask.wsTask = webSocketTask
var completionHandler: ((Result<URLSessionWebSocketTask.Message, any Error>) -> Void)!
completionHandler = {
let result = $0
urlSchemeTask.stopped?.withLock({
let stopped = $0
if stopped {
return
}
let jsonEncoder = JSONEncoder()
switch result {
case .success(let message):
if case .string(let string) = message {
let json = String(data: try! jsonEncoder.encode(["type": "message", "data": string]), encoding: .utf8)!
urlSchemeTask.didReceive("data: \(json)\n\n".data(using: .utf8)!)
}
case .failure(let error):
let json = String(data: try! jsonEncoder.encode(["type": "error", "error": error.localizedDescription]), encoding: .utf8)!
urlSchemeTask.didReceive("data: \(json)\n\n".data(using: .utf8)!)
urlSchemeTask.didFinish()
}
})
// recursive calls
webSocketTask.receive(completionHandler: completionHandler)
}
webSocketTask.receive(completionHandler: completionHandler)
} else if path == "/send" {
if urlSchemeTask.request.httpMethod != "POST" {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "Method should be POST"))
return
}
guard let uuidStr = urlComponents.queryItems?.first(where: { $0.name == "uuid" })?.value else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "url is request"))
return
}
guard let uuid = UUID(uuidString: uuidStr) else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "invalid uuid"))
return
}
guard let ContentType = urlSchemeTask.request.allHTTPHeaderFields?.first(where: {$0.key.lowercased() == "content-type"})?.value else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "content-type is request"))
return
}
if ContentType != "text/plain" {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "content-type not support"))
return
}
guard let body = urlSchemeTask.request.httpBody else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "no body"))
return
}
let stringBody = String(decoding: body, as: UTF8.self)
guard let webSocketTask = self.wsTasks[uuid] else {
urlSchemeTask.didFailWithError(AffineWsError.invalidOperation(reason: "connection not found"))
return
}
guard let response = HTTPURLResponse.init(url: rawUrl, statusCode: 200, httpVersion: nil, headerFields: [
"Content-Type": "application/json",
"Cache-Control": "no-cache",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "*"
]) else {
urlSchemeTask.didFailWithError(AffineHttpError.invalidState(reason: "failed to create response"))
return
}
let jsonEncoder = JSONEncoder()
webSocketTask.send(.string(stringBody), completionHandler: {
error in
urlSchemeTask.stopped?.withLock({
if $0 {
return
}
if error != nil {
let json = try! jsonEncoder.encode(["error": error!.localizedDescription])
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(json)
} else {
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(try! jsonEncoder.encode(["uuid": uuid.uuidString.data(using: .utf8)!]))
urlSchemeTask.didFinish()
}
})
})
}
}
func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
urlSchemeTask.stopped?.withLock({
$0 = false
})
urlSchemeTask.wsTask?.cancel(with: .abnormalClosure, reason: "Closed".data(using: .utf8))
}
}
private extension WKURLSchemeTask {
var stopped: Mutex<Bool>? {
get {
return objc_getAssociatedObject(self, &stoppedKey) as? Mutex<Bool> ?? nil
}
set {
objc_setAssociatedObject(self, &stoppedKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}
var wsTask: URLSessionWebSocketTask? {
get {
return objc_getAssociatedObject(self, &wsTaskKey) as? URLSessionWebSocketTask
}
set {
objc_setAssociatedObject(self, &stoppedKey, newValue, .OBJC_ASSOCIATION_ASSIGN)
}
}
}
private var stoppedKey = malloc(1)
private var wsTaskKey = malloc(1)
@@ -0,0 +1,23 @@
//
// Mutex.swift
// App
//
// Created by EYHN on 2025/1/11.
//
import Foundation
final class Mutex<Wrapped>: @unchecked Sendable {
private let lock = NSLock.init()
private var wrapped: Wrapped
init(_ wrapped: Wrapped) {
self.wrapped = wrapped
}
func withLock<R>(_ body: @Sendable (inout Wrapped) throws -> R) rethrows -> R {
self.lock.lock()
defer { self.lock.unlock() }
return try body(&wrapped)
}
}
@@ -30,6 +30,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "getPeerPulledRemoteClocks", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getPeerPulledRemoteClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setPeerPulledRemoteClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getPeerPushedClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getPeerPushedClocks", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setPeerPushedClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "clearClocks", returnType: CAPPluginReturnPromise),
@@ -334,11 +335,14 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
let peer = try call.getStringEnsure("peer")
let docId = try call.getStringEnsure("docId")
let clock = try await docStoragePool.getPeerRemoteClock(universalId: id, peer: peer, docId: docId)
call.resolve([
"docId": clock.docId,
"timestamp": clock.timestamp,
])
if let clock = try await docStoragePool.getPeerRemoteClock(universalId: id, peer: peer, docId: docId) {
call.resolve([
"docId": clock.docId,
"timestamp": clock.timestamp,
])
} else {
call.resolve()
}
} catch {
call.reject("Failed to get peer remote clock, \(error)", nil, error)
@@ -391,11 +395,14 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
let peer = try call.getStringEnsure("peer")
let docId = try call.getStringEnsure("docId")
let clock = try await docStoragePool.getPeerPulledRemoteClock(universalId: id, peer: peer, docId: docId)
call.resolve([
"docId": clock.docId,
"timestamp": clock.timestamp,
])
if let clock = try await docStoragePool.getPeerPulledRemoteClock(universalId: id, peer: peer, docId: docId) {
call.resolve([
"docId": clock.docId,
"timestamp": clock.timestamp,
])
} else {
call.resolve()
}
} catch {
call.reject("Failed to get peer pulled remote clock, \(error)", nil, error)
@@ -424,6 +431,26 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
}
}
@objc func getPeerPushedClock(_ call: CAPPluginCall) {
Task {
do {
let id = try call.getStringEnsure("id")
let peer = try call.getStringEnsure("peer")
let docId = try call.getStringEnsure("docId")
if let clock = try await docStoragePool.getPeerPushedClock(universalId: id, peer: peer, docId: docId) {
call.resolve([
"docId": clock.docId,
"timestamp": clock.timestamp,
])
} else {
call.resolve()
}
} catch {
call.reject("Failed to get peer pushed clock, \(error)", nil, error)
}
}
}
@objc func getPeerPushedClocks(_ call: CAPPluginCall) {
Task {
do {
@@ -0,0 +1,36 @@
//
// SafeWKURLSchemeTask.swift
// App
//
// Created by EYHN on 2025/1/11.
//
import WebKit
class SafeWKURLSchemeTask: WKURLSchemeTask, NSObject {
var origin: any WKURLSchemeTask
init(origin: any WKURLSchemeTask) {
self.origin = origin
self.request = origin.request
}
var request: URLRequest
func didReceive(_ response: URLResponse) {
<#code#>
}
func didReceive(_ data: Data) {
self.origin.didReceive(<#T##response: URLResponse##URLResponse#>)
}
func didFinish() {
self.origin.didFinish()
}
func didFailWithError(_ error: any Error) {
self.origin.didFailWithError(error)
}
}
File diff suppressed because it is too large Load Diff
@@ -321,6 +321,11 @@ uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pulled_re
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pulled_remote_clocks(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCK
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCK
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pushed_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCKS
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCKS
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_pushed_clocks(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer
@@ -759,6 +764,12 @@ uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_pul
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PULLED_REMOTE_CLOCKS
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_pulled_remote_clocks(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCK
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCK
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_pushed_clock(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_PUSHED_CLOCKS
@@ -14,10 +14,10 @@ const config: CapacitorConfig = {
},
plugins: {
CapacitorCookies: {
enabled: true,
enabled: false,
},
CapacitorHttp: {
enabled: true,
enabled: false,
},
Keyboard: {
resize: KeyboardResize.Native,
+1
View File
@@ -26,6 +26,7 @@
"@capacitor/keyboard": "^6.0.3",
"@sentry/react": "^8.44.0",
"@toeverything/infra": "workspace:^",
"async-call-rpc": "^6.4.2",
"next-themes": "^0.4.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
+54 -23
View File
@@ -12,23 +12,22 @@ import {
DefaultServerService,
ServersService,
ValidatorProvider,
WebSocketAuthProvider,
} from '@affine/core/modules/cloud';
import { DocsService } from '@affine/core/modules/doc';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { I18nProvider } from '@affine/core/modules/i18n';
import { LifecycleService } from '@affine/core/modules/lifecycle';
import { configureLocalStorageStateStorageImpls } from '@affine/core/modules/storage';
import {
configureLocalStorageStateStorageImpls,
NbstoreProvider,
} from '@affine/core/modules/storage';
import { PopupWindowProvider } from '@affine/core/modules/url';
import { ClientSchemeProvider } from '@affine/core/modules/url/providers/client-schema';
import { configureIndexedDBUserspaceStorageProvider } from '@affine/core/modules/userspace';
import { configureBrowserWorkbenchModule } from '@affine/core/modules/workbench';
import { WorkspacesService } from '@affine/core/modules/workspace';
import {
configureBrowserWorkspaceFlavours,
configureIndexedDBWorkspaceEngineStorageProvider,
} from '@affine/core/modules/workspace-engine';
import { configureBrowserWorkspaceFlavours } from '@affine/core/modules/workspace-engine';
import { I18n } from '@affine/i18n';
import { WorkerClient } from '@affine/nbstore/worker/client';
import {
defaultBlockMarkdownAdapterMatchers,
docLinkBaseURLMiddleware,
@@ -44,16 +43,17 @@ import { Browser } from '@capacitor/browser';
import { Haptics } from '@capacitor/haptics';
import { Keyboard, KeyboardStyle } from '@capacitor/keyboard';
import { Framework, FrameworkRoot, getCurrentStore } from '@toeverything/infra';
import { OpClient } from '@toeverything/infra/op';
import { AsyncCall } from 'async-call-rpc';
import { useTheme } from 'next-themes';
import { Suspense, useEffect } from 'react';
import { RouterProvider } from 'react-router-dom';
import { BlocksuiteMenuConfigProvider } from './bs-menu-config';
import { configureFetchProvider } from './fetch';
import { ModalConfigProvider } from './modal-config';
import { Cookie } from './plugins/cookie';
import { Hashcash } from './plugins/hashcash';
import { Intelligents } from './plugins/intelligents';
import { NbStoreNativeDBApis } from './plugins/nbstore';
import { enableNavigationGesture$ } from './web-navigation-control';
const future = {
@@ -65,9 +65,52 @@ configureCommonModules(framework);
configureBrowserWorkbenchModule(framework);
configureLocalStorageStateStorageImpls(framework);
configureBrowserWorkspaceFlavours(framework);
configureIndexedDBWorkspaceEngineStorageProvider(framework);
configureIndexedDBUserspaceStorageProvider(framework);
configureMobileModules(framework);
framework.impl(NbstoreProvider, {
openStore(_key, options) {
const worker = new Worker(
new URL(
/* webpackChunkName: "nbstore-worker" */ './worker.ts',
import.meta.url
)
);
const { port1: nativeDBApiChannelServer, port2: nativeDBApiChannelClient } =
new MessageChannel();
AsyncCall<typeof NbStoreNativeDBApis>(NbStoreNativeDBApis, {
channel: {
on(listener) {
const f = (e: MessageEvent<any>) => {
listener(e.data);
};
nativeDBApiChannelServer.addEventListener('message', f);
return () => {
nativeDBApiChannelServer.removeEventListener('message', f);
};
},
send(data) {
nativeDBApiChannelServer.postMessage(data);
},
},
log: false,
});
nativeDBApiChannelServer.start();
worker.postMessage(
{
type: 'native-db-api-channel',
port: nativeDBApiChannelClient,
},
[nativeDBApiChannelClient]
);
const client = new WorkerClient(new OpClient(worker), options);
return {
store: client,
dispose: () => {
worker.terminate();
nativeDBApiChannelServer.close();
},
};
},
});
framework.impl(PopupWindowProvider, {
open: (url: string) => {
Browser.open({
@@ -81,18 +124,6 @@ framework.impl(ClientSchemeProvider, {
return 'affine';
},
});
configureFetchProvider(framework);
framework.impl(WebSocketAuthProvider, {
getAuthToken: async url => {
const cookies = await Cookie.getCookies({
url,
});
return {
userId: cookies['affine_user_id'],
token: cookies['affine_session'],
};
},
});
framework.impl(ValidatorProvider, {
async validate(_challenge, resource) {
const res = await Hashcash.hash({ challenge: resource });
-191
View File
@@ -1,191 +0,0 @@
/**
* this file is modified from part of https://github.com/ionic-team/capacitor/blob/74c3e9447e1e32e73f818d252eb12f453d849e8d/ios/Capacitor/Capacitor/assets/native-bridge.js#L466
*
* for support arraybuffer response type
*/
import { RawFetchProvider } from '@affine/core/modules/cloud/provider/fetch';
import { CapacitorHttp } from '@capacitor/core';
import type { Framework } from '@toeverything/infra';
const readFileAsBase64 = (file: File) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const data = reader.result;
if (data === null) {
reject(new Error('Failed to read file'));
} else {
resolve(btoa(data as string));
}
};
reader.onerror = reject;
reader.readAsBinaryString(file);
});
const convertFormData = async (formData: FormData) => {
const newFormData = [];
for (const pair of formData.entries()) {
const [key, value] = pair;
if (value instanceof File) {
const base64File = await readFileAsBase64(value);
newFormData.push({
key,
value: base64File,
type: 'base64File',
contentType: value.type,
fileName: value.name,
});
} else {
newFormData.push({ key, value, type: 'string' });
}
}
return newFormData;
};
const convertBody = async (body: unknown, contentType: string) => {
if (body instanceof ReadableStream || body instanceof Uint8Array) {
let encodedData;
if (body instanceof ReadableStream) {
const reader = body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const concatenated = new Uint8Array(
chunks.reduce((acc, chunk) => acc + chunk.length, 0)
);
let position = 0;
for (const chunk of chunks) {
concatenated.set(chunk, position);
position += chunk.length;
}
encodedData = concatenated;
} else {
encodedData = body;
}
let data = new TextDecoder().decode(encodedData);
let type;
if (contentType === 'application/json') {
try {
data = JSON.parse(data);
} catch {
// ignore
}
type = 'json';
} else if (contentType === 'multipart/form-data') {
type = 'formData';
} else if (
contentType === null || contentType === void 0
? void 0
: contentType.startsWith('image')
) {
type = 'image';
} else if (contentType === 'application/octet-stream') {
type = 'binary';
} else {
type = 'text';
}
return {
data,
type,
headers: { 'Content-Type': contentType || 'application/octet-stream' },
};
} else if (body instanceof URLSearchParams) {
return {
data: body.toString(),
type: 'text',
};
} else if (body instanceof FormData) {
const formData = await convertFormData(body);
return {
data: formData,
type: 'formData',
};
} else if (body instanceof File) {
const fileData = await readFileAsBase64(body);
return {
data: fileData,
type: 'file',
headers: { 'Content-Type': body.type },
};
}
return { data: body, type: 'json' };
};
function base64ToUint8Array(base64: string) {
const binaryString = atob(base64);
const binaryArray = [...binaryString].map(function (char) {
return char.charCodeAt(0);
});
return new Uint8Array(binaryArray);
}
export function configureFetchProvider(framework: Framework) {
framework.override(RawFetchProvider, {
fetch: async (input, init) => {
const request = new Request(input, init);
const { method } = request;
const tag = `CapacitorHttp fetch ${Date.now()} ${input}`;
console.time(tag);
try {
const { body } = request;
const optionHeaders = Object.fromEntries(request.headers.entries());
const {
data: requestData,
type,
headers,
} = await convertBody(
(init === null || init === void 0 ? void 0 : init.body) ||
body ||
undefined,
optionHeaders['Content-Type'] || optionHeaders['content-type']
);
const accept = optionHeaders['Accept'] || optionHeaders['accept'];
const nativeResponse = await CapacitorHttp.request({
url: request.url,
method: method,
data: requestData,
dataType: type as any,
responseType:
accept === 'application/octet-stream' ? 'arraybuffer' : undefined,
headers: Object.assign(Object.assign({}, headers), optionHeaders),
});
const contentType =
nativeResponse.headers['Content-Type'] ||
nativeResponse.headers['content-type'];
let data =
accept === 'application/octet-stream'
? base64ToUint8Array(nativeResponse.data)
: contentType === null || contentType === void 0
? void 0
: contentType.startsWith('application/json')
? JSON.stringify(nativeResponse.data)
: contentType === 'application/octet-stream'
? base64ToUint8Array(nativeResponse.data)
: nativeResponse.data;
// use null data for 204 No Content HTTP response
if (nativeResponse.status === 204) {
data = null;
}
// intercept & parse response before returning
const response = new Response(new Blob([data], { type: contentType }), {
headers: nativeResponse.headers,
status: nativeResponse.status,
});
/*
* copy url to response, `cordova-plugin-ionic` uses this url from the response
* we need `Object.defineProperty` because url is an inherited getter on the Response
* see: https://stackoverflow.com/a/57382543
* */
Object.defineProperty(response, 'url', {
value: nativeResponse.url,
});
console.timeEnd(tag);
return response;
} catch (error) {
console.timeEnd(tag);
throw error;
}
},
});
}
+9 -9
View File
@@ -1,5 +1,8 @@
import './setup';
import '@affine/component/theme';
import '@affine/core/mobile/styles/mobile.css';
import { bindNativeDBApis } from '@affine/nbstore/sqlite';
import {
init,
reactRouterV6BrowserTracingIntegration,
@@ -15,18 +18,15 @@ import {
} from 'react-router-dom';
import { App } from './app';
import { NbStoreNativeDBApis } from './plugins/nbstore';
bindNativeDBApis(NbStoreNativeDBApis);
// TODO(@L-Sun) Uncomment this when the `show` method implement by `@capacitor/keyboard` in ios
// import './virtual-keyboard';
function main() {
if (BUILD_CONFIG.debug || window.SENTRY_RELEASE) {
// workaround for Capacitor HttpPlugin
// capacitor-http-plugin will replace window.XMLHttpRequest with its own implementation
// but XMLHttpRequest.prototype is not defined which is used by sentry
// see: https://github.com/ionic-team/capacitor/blob/74c3e9447e1e32e73f818d252eb12f453d849e8d/core/native-bridge.ts#L581
if ('CapacitorWebXMLHttpRequest' in window) {
window.XMLHttpRequest.prototype = (
window.CapacitorWebXMLHttpRequest as any
).prototype;
}
// https://docs.sentry.io/platforms/javascript/guides/react/#configure
init({
dsn: process.env.SENTRY_DSN,
@@ -1,6 +0,0 @@
export interface CookiePlugin {
/**
* Returns the screen's current orientation.
*/
getCookies(options: { url: string }): Promise<Record<string, string>>;
}
@@ -1,8 +0,0 @@
import { registerPlugin } from '@capacitor/core';
import type { CookiePlugin } from './definitions';
const Cookie = registerPlugin<CookiePlugin>('Cookie');
export * from './definitions';
export { Cookie };
@@ -70,12 +70,12 @@ export interface NbStorePlugin {
timestamps: number[];
}) => Promise<{ count: number }>;
deleteDoc: (options: { id: string; docId: string }) => Promise<void>;
getDocClocks: (options: { id: string; after?: number | null }) => Promise<
{
getDocClocks: (options: { id: string; after?: number | null }) => Promise<{
clocks: {
docId: string;
timestamp: number;
}[]
>;
}[];
}>;
getDocClock: (options: { id: string; docId: string }) => Promise<
| {
docId: string;
@@ -95,47 +95,47 @@ export interface NbStorePlugin {
getPeerRemoteClocks: (options: {
id: string;
peer: string;
}) => Promise<Array<DocClock>>;
}) => Promise<{ clocks: Array<DocClock> }>;
getPeerRemoteClock: (options: {
id: string;
peer: string;
docId: string;
}) => Promise<DocClock>;
}) => Promise<DocClock | null>;
setPeerRemoteClock: (options: {
id: string;
peer: string;
docId: string;
clock: number;
timestamp: number;
}) => Promise<void>;
getPeerPushedClocks: (options: {
id: string;
peer: string;
}) => Promise<Array<DocClock>>;
}) => Promise<{ clocks: Array<DocClock> }>;
getPeerPushedClock: (options: {
id: string;
peer: string;
docId: string;
}) => Promise<DocClock>;
}) => Promise<DocClock | null>;
setPeerPushedClock: (options: {
id: string;
peer: string;
docId: string;
clock: number;
timestamp: number;
}) => Promise<void>;
getPeerPulledRemoteClocks: (options: {
id: string;
peer: string;
}) => Promise<Array<DocClock>>;
}) => Promise<{ clocks: Array<DocClock> }>;
getPeerPulledRemoteClock: (options: {
id: string;
peer: string;
docId: string;
}) => Promise<DocClock>;
}) => Promise<DocClock | null>;
setPeerPulledRemoteClock: (options: {
id: string;
peer: string;
docId: string;
clock: number;
timestamp: number;
}) => Promise<void>;
clearClocks: (options: { id: string }) => Promise<void>;
}
@@ -96,10 +96,12 @@ export const NbStoreNativeDBApis: NativeDBApis = {
id: string,
after?: Date | undefined | null
): Promise<DocClock[]> {
const clocks = await NbStore.getDocClocks({
id,
after: after?.getTime(),
});
const clocks = (
await NbStore.getDocClocks({
id,
after: after?.getTime(),
})
).clocks;
return clocks.map(c => ({
docId: c.docId,
timestamp: new Date(c.timestamp),
@@ -176,30 +178,30 @@ export const NbStoreNativeDBApis: NativeDBApis = {
id: string,
peer: string
): Promise<DocClock[]> {
const clocks = await NbStore.getPeerRemoteClocks({
id,
peer,
});
const clocks = (
await NbStore.getPeerRemoteClocks({
id,
peer,
})
).clocks;
return clocks.map(c => ({
docId: c.docId,
timestamp: new Date(c.timestamp),
}));
},
getPeerRemoteClock: async function (
id: string,
peer: string,
docId: string
): Promise<DocClock> {
getPeerRemoteClock: async function (id: string, peer: string, docId: string) {
const clock = await NbStore.getPeerRemoteClock({
id,
peer,
docId,
});
return {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
};
return clock
? {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
}
: null;
},
setPeerRemoteClock: async function (
id: string,
@@ -211,17 +213,19 @@ export const NbStoreNativeDBApis: NativeDBApis = {
id,
peer,
docId,
clock: clock.getTime(),
timestamp: clock.getTime(),
});
},
getPeerPulledRemoteClocks: async function (
id: string,
peer: string
): Promise<DocClock[]> {
const clocks = await NbStore.getPeerPulledRemoteClocks({
id,
peer,
});
const clocks = (
await NbStore.getPeerPulledRemoteClocks({
id,
peer,
})
).clocks;
return clocks.map(c => ({
docId: c.docId,
timestamp: new Date(c.timestamp),
@@ -231,16 +235,18 @@ export const NbStoreNativeDBApis: NativeDBApis = {
id: string,
peer: string,
docId: string
): Promise<DocClock> {
) {
const clock = await NbStore.getPeerPulledRemoteClock({
id,
peer,
docId,
});
return {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
};
return clock
? {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
}
: null;
},
setPeerPulledRemoteClock: async function (
id: string,
@@ -252,17 +258,19 @@ export const NbStoreNativeDBApis: NativeDBApis = {
id,
peer,
docId,
clock: clock.getTime(),
timestamp: clock.getTime(),
});
},
getPeerPushedClocks: async function (
id: string,
peer: string
): Promise<DocClock[]> {
const clocks = await NbStore.getPeerPushedClocks({
id,
peer,
});
const clocks = (
await NbStore.getPeerPushedClocks({
id,
peer,
})
).clocks;
return clocks.map(c => ({
docId: c.docId,
timestamp: new Date(c.timestamp),
@@ -272,16 +280,18 @@ export const NbStoreNativeDBApis: NativeDBApis = {
id: string,
peer: string,
docId: string
): Promise<DocClock> {
): Promise<DocClock | null> {
const clock = await NbStore.getPeerPushedClock({
id,
peer,
docId,
});
return {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
};
return clock
? {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
}
: null;
},
setPeerPushedClock: async function (
id: string,
@@ -293,7 +303,7 @@ export const NbStoreNativeDBApis: NativeDBApis = {
id,
peer,
docId,
clock: clock.getTime(),
timestamp: clock.getTime(),
});
},
clearClocks: async function (id: string): Promise<void> {
+189 -4
View File
@@ -1,6 +1,191 @@
import '@affine/core/bootstrap/browser';
import '@affine/component/theme';
import '@affine/core/mobile/styles/mobile.css';
// TODO(@L-Sun) Uncomment this when the `show` method implement by `@capacitor/keyboard` in ios
// import './virtual-keyboard';
/**
* the below code includes the custom fetch and websocket implementation for ios webview.
* should be included in the entry file of the app or webworker.
*/
/*
* we override the browser's fetch function with our custom fetch function to
* overcome the restrictions of cross-domain and third-party cookies in ios webview.
*
* the custom fetch function will convert the request to `affine-http://` or `affine-https://`
* and send the request to the server.
*/
const rawFetch = globalThis.fetch;
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
const url = new URL(
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url,
globalThis.location.origin
);
if (url.protocol === 'capacitor:') {
return rawFetch(input, init);
}
if (url.protocol === 'http:') {
url.protocol = 'affine-http:';
}
if (url.protocol === 'https:') {
url.protocol = 'affine-https:';
}
return rawFetch(url, input instanceof Request ? input : init);
};
/**
* we create a custom websocket class to simulate the browser's websocket connection
* through the custom url scheme handler.
*
* to overcome the restrictions of cross-domain and third-party cookies in ios webview,
* the front-end opens a websocket connection and sends a message by sending a request
* to `affine-ws://` or `affine-wss://`.
*
* the scheme has two endpoints:
*
* `affine-ws:///open?uuid={uuid}&url={wsUrl}`: opens a websocket connection and returns
* the received data via the SSE protocol.
* If the front-end closes the http connection, the websocket connection will also be closed.
*
* `affine-ws:///send?uuid={uuid}`: sends the request body data to the websocket connection
* with the specified uuid.
*/
class WrappedWebSocket {
static CLOSED = WebSocket.CLOSED;
static CLOSING = WebSocket.CLOSING;
static CONNECTING = WebSocket.CONNECTING;
static OPEN = WebSocket.OPEN;
readonly isWss: boolean;
readonly uuid = crypto.randomUUID();
readyState: number = WebSocket.CONNECTING;
events: Record<string, ((event: any) => void)[]> = {};
onopen: ((event: any) => void) | undefined = undefined;
onclose: ((event: any) => void) | undefined = undefined;
onerror: ((event: any) => void) | undefined = undefined;
onmessage: ((event: any) => void) | undefined = undefined;
eventSource: EventSource;
constructor(
readonly url: string,
_protocols?: string | string[] // not supported yet
) {
const parsedUrl = new URL(url);
this.isWss = parsedUrl.protocol === 'wss:';
this.eventSource = new EventSource(
`${this.isWss ? 'affine-wss' : 'affine-ws'}:///open?uuid=${this.uuid}&url=${encodeURIComponent(this.url)}`
);
this.eventSource.addEventListener('open', () => {
this.emitOpen(new Event('open'));
});
this.eventSource.addEventListener('error', () => {
this.eventSource.close();
this.emitError(new Event('error'));
this.emitClose(new CloseEvent('close'));
});
this.eventSource.addEventListener('message', data => {
const decodedData = JSON.parse(data.data);
if (decodedData.type === 'message') {
this.emitMessage(
new MessageEvent('message', { data: decodedData.data })
);
}
});
}
send(data: string) {
rawFetch(
`${this.isWss ? 'affine-wss' : 'affine-ws'}:///send?uuid=${this.uuid}`,
{
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
body: data,
}
).catch(e => {
console.error('Failed to send message', e);
});
}
close() {
this.eventSource.close();
this.emitClose(new CloseEvent('close'));
}
addEventListener(type: string, listener: (event: any) => void) {
this.events[type] = this.events[type] || [];
this.events[type].push(listener);
}
removeEventListener(type: string, listener: (event: any) => void) {
this.events[type] = this.events[type] || [];
this.events[type] = this.events[type].filter(l => l !== listener);
}
private emitOpen(event: Event) {
this.readyState = WebSocket.OPEN;
this.events['open']?.forEach(listener => {
try {
listener(event);
} catch (e) {
console.error(e);
}
});
try {
this.onopen?.(event);
} catch (e) {
console.error(e);
}
}
private emitClose(event: CloseEvent) {
this.readyState = WebSocket.CLOSED;
this.events['close']?.forEach(listener => {
try {
listener(event);
} catch (e) {
console.error(e);
}
});
try {
this.onclose?.(event);
} catch (e) {
console.error(e);
}
}
private emitMessage(event: MessageEvent) {
this.events['message']?.forEach(listener => {
try {
listener(event);
} catch (e) {
console.error(e);
}
});
try {
this.onmessage?.(event);
} catch (e) {
console.error(e);
}
}
private emitError(event: Event) {
this.events['error']?.forEach(listener => {
try {
listener(event);
} catch (e) {
console.error(e);
}
});
try {
this.onerror?.(event);
} catch (e) {
console.error(e);
}
}
}
globalThis.WebSocket = WrappedWebSocket as any;
+52
View File
@@ -0,0 +1,52 @@
import './setup';
import { broadcastChannelStorages } from '@affine/nbstore/broadcast-channel';
import { cloudStorages } from '@affine/nbstore/cloud';
import {
bindNativeDBApis,
type NativeDBApis,
sqliteStorages,
} from '@affine/nbstore/sqlite';
import {
WorkerConsumer,
type WorkerOps,
} from '@affine/nbstore/worker/consumer';
import { type MessageCommunicapable, OpConsumer } from '@toeverything/infra/op';
import { AsyncCall } from 'async-call-rpc';
globalThis.addEventListener('message', e => {
if (e.data.type === 'native-db-api-channel') {
const port = e.ports[0] as MessagePort;
const rpc = AsyncCall<NativeDBApis>(
{},
{
channel: {
on(listener) {
const f = (e: MessageEvent<any>) => {
listener(e.data);
};
port.addEventListener('message', f);
return () => {
port.removeEventListener('message', f);
};
},
send(data) {
port.postMessage(data);
},
},
}
);
bindNativeDBApis(rpc);
port.start();
}
});
const consumer = new OpConsumer<WorkerOps>(globalThis as MessageCommunicapable);
const worker = new WorkerConsumer([
...sqliteStorages,
...broadcastChannelStorages,
...cloudStorages,
]);
worker.bindConsumer(consumer);