feat(nbstore): improve nbstore (#9512)

This commit is contained in:
EYHN
2025-01-06 09:38:03 +00:00
parent a2563d2180
commit 46c8c4a408
103 changed files with 3337 additions and 3423 deletions
Generated
+16
View File
@@ -68,11 +68,13 @@ dependencies = [
"affine_schema", "affine_schema",
"anyhow", "anyhow",
"chrono", "chrono",
"dashmap",
"dotenvy", "dotenvy",
"napi", "napi",
"napi-build", "napi-build",
"napi-derive", "napi-derive",
"sqlx", "sqlx",
"thiserror 2.0.9",
"tokio", "tokio",
"uniffi", "uniffi",
] ]
@@ -273,6 +275,19 @@ dependencies = [
"nom", "nom",
] ]
[[package]]
name = "async-compat"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bab94bde396a3f7b4962e396fdad640e241ed797d4d8d77fc8c237d14c58fc0"
dependencies = [
"futures-core",
"futures-io",
"once_cell",
"pin-project-lite",
"tokio",
]
[[package]] [[package]]
name = "async-lock" name = "async-lock"
version = "3.4.0" version = "3.4.0"
@@ -2940,6 +2955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f" checksum = "bc7687007d2546c454d8ae609b105daceb88175477dac280707ad6d95bcd6f1f"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-compat",
"bytes", "bytes",
"log", "log",
"once_cell", "once_cell",
+1
View File
@@ -16,4 +16,5 @@ declare module 'y-protocols/awareness.js' {
value: State[Field] value: State[Field]
): void; ): void;
} }
export { applyAwarenessUpdate, encodeAwarenessUpdate, modifyAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'
} }
+3
View File
@@ -32,6 +32,9 @@ export class UaHelper {
} }
private isStandaloneMode() { private isStandaloneMode() {
if (typeof window === 'undefined') {
return false;
}
if ('standalone' in window.navigator) { if ('standalone' in window.navigator) {
return !!window.navigator.standalone; return !!window.navigator.standalone;
} }
@@ -27,7 +27,9 @@ describe('op client', () => {
port1.postMessage = vi.fn(port1.postMessage); port1.postMessage = vi.fn(port1.postMessage);
// @ts-expect-error patch postMessage // @ts-expect-error patch postMessage
ctx.postMessage = port1.postMessage; ctx.postMessage = port1.postMessage;
ctx.producer = new OpClient(port1); ctx.producer = new OpClient(port1, {
timeout: 1000,
});
// @ts-expect-error internal api // @ts-expect-error internal api
ctx.handlers = ctx.producer.handlers; ctx.handlers = ctx.producer.handlers;
vi.useFakeTimers(); vi.useFakeTimers();
+7 -4
View File
@@ -31,7 +31,7 @@ export class OpClient<Ops extends OpSchema> extends AutoMessageHandler {
private readonly pendingCalls = new Map<string, PendingCall>(); private readonly pendingCalls = new Map<string, PendingCall>();
private readonly obs = new Map<string, Observer<any>>(); private readonly obs = new Map<string, Observer<any>>();
private readonly options: OpClientOptions = { private readonly options: OpClientOptions = {
timeout: 3000, timeout: Infinity,
}; };
constructor(port: MessageCommunicapable, options: OpClientOptions = {}) { constructor(port: MessageCommunicapable, options: OpClientOptions = {}) {
@@ -139,9 +139,12 @@ export class OpClient<Ops extends OpSchema> extends AutoMessageHandler {
raise('canceled'); raise('canceled');
}; };
const timeout = setTimeout(() => { const timeout =
raise('timeout'); this.options.timeout === Infinity
}, this.options.timeout); ? 0
: setTimeout(() => {
raise('timeout');
}, this.options.timeout);
const transferables = fetchTransferables(payload); const transferables = fetchTransferables(payload);
+2 -3
View File
@@ -6,7 +6,8 @@
"sideEffects": false, "sideEffects": false,
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",
"./worker": "./src/worker/index.ts", "./worker/client": "./src/worker/client.ts",
"./worker/consumer": "./src/worker/consumer.ts",
"./idb": "./src/impls/idb/index.ts", "./idb": "./src/impls/idb/index.ts",
"./idb/v1": "./src/impls/idb/v1/index.ts", "./idb/v1": "./src/impls/idb/v1/index.ts",
"./cloud": "./src/impls/cloud/index.ts", "./cloud": "./src/impls/cloud/index.ts",
@@ -24,7 +25,6 @@
"yjs": "^13.6.21" "yjs": "^13.6.21"
}, },
"devDependencies": { "devDependencies": {
"@affine/electron-api": "workspace:*",
"@affine/graphql": "workspace:*", "@affine/graphql": "workspace:*",
"fake-indexeddb": "^6.0.0", "fake-indexeddb": "^6.0.0",
"idb": "^8.0.0", "idb": "^8.0.0",
@@ -32,7 +32,6 @@
"vitest": "2.1.8" "vitest": "2.1.8"
}, },
"peerDependencies": { "peerDependencies": {
"@affine/electron-api": "workspace:*",
"@affine/graphql": "workspace:*", "@affine/graphql": "workspace:*",
"idb": "^8.0.0", "idb": "^8.0.0",
"socket.io-client": "^4.7.5" "socket.io-client": "^4.7.5"
@@ -9,6 +9,7 @@ import { DocFrontend } from '../frontend/doc';
import { BroadcastChannelAwarenessStorage } from '../impls/broadcast-channel/awareness'; import { BroadcastChannelAwarenessStorage } from '../impls/broadcast-channel/awareness';
import { IndexedDBDocStorage } from '../impls/idb'; import { IndexedDBDocStorage } from '../impls/idb';
import { AwarenessSyncImpl } from '../sync/awareness'; import { AwarenessSyncImpl } from '../sync/awareness';
import { DocSyncImpl } from '../sync/doc';
import { expectYjsEqual } from './utils'; import { expectYjsEqual } from './utils';
test('doc', async () => { test('doc', async () => {
@@ -19,7 +20,7 @@ test('doc', async () => {
const docStorage = new IndexedDBDocStorage({ const docStorage = new IndexedDBDocStorage({
id: 'ws1', id: 'ws1',
peer: 'a', flavour: 'a',
type: 'workspace', type: 'workspace',
}); });
@@ -27,7 +28,7 @@ test('doc', async () => {
await docStorage.connection.waitForConnected(); await docStorage.connection.waitForConnected();
const frontend1 = new DocFrontend(docStorage, null); const frontend1 = new DocFrontend(docStorage, DocSyncImpl.dummy);
frontend1.start(); frontend1.start();
frontend1.addDoc(doc1); frontend1.addDoc(doc1);
await vitest.waitFor(async () => { await vitest.waitFor(async () => {
@@ -42,7 +43,7 @@ test('doc', async () => {
const doc2 = new YDoc({ const doc2 = new YDoc({
guid: 'test-doc', guid: 'test-doc',
}); });
const frontend2 = new DocFrontend(docStorage, null); const frontend2 = new DocFrontend(docStorage, DocSyncImpl.dummy);
frontend2.start(); frontend2.start();
frontend2.addDoc(doc2); frontend2.addDoc(doc2);
@@ -57,15 +58,11 @@ test('doc', async () => {
test('awareness', async () => { test('awareness', async () => {
const storage1 = new BroadcastChannelAwarenessStorage({ const storage1 = new BroadcastChannelAwarenessStorage({
id: 'ws1', id: 'ws1:a',
peer: 'a',
type: 'workspace',
}); });
const storage2 = new BroadcastChannelAwarenessStorage({ const storage2 = new BroadcastChannelAwarenessStorage({
id: 'ws1', id: 'ws1:b',
peer: 'b',
type: 'workspace',
}); });
storage1.connection.connect(); storage1.connection.connect();
@@ -90,13 +87,23 @@ test('awareness', async () => {
const awarenessC = new Awareness(docC); const awarenessC = new Awareness(docC);
{ {
const sync = new AwarenessSyncImpl(storage1, [storage2]); const sync = new AwarenessSyncImpl({
local: storage1,
remotes: {
b: storage2,
},
});
const frontend = new AwarenessFrontend(sync); const frontend = new AwarenessFrontend(sync);
frontend.connect(awarenessA); frontend.connect(awarenessA);
frontend.connect(awarenessB); frontend.connect(awarenessB);
} }
{ {
const sync = new AwarenessSyncImpl(storage2, [storage1]); const sync = new AwarenessSyncImpl({
local: storage2,
remotes: {
a: storage1,
},
});
const frontend = new AwarenessFrontend(sync); const frontend = new AwarenessFrontend(sync);
frontend.connect(awarenessC); frontend.connect(awarenessC);
} }
@@ -19,30 +19,37 @@ test('doc', async () => {
const peerADoc = new IndexedDBDocStorage({ const peerADoc = new IndexedDBDocStorage({
id: 'ws1', id: 'ws1',
peer: 'a', flavour: 'a',
type: 'workspace', type: 'workspace',
}); });
const peerASync = new IndexedDBSyncStorage({ const peerASync = new IndexedDBSyncStorage({
id: 'ws1', id: 'ws1',
peer: 'a', flavour: 'a',
type: 'workspace', type: 'workspace',
}); });
const peerBDoc = new IndexedDBDocStorage({ const peerBDoc = new IndexedDBDocStorage({
id: 'ws1', id: 'ws1',
peer: 'b', flavour: 'b',
type: 'workspace', type: 'workspace',
}); });
const peerCDoc = new IndexedDBDocStorage({ const peerCDoc = new IndexedDBDocStorage({
id: 'ws1', id: 'ws1',
peer: 'c', flavour: 'c',
type: 'workspace', type: 'workspace',
}); });
const peerA = new SpaceStorage([peerADoc, peerASync]); const peerA = new SpaceStorage({
const peerB = new SpaceStorage([peerBDoc]); doc: peerADoc,
const peerC = new SpaceStorage([peerCDoc]); sync: peerASync,
});
const peerB = new SpaceStorage({
doc: peerBDoc,
});
const peerC = new SpaceStorage({
doc: peerCDoc,
});
peerA.connect(); peerA.connect();
peerB.connect(); peerB.connect();
@@ -57,7 +64,13 @@ test('doc', async () => {
bin: update, bin: update,
}); });
const sync = new Sync(peerA, [peerB, peerC]); const sync = new Sync({
local: peerA,
remotes: {
b: peerB,
c: peerC,
},
});
sync.start(); sync.start();
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
@@ -109,25 +122,31 @@ test('doc', async () => {
test('blob', async () => { test('blob', async () => {
const a = new IndexedDBBlobStorage({ const a = new IndexedDBBlobStorage({
id: 'ws1', id: 'ws1',
peer: 'a', flavour: 'a',
type: 'workspace', type: 'workspace',
}); });
const b = new IndexedDBBlobStorage({ const b = new IndexedDBBlobStorage({
id: 'ws1', id: 'ws1',
peer: 'b', flavour: 'b',
type: 'workspace', type: 'workspace',
}); });
const c = new IndexedDBBlobStorage({ const c = new IndexedDBBlobStorage({
id: 'ws1', id: 'ws1',
peer: 'c', flavour: 'c',
type: 'workspace', type: 'workspace',
}); });
const peerA = new SpaceStorage([a]); const peerA = new SpaceStorage({
const peerB = new SpaceStorage([b]); blob: a,
const peerC = new SpaceStorage([c]); });
const peerB = new SpaceStorage({
blob: b,
});
const peerC = new SpaceStorage({
blob: c,
});
peerA.connect(); peerA.connect();
peerB.connect(); peerB.connect();
@@ -151,7 +170,13 @@ test('blob', async () => {
createdAt: new Date(100), createdAt: new Date(100),
}); });
const sync = new Sync(peerA, [peerB, peerC]); const sync = new Sync({
local: peerA,
remotes: {
b: peerB,
c: peerC,
},
});
sync.start(); sync.start();
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
@@ -92,6 +92,7 @@ export abstract class AutoReconnectConnection<T = any>
}) })
.catch(error => { .catch(error => {
if (!this.connectingAbort?.signal.aborted) { if (!this.connectingAbort?.signal.aborted) {
console.error('failed to connect', error);
this.setStatus('error', error as any); this.setStatus('error', error as any);
} }
}); });
@@ -3,7 +3,7 @@ import {
applyAwarenessUpdate, applyAwarenessUpdate,
type Awareness, type Awareness,
encodeAwarenessUpdate, encodeAwarenessUpdate,
} from 'y-protocols/awareness.js'; } from 'y-protocols/awareness';
import type { AwarenessRecord } from '../storage/awareness'; import type { AwarenessRecord } from '../storage/awareness';
import type { AwarenessSync } from '../sync/awareness'; import type { AwarenessSync } from '../sync/awareness';
+18 -6
View File
@@ -3,21 +3,33 @@ import type { BlobSync } from '../sync/blob';
export class BlobFrontend { export class BlobFrontend {
constructor( constructor(
readonly storage: BlobStorage, public readonly storage: BlobStorage,
readonly sync?: BlobSync private readonly sync: BlobSync
) {} ) {}
get(blobId: string) { get(blobId: string) {
return this.sync return this.sync.downloadBlob(blobId);
? this.sync.downloadBlob(blobId)
: this.storage.get(blobId);
} }
set(blob: BlobRecord) { set(blob: BlobRecord) {
return this.sync ? this.sync.uploadBlob(blob) : this.storage.set(blob); return this.sync.uploadBlob(blob);
}
fullSync() {
return this.sync.fullSync();
} }
addPriority(_id: string, _priority: number) { addPriority(_id: string, _priority: number) {
// not support yet // not support yet
} }
readonly state$ = this.sync.state$;
setMaxBlobSize(max: number) {
this.sync.setMaxBlobSize(max);
}
onReachedMaxBlobSize(cb: (byteSize: number) => void): () => void {
return this.sync.onReachedMaxBlobSize(cb);
}
} }
+214 -5
View File
@@ -1,6 +1,7 @@
import { groupBy } from 'lodash-es'; import { groupBy } from 'lodash-es';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { Subject } from 'rxjs'; import type { Subscription } from 'rxjs';
import { combineLatest, map, Observable, Subject } from 'rxjs';
import { import {
applyUpdate, applyUpdate,
type Doc as YDoc, type Doc as YDoc,
@@ -12,7 +13,7 @@ import type { DocRecord, DocStorage } from '../storage';
import type { DocSync } from '../sync/doc'; import type { DocSync } from '../sync/doc';
import { AsyncPriorityQueue } from '../utils/async-priority-queue'; import { AsyncPriorityQueue } from '../utils/async-priority-queue';
import { isEmptyUpdate } from '../utils/is-empty-update'; import { isEmptyUpdate } from '../utils/is-empty-update';
import { throwIfAborted } from '../utils/throw-if-aborted'; import { MANUALLY_STOP, throwIfAborted } from '../utils/throw-if-aborted';
const NBSTORE_ORIGIN = 'nbstore-frontend'; const NBSTORE_ORIGIN = 'nbstore-frontend';
@@ -36,6 +37,64 @@ interface DocFrontendOptions {
mergeUpdates?: (updates: Uint8Array[]) => Promise<Uint8Array> | Uint8Array; mergeUpdates?: (updates: Uint8Array[]) => Promise<Uint8Array> | Uint8Array;
} }
export type DocFrontendDocState = {
/**
* some data is available in yjs doc instance
*/
ready: boolean;
/**
* data is loaded from local doc storage and applied to yjs doc instance
*/
loaded: boolean;
/**
* some data is being applied to yjs doc instance, or some data is being saved to local doc storage
*/
updating: boolean;
/**
* the doc is syncing with remote peers
*/
syncing: boolean;
/**
* the doc is synced with remote peers
*/
synced: boolean;
/**
* the doc is retrying to sync with remote peers
*/
syncRetrying: boolean;
/**
* the error message when syncing with remote peers
*/
syncErrorMessage: string | null;
};
export type DocFrontendState = {
/**
* total number of docs
*/
total: number;
/**
* number of docs that have been loaded to yjs doc instance
*/
loaded: number;
/**
* number of docs that are syncing with remote peers
*/
syncing: number;
/**
* whether all docs are synced with remote peers
*/
synced: boolean;
/**
* whether the doc is retrying to sync with remote peers
*/
syncRetrying: boolean;
/**
* the error message when syncing with remote peers
*/
syncErrorMessage: string | null;
};
export class DocFrontend { export class DocFrontend {
private readonly uniqueId = `frontend:${nanoid()}`; private readonly uniqueId = `frontend:${nanoid()}`;
@@ -55,11 +114,68 @@ export class DocFrontend {
private readonly abort = new AbortController(); private readonly abort = new AbortController();
constructor( constructor(
private readonly storage: DocStorage, public readonly storage: DocStorage,
private readonly sync: DocSync | null, private readonly sync: DocSync,
readonly options: DocFrontendOptions = {} readonly options: DocFrontendOptions = {}
) {} ) {}
docState$(docId: string): Observable<DocFrontendDocState> {
const frontendState$ = new Observable<{
ready: boolean;
loaded: boolean;
updating: boolean;
}>(subscribe => {
const next = () => {
subscribe.next({
ready: this.status.readyDocs.has(docId),
loaded: this.status.connectedDocs.has(docId),
updating:
(this.status.jobMap.get(docId)?.length ?? 0) > 0 ||
this.status.currentJob?.docId === docId,
});
};
next();
return this.statusUpdatedSubject$.subscribe(updatedId => {
if (updatedId === docId) next();
});
});
const syncState$ = this.sync.docState$(docId);
return combineLatest([frontendState$, syncState$]).pipe(
map(([frontend, sync]) => ({
...frontend,
synced: sync.synced,
syncing: sync.syncing,
syncRetrying: sync.retrying,
syncErrorMessage: sync.errorMessage,
}))
);
}
state$ = combineLatest([
new Observable<{ total: number; loaded: number }>(subscriber => {
const next = () => {
subscriber.next({
total: this.status.docs.size,
loaded: this.status.connectedDocs.size,
});
};
next();
return this.statusUpdatedSubject$.subscribe(() => {
next();
});
}),
this.sync.state$,
]).pipe(
map(([frontend, sync]) => ({
total: sync.total ?? frontend.total,
loaded: frontend.loaded,
syncing: sync.syncing,
synced: sync.synced,
syncRetrying: sync.retrying,
syncErrorMessage: sync.errorMessage,
}))
) satisfies Observable<DocFrontendState>;
start() { start() {
if (this.abort.signal.aborted) { if (this.abort.signal.aborted) {
throw new Error('doc frontend can only start once'); throw new Error('doc frontend can only start once');
@@ -70,10 +186,11 @@ export class DocFrontend {
} }
stop() { stop() {
this.abort.abort(); this.abort.abort(MANUALLY_STOP);
} }
private async mainLoop(signal?: AbortSignal) { private async mainLoop(signal?: AbortSignal) {
await this.storage.connection.waitForConnected(signal);
const dispose = this.storage.subscribeDocUpdate((record, origin) => { const dispose = this.storage.subscribeDocUpdate((record, origin) => {
this.event.onStorageUpdate(record, origin); this.event.onStorageUpdate(record, origin);
}); });
@@ -314,4 +431,96 @@ export class DocFrontend {
return merge(updates.filter(bin => !isEmptyUpdate(bin))); return merge(updates.filter(bin => !isEmptyUpdate(bin)));
} }
async waitForSynced(abort?: AbortSignal) {
let sub: Subscription | undefined = undefined;
return Promise.race([
new Promise<void>(resolve => {
sub = this.state$?.subscribe(status => {
if (status.synced) {
resolve();
}
});
}),
new Promise<void>((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]).finally(() => {
sub?.unsubscribe();
});
}
async waitForDocLoaded(docId: string, abort?: AbortSignal) {
let sub: Subscription | undefined = undefined;
return Promise.race([
new Promise<void>(resolve => {
sub = this.docState$(docId).subscribe(state => {
if (state.loaded) {
resolve();
}
});
}),
new Promise<void>((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]).finally(() => {
sub?.unsubscribe();
});
}
async waitForDocSynced(docId: string, abort?: AbortSignal) {
let sub: Subscription | undefined = undefined;
return Promise.race([
new Promise<void>(resolve => {
sub = this.docState$(docId).subscribe(state => {
if (state.syncing) {
resolve();
}
});
}),
new Promise<void>((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]).finally(() => {
sub?.unsubscribe();
});
}
async waitForDocReady(docId: string, abort?: AbortSignal) {
let sub: Subscription | undefined = undefined;
return Promise.race([
new Promise<void>(resolve => {
sub = this.docState$(docId).subscribe(state => {
if (state.ready) {
resolve();
}
});
}),
new Promise<void>((_, reject) => {
if (abort?.aborted) {
reject(abort?.reason);
}
abort?.addEventListener('abort', () => {
reject(abort.reason);
});
}),
]).finally(() => {
sub?.unsubscribe();
});
}
} }
@@ -0,0 +1,3 @@
export * from './awareness';
export * from './blob';
export * from './doc';
@@ -22,13 +22,27 @@ type ChannelMessage =
collectId: string; collectId: string;
}; };
interface BroadcastChannelAwarenessStorageOptions {
id: string;
}
export class BroadcastChannelAwarenessStorage extends AwarenessStorageBase { export class BroadcastChannelAwarenessStorage extends AwarenessStorageBase {
static readonly identifier = 'BroadcastChannelAwarenessStorage';
override readonly storageType = 'awareness'; override readonly storageType = 'awareness';
override readonly connection = new BroadcastChannelConnection(this.options); override readonly connection = new BroadcastChannelConnection({
id: this.options.id,
});
get channel() { get channel() {
return this.connection.inner; return this.connection.inner;
} }
constructor(
private readonly options: BroadcastChannelAwarenessStorageOptions
) {
super();
}
private readonly subscriptions = new Map< private readonly subscriptions = new Map<
string, string,
Set<{ Set<{
@@ -1,10 +1,13 @@
import { AutoReconnectConnection } from '../../connection'; import { AutoReconnectConnection } from '../../connection';
import type { StorageOptions } from '../../storage';
export interface BroadcastChannelConnectionOptions {
id: string;
}
export class BroadcastChannelConnection extends AutoReconnectConnection<BroadcastChannel> { export class BroadcastChannelConnection extends AutoReconnectConnection<BroadcastChannel> {
readonly channelName = `channel:${this.opts.peer}:${this.opts.type}:${this.opts.id}`; readonly channelName = `channel:${this.opts.id}`;
constructor(private readonly opts: StorageOptions) { constructor(private readonly opts: BroadcastChannelConnectionOptions) {
super(); super();
} }
@@ -0,0 +1,6 @@
import type { StorageConstructor } from '..';
import { BroadcastChannelAwarenessStorage } from './awareness';
export const broadcastChannelStorages = [
BroadcastChannelAwarenessStorage,
] satisfies StorageConstructor[];
@@ -4,21 +4,33 @@ import { share } from '../../connection';
import { import {
type AwarenessRecord, type AwarenessRecord,
AwarenessStorageBase, AwarenessStorageBase,
type AwarenessStorageOptions,
} from '../../storage/awareness'; } from '../../storage/awareness';
import type { SpaceType } from '../../utils/universal-id';
import { import {
base64ToUint8Array, base64ToUint8Array,
SocketConnection, SocketConnection,
uint8ArrayToBase64, uint8ArrayToBase64,
} from './socket'; } from './socket';
interface CloudAwarenessStorageOptions extends AwarenessStorageOptions { interface CloudAwarenessStorageOptions {
socketOptions: SocketOptions; socketOptions?: SocketOptions;
serverBaseUrl: string;
type: SpaceType;
id: string;
} }
export class CloudAwarenessStorage extends AwarenessStorageBase<CloudAwarenessStorageOptions> { export class CloudAwarenessStorage extends AwarenessStorageBase {
static readonly identifier = 'CloudAwarenessStorage';
constructor(private readonly options: CloudAwarenessStorageOptions) {
super();
}
connection = share( connection = share(
new SocketConnection(this.peer, this.options.socketOptions) new SocketConnection(
`${this.options.serverBaseUrl}/`,
this.options.socketOptions
)
); );
private get socket() { private get socket() {
@@ -28,8 +40,8 @@ export class CloudAwarenessStorage extends AwarenessStorageBase<CloudAwarenessSt
override async update(record: AwarenessRecord): Promise<void> { override async update(record: AwarenessRecord): Promise<void> {
const encodedUpdate = await uint8ArrayToBase64(record.bin); const encodedUpdate = await uint8ArrayToBase64(record.bin);
this.socket.emit('space:update-awareness', { this.socket.emit('space:update-awareness', {
spaceType: this.spaceType, spaceType: this.options.type,
spaceId: this.spaceId, spaceId: this.options.id,
docId: record.docId, docId: record.docId,
awarenessUpdate: encodedUpdate, awarenessUpdate: encodedUpdate,
}); });
@@ -44,8 +56,8 @@ export class CloudAwarenessStorage extends AwarenessStorageBase<CloudAwarenessSt
// leave awareness // leave awareness
const leave = () => { const leave = () => {
this.socket.emit('space:leave-awareness', { this.socket.emit('space:leave-awareness', {
spaceType: this.spaceType, spaceType: this.options.type,
spaceId: this.spaceId, spaceId: this.options.id,
docId: id, docId: id,
}); });
}; };
@@ -53,14 +65,14 @@ export class CloudAwarenessStorage extends AwarenessStorageBase<CloudAwarenessSt
// join awareness, and collect awareness from others // join awareness, and collect awareness from others
const joinAndCollect = async () => { const joinAndCollect = async () => {
await this.socket.emitWithAck('space:join-awareness', { await this.socket.emitWithAck('space:join-awareness', {
spaceType: this.spaceType, spaceType: this.options.type,
spaceId: this.spaceId, spaceId: this.options.id,
docId: id, docId: id,
clientVersion: BUILD_CONFIG.appVersion, clientVersion: BUILD_CONFIG.appVersion,
}); });
this.socket.emit('space:load-awarenesses', { this.socket.emit('space:load-awarenesses', {
spaceType: this.spaceType, spaceType: this.options.type,
spaceId: this.spaceId, spaceId: this.options.id,
docId: id, docId: id,
}); });
}; };
@@ -87,8 +99,8 @@ export class CloudAwarenessStorage extends AwarenessStorageBase<CloudAwarenessSt
docId: string; docId: string;
}) => { }) => {
if ( if (
spaceId === this.spaceId && spaceId === this.options.id &&
spaceType === this.spaceType && spaceType === this.options.type &&
docId === id docId === id
) { ) {
(async () => { (async () => {
@@ -96,8 +108,8 @@ export class CloudAwarenessStorage extends AwarenessStorageBase<CloudAwarenessSt
if (record) { if (record) {
const encodedUpdate = await uint8ArrayToBase64(record.bin); const encodedUpdate = await uint8ArrayToBase64(record.bin);
this.socket.emit('space:update-awareness', { this.socket.emit('space:update-awareness', {
spaceType: this.spaceType, spaceType: this.options.type,
spaceId: this.spaceId, spaceId: this.options.id,
docId: record.docId, docId: record.docId,
awarenessUpdate: encodedUpdate, awarenessUpdate: encodedUpdate,
}); });
@@ -118,8 +130,8 @@ export class CloudAwarenessStorage extends AwarenessStorageBase<CloudAwarenessSt
awarenessUpdate: string; awarenessUpdate: string;
}) => { }) => {
if ( if (
spaceId === this.spaceId && spaceId === this.options.id &&
spaceType === this.spaceType && spaceType === this.options.type &&
docId === id docId === id
) { ) {
onUpdate({ onUpdate({
+36 -37
View File
@@ -1,35 +1,30 @@
import { import {
deleteBlobMutation, deleteBlobMutation,
gqlFetcherFactory,
listBlobsQuery, listBlobsQuery,
releaseDeletedBlobsMutation, releaseDeletedBlobsMutation,
setBlobMutation, setBlobMutation,
} from '@affine/graphql'; } from '@affine/graphql';
import { DummyConnection } from '../../connection'; import { type BlobRecord, BlobStorageBase } from '../../storage';
import { import { HttpConnection } from './http';
type BlobRecord,
BlobStorageBase,
type BlobStorageOptions,
} from '../../storage';
interface CloudBlobStorageOptions extends BlobStorageOptions { interface CloudBlobStorageOptions {
apiBaseUrl: string; serverBaseUrl: string;
id: string;
} }
export class CloudBlobStorage extends BlobStorageBase<CloudBlobStorageOptions> { export class CloudBlobStorage extends BlobStorageBase {
private readonly gql = gqlFetcherFactory( static readonly identifier = 'CloudBlobStorage';
this.options.apiBaseUrl + '/graphql'
); constructor(private readonly options: CloudBlobStorageOptions) {
override connection = new DummyConnection(); super();
}
readonly connection = new HttpConnection(this.options.serverBaseUrl);
override async get(key: string) { override async get(key: string) {
const res = await fetch( const res = await this.connection.fetch(
this.options.apiBaseUrl + '/api/workspaces/' + this.options.id + '/blobs/' + key,
'/api/workspaces/' +
this.spaceId +
'/blobs/' +
key,
{ {
cache: 'default', cache: 'default',
headers: { headers: {
@@ -38,49 +33,53 @@ export class CloudBlobStorage extends BlobStorageBase<CloudBlobStorageOptions> {
} }
); );
if (!res.ok) { if (res.status === 404) {
return null; return null;
} }
const data = await res.arrayBuffer(); try {
const blob = await res.blob();
return { return {
key, key,
data: new Uint8Array(data), data: new Uint8Array(await blob.arrayBuffer()),
mime: res.headers.get('content-type') || '', mime: blob.type,
size: data.byteLength, size: blob.size,
createdAt: new Date(res.headers.get('last-modified') || Date.now()), createdAt: new Date(res.headers.get('last-modified') || Date.now()),
}; };
} catch (err) {
throw new Error('blob download error: ' + err);
}
} }
override async set(blob: BlobRecord) { override async set(blob: BlobRecord) {
await this.gql({ await this.connection.gql({
query: setBlobMutation, query: setBlobMutation,
variables: { variables: {
workspaceId: this.spaceId, workspaceId: this.options.id,
blob: new File([blob.data], blob.key, { type: blob.mime }), blob: new File([blob.data], blob.key, { type: blob.mime }),
}, },
}); });
} }
override async delete(key: string, permanently: boolean) { override async delete(key: string, permanently: boolean) {
await this.gql({ await this.connection.gql({
query: deleteBlobMutation, query: deleteBlobMutation,
variables: { workspaceId: this.spaceId, key, permanently }, variables: { workspaceId: this.options.id, key, permanently },
}); });
} }
override async release() { override async release() {
await this.gql({ await this.connection.gql({
query: releaseDeletedBlobsMutation, query: releaseDeletedBlobsMutation,
variables: { workspaceId: this.spaceId }, variables: { workspaceId: this.options.id },
}); });
} }
override async list() { override async list() {
const res = await this.gql({ const res = await this.connection.gql({
query: listBlobsQuery, query: listBlobsQuery,
variables: { workspaceId: this.spaceId }, variables: { workspaceId: this.options.id },
}); });
return res.workspace.blobs.map(blob => ({ return res.workspace.blobs.map(blob => ({
@@ -0,0 +1,82 @@
import {
type DocClock,
type DocClocks,
type DocRecord,
DocStorageBase,
type DocStorageOptions,
type DocUpdate,
} from '../../storage';
import { HttpConnection } from './http';
interface CloudDocStorageOptions extends DocStorageOptions {
serverBaseUrl: string;
}
export class StaticCloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
static readonly identifier = 'StaticCloudDocStorage';
constructor(options: CloudDocStorageOptions) {
super({ ...options, readonlyMode: true });
}
override connection = new HttpConnection(this.options.serverBaseUrl);
override async pushDocUpdate(
update: DocUpdate,
_origin?: string
): Promise<DocClock> {
// http is readonly
return { docId: update.docId, timestamp: new Date() };
}
override async getDocTimestamp(docId: string): Promise<DocClock | null> {
// http doesn't support this, so we just return a new timestamp
return {
docId,
timestamp: new Date(),
};
}
override async getDocTimestamps(): Promise<DocClocks> {
// http doesn't support this
return {};
}
override deleteDoc(_docId: string): Promise<void> {
// http is readonly
return Promise.resolve();
}
protected override async getDocSnapshot(
docId: string
): Promise<DocRecord | null> {
const arrayBuffer = await this.connection.fetchArrayBuffer(
`/api/workspaces/${this.spaceId}/docs/${docId}`,
{
priority: 'high',
headers: {
Accept: 'application/octet-stream', // this is necessary for ios native fetch to return arraybuffer
},
}
);
if (!arrayBuffer) {
return null;
}
return {
docId: docId,
bin: new Uint8Array(arrayBuffer),
timestamp: new Date(),
};
}
protected override setDocSnapshot(
_snapshot: DocRecord,
_prevSnapshot: DocRecord | null
): Promise<boolean> {
// http is readonly
return Promise.resolve(false);
}
protected override getDocUpdates(_docId: string): Promise<DocRecord[]> {
return Promise.resolve([]);
}
protected override markUpdatesMerged(
_docId: string,
_updates: DocRecord[]
): Promise<number> {
return Promise.resolve(0);
}
}
@@ -12,6 +12,7 @@ import {
type DocStorageOptions, type DocStorageOptions,
type DocUpdate, type DocUpdate,
} from '../../storage'; } from '../../storage';
import type { SpaceType } from '../../utils/universal-id';
import { import {
base64ToUint8Array, base64ToUint8Array,
type ServerEventsMap, type ServerEventsMap,
@@ -20,15 +21,20 @@ import {
} from './socket'; } from './socket';
interface CloudDocStorageOptions extends DocStorageOptions { interface CloudDocStorageOptions extends DocStorageOptions {
socketOptions: SocketOptions; socketOptions?: SocketOptions;
serverBaseUrl: string; serverBaseUrl: string;
type: SpaceType;
} }
export class CloudDocStorage extends DocStorageBase<CloudDocStorageOptions> { export class CloudDocStorage extends DocStorageBase<CloudDocStorageOptions> {
static readonly identifier = 'CloudDocStorage';
get socket() { get socket() {
return this.connection.inner; return this.connection.inner;
} }
readonly spaceType = this.options.type;
onServerUpdate: ServerEventsMap['space:broadcast-doc-update'] = message => { onServerUpdate: ServerEventsMap['space:broadcast-doc-update'] = message => {
if ( if (
this.spaceType === message.spaceType && this.spaceType === message.spaceType &&
@@ -0,0 +1,69 @@
import { gqlFetcherFactory } from '@affine/graphql';
import { DummyConnection } from '../../connection';
export class HttpConnection extends DummyConnection {
readonly fetch = async (input: string, init?: RequestInit) => {
const externalSignal = init?.signal;
if (externalSignal?.aborted) {
throw externalSignal.reason;
}
const abortController = new AbortController();
externalSignal?.addEventListener('abort', reason => {
abortController.abort(reason);
});
const timeout = 15000;
const timeoutId = setTimeout(() => {
abortController.abort('timeout');
}, timeout);
const res = await globalThis
.fetch(new URL(input, this.serverBaseUrl), {
...init,
signal: abortController.signal,
headers: {
...init?.headers,
'x-affine-version': BUILD_CONFIG.appVersion,
},
})
.catch(err => {
throw new Error('fetch error: ' + err);
});
clearTimeout(timeoutId);
if (!res.ok && res.status !== 404) {
let reason: string | any = '';
if (res.headers.get('Content-Type')?.includes('application/json')) {
try {
reason = await res.json();
} catch {
// ignore
}
}
throw new Error('fetch error status: ' + res.status + ' ' + reason);
}
return res;
};
readonly fetchArrayBuffer = async (input: string, init?: RequestInit) => {
const res = await this.fetch(input, init);
if (res.status === 404) {
// 404
return null;
}
try {
return await res.arrayBuffer();
} catch (err) {
throw new Error('fetch download error: ' + err);
}
};
readonly gql = gqlFetcherFactory(
new URL('/graphql', this.serverBaseUrl).href,
this.fetch
);
constructor(private readonly serverBaseUrl: string) {
super();
}
}
@@ -1,3 +1,17 @@
import type { StorageConstructor } from '..';
import { CloudAwarenessStorage } from './awareness';
import { CloudBlobStorage } from './blob';
import { CloudDocStorage } from './doc';
import { StaticCloudDocStorage } from './doc-static';
export * from './awareness'; export * from './awareness';
export * from './blob'; export * from './blob';
export * from './doc'; export * from './doc';
export * from './doc-static';
export const cloudStorages = [
CloudDocStorage,
StaticCloudDocStorage,
CloudBlobStorage,
CloudAwarenessStorage,
] satisfies StorageConstructor[];
@@ -162,7 +162,7 @@ export class SocketConnection extends AutoReconnectConnection<Socket> {
constructor( constructor(
private readonly endpoint: string, private readonly endpoint: string,
private readonly socketOptions: SocketOptions private readonly socketOptions?: SocketOptions
) { ) {
super(); super();
} }
@@ -4,11 +4,17 @@ import {
BlobStorageBase, BlobStorageBase,
type ListedBlobRecord, type ListedBlobRecord,
} from '../../storage'; } from '../../storage';
import { IDBConnection } from './db'; import { IDBConnection, type IDBConnectionOptions } from './db';
export class IndexedDBBlobStorage extends BlobStorageBase { export class IndexedDBBlobStorage extends BlobStorageBase {
static readonly identifier = 'IndexedDBBlobStorage';
readonly connection = share(new IDBConnection(this.options)); readonly connection = share(new IDBConnection(this.options));
constructor(private readonly options: IDBConnectionOptions) {
super();
}
get db() { get db() {
return this.connection.inner.db; return this.connection.inner.db;
} }
+9 -3
View File
@@ -1,20 +1,26 @@
import { type IDBPDatabase, openDB } from 'idb'; import { type IDBPDatabase, openDB } from 'idb';
import { AutoReconnectConnection } from '../../connection'; import { AutoReconnectConnection } from '../../connection';
import type { StorageOptions } from '../../storage'; import type { SpaceType } from '../../utils/universal-id';
import { type DocStorageSchema, migrator } from './schema'; import { type DocStorageSchema, migrator } from './schema';
export interface IDBConnectionOptions {
flavour: string;
type: SpaceType;
id: string;
}
export class IDBConnection extends AutoReconnectConnection<{ export class IDBConnection extends AutoReconnectConnection<{
db: IDBPDatabase<DocStorageSchema>; db: IDBPDatabase<DocStorageSchema>;
channel: BroadcastChannel; channel: BroadcastChannel;
}> { }> {
readonly dbName = `${this.opts.peer}:${this.opts.type}:${this.opts.id}`; readonly dbName = `${this.opts.flavour}:${this.opts.type}:${this.opts.id}`;
override get shareId() { override get shareId() {
return `idb(${migrator.version}):${this.dbName}`; return `idb(${migrator.version}):${this.dbName}`;
} }
constructor(private readonly opts: StorageOptions) { constructor(private readonly opts: IDBConnectionOptions) {
super(); super();
} }
+4 -7
View File
@@ -3,10 +3,9 @@ import {
type DocClocks, type DocClocks,
type DocRecord, type DocRecord,
DocStorageBase, DocStorageBase,
type DocStorageOptions,
type DocUpdate, type DocUpdate,
} from '../../storage'; } from '../../storage';
import { IDBConnection } from './db'; import { IDBConnection, type IDBConnectionOptions } from './db';
import { IndexedDBLocker } from './lock'; import { IndexedDBLocker } from './lock';
interface ChannelMessage { interface ChannelMessage {
@@ -15,7 +14,9 @@ interface ChannelMessage {
origin?: string; origin?: string;
} }
export class IndexedDBDocStorage extends DocStorageBase { export class IndexedDBDocStorage extends DocStorageBase<IDBConnectionOptions> {
static readonly identifier = 'IndexedDBDocStorage';
readonly connection = new IDBConnection(this.options); readonly connection = new IDBConnection(this.options);
get db() { get db() {
@@ -30,10 +31,6 @@ export class IndexedDBDocStorage extends DocStorageBase {
private _lastTimestamp = new Date(0); private _lastTimestamp = new Date(0);
constructor(options: DocStorageOptions) {
super(options);
}
private generateTimestamp() { private generateTimestamp() {
const timestamp = new Date(); const timestamp = new Date();
if (timestamp.getTime() <= this._lastTimestamp.getTime()) { if (timestamp.getTime() <= this._lastTimestamp.getTime()) {
@@ -1,3 +1,20 @@
import type { StorageConstructor } from '..';
import { IndexedDBBlobStorage } from './blob';
import { IndexedDBDocStorage } from './doc';
import { IndexedDBSyncStorage } from './sync';
import { IndexedDBV1BlobStorage, IndexedDBV1DocStorage } from './v1';
export * from './blob'; export * from './blob';
export * from './doc'; export * from './doc';
export * from './sync'; export * from './sync';
export const idbStorages = [
IndexedDBDocStorage,
IndexedDBBlobStorage,
IndexedDBSyncStorage,
] satisfies StorageConstructor[];
export const idbv1Storages = [
IndexedDBV1DocStorage,
IndexedDBV1BlobStorage,
] satisfies StorageConstructor[];
+10 -3
View File
@@ -1,7 +1,14 @@
import { share } from '../../connection'; import { share } from '../../connection';
import { BasicSyncStorage, type DocClock, type DocClocks } from '../../storage'; import { type DocClock, type DocClocks, SyncStorageBase } from '../../storage';
import { IDBConnection } from './db'; import { IDBConnection, type IDBConnectionOptions } from './db';
export class IndexedDBSyncStorage extends BasicSyncStorage {
export class IndexedDBSyncStorage extends SyncStorageBase {
static readonly identifier = 'IndexedDBSyncStorage';
constructor(private readonly options: IDBConnectionOptions) {
super();
}
readonly connection = share(new IDBConnection(this.options)); readonly connection = share(new IDBConnection(this.options));
get db() { get db() {
@@ -1,12 +1,18 @@
import { share } from '../../../connection'; import { share } from '../../../connection';
import { BlobStorageBase, type ListedBlobRecord } from '../../../storage'; import { BlobStorageBase, type ListedBlobRecord } from '../../../storage';
import { BlobIDBConnection } from './db'; import { BlobIDBConnection, type BlobIDBConnectionOptions } from './db';
/** /**
* @deprecated readonly * @deprecated readonly
*/ */
export class IndexedDBV1BlobStorage extends BlobStorageBase { export class IndexedDBV1BlobStorage extends BlobStorageBase {
readonly connection = share(new BlobIDBConnection(this.spaceId)); static readonly identifier = 'IndexedDBV1BlobStorage';
constructor(private readonly options: BlobIDBConnectionOptions) {
super();
}
readonly connection = share(new BlobIDBConnection(this.options));
get db() { get db() {
return this.connection.inner; return this.connection.inner;
@@ -42,19 +42,23 @@ export interface BlobDBSchema extends DBSchema {
}; };
} }
export interface BlobIDBConnectionOptions {
id: string;
}
export class BlobIDBConnection extends AutoReconnectConnection< export class BlobIDBConnection extends AutoReconnectConnection<
IDBPDatabase<BlobDBSchema> IDBPDatabase<BlobDBSchema>
> { > {
constructor(private readonly workspaceId: string) { constructor(private readonly options: BlobIDBConnectionOptions) {
super(); super();
} }
override get shareId() { override get shareId() {
return `idb(old-blob):${this.workspaceId}`; return `idb(old-blob):${this.options.id}`;
} }
override async doConnect() { override async doConnect() {
return openDB<BlobDBSchema>(`${this.workspaceId}_blob`, 1, { return openDB<BlobDBSchema>(`${this.options.id}_blob`, 1, {
upgrade: db => { upgrade: db => {
db.createObjectStore('blob'); db.createObjectStore('blob');
}, },
@@ -10,6 +10,8 @@ import { DocIDBConnection } from './db';
* @deprecated readonly * @deprecated readonly
*/ */
export class IndexedDBV1DocStorage extends DocStorageBase { export class IndexedDBV1DocStorage extends DocStorageBase {
static readonly identifier = 'IndexedDBV1DocStorage';
readonly connection = share(new DocIDBConnection()); readonly connection = share(new DocIDBConnection());
get db() { get db() {
+21 -44
View File
@@ -1,47 +1,24 @@
import type { Storage } from '../storage'; import type { Storage } from '../storage';
import { BroadcastChannelAwarenessStorage } from './broadcast-channel/awareness'; import type { broadcastChannelStorages } from './broadcast-channel';
import { import type { cloudStorages } from './cloud';
CloudAwarenessStorage, import type { idbStorages, idbv1Storages } from './idb';
CloudBlobStorage, import type { sqliteStorages } from './sqlite';
CloudDocStorage,
} from './cloud';
import {
IndexedDBBlobStorage,
IndexedDBDocStorage,
IndexedDBSyncStorage,
} from './idb';
import { IndexedDBV1BlobStorage, IndexedDBV1DocStorage } from './idb/v1';
type StorageConstructor = new (...args: any[]) => Storage; export type StorageConstructor = {
new (...args: any[]): Storage;
const idb: StorageConstructor[] = [ readonly identifier: string;
IndexedDBDocStorage, };
IndexedDBBlobStorage,
IndexedDBSyncStorage, type Storages =
BroadcastChannelAwarenessStorage, | typeof cloudStorages
]; | typeof idbv1Storages
| typeof idbStorages
const idbv1: StorageConstructor[] = [ | typeof sqliteStorages
IndexedDBV1DocStorage, | typeof broadcastChannelStorages;
IndexedDBV1BlobStorage,
]; // oxlint-disable-next-line no-redeclare
export type AvailableStorageImplementations = {
const cloud: StorageConstructor[] = [ [key in Storages[number]['identifier']]: Storages[number] & {
CloudDocStorage, identifier: key;
CloudBlobStorage, };
CloudAwarenessStorage,
];
export const storages: StorageConstructor[] = cloud.concat(idbv1, idb);
const AvailableStorageImplementations = storages.reduce(
(acc, curr) => {
acc[curr.name] = curr;
return acc;
},
{} as Record<string, StorageConstructor>
);
export const getAvailableStorageImplementations = (name: string) => {
return AvailableStorageImplementations[name];
}; };
@@ -1,11 +1,15 @@
import { share } from '../../connection'; import { share } from '../../connection';
import { type BlobRecord, BlobStorageBase } from '../../storage'; import { type BlobRecord, BlobStorageBase } from '../../storage';
import { NativeDBConnection } from './db'; import { NativeDBConnection, type SqliteNativeDBOptions } from './db';
export class SqliteBlobStorage extends BlobStorageBase { export class SqliteBlobStorage extends BlobStorageBase {
override connection = share( static readonly identifier = 'SqliteBlobStorage';
new NativeDBConnection(this.peer, this.spaceType, this.spaceId)
); override connection = share(new NativeDBConnection(this.options));
constructor(private readonly options: SqliteNativeDBOptions) {
super();
}
get db() { get db() {
return this.connection.apis; return this.connection.apis;
+112 -33
View File
@@ -1,9 +1,81 @@
import { apis } from '@affine/electron-api';
import { AutoReconnectConnection } from '../../connection'; import { AutoReconnectConnection } from '../../connection';
import { type SpaceType, universalId } from '../../storage'; import type {
BlobRecord,
DocClock,
DocRecord,
ListedBlobRecord,
} from '../../storage';
import { type SpaceType, universalId } from '../../utils/universal-id';
type NativeDBApis = NonNullable<typeof apis>['nbstore'] extends infer APIs export interface SqliteNativeDBOptions {
readonly flavour: string;
readonly type: SpaceType;
readonly id: string;
}
export type NativeDBApis = {
connect(id: string): Promise<void>;
disconnect(id: string): Promise<void>;
pushUpdate(id: string, docId: string, update: Uint8Array): Promise<Date>;
getDocSnapshot(id: string, docId: string): Promise<DocRecord | null>;
setDocSnapshot(id: string, snapshot: DocRecord): Promise<boolean>;
getDocUpdates(id: string, docId: string): Promise<DocRecord[]>;
markUpdatesMerged(
id: string,
docId: string,
updates: Date[]
): Promise<number>;
deleteDoc(id: string, docId: string): Promise<void>;
getDocClocks(
id: string,
after?: Date | undefined | null
): Promise<DocClock[]>;
getDocClock(id: string, docId: string): Promise<DocClock | null>;
getBlob(id: string, key: string): Promise<BlobRecord | null>;
setBlob(id: string, blob: BlobRecord): Promise<void>;
deleteBlob(id: string, key: string, permanently: boolean): Promise<void>;
releaseBlobs(id: string): Promise<void>;
listBlobs(id: string): Promise<ListedBlobRecord[]>;
getPeerRemoteClocks(id: string, peer: string): Promise<DocClock[]>;
getPeerRemoteClock(
id: string,
peer: string,
docId: string
): Promise<DocClock>;
setPeerRemoteClock(
id: string,
peer: string,
docId: string,
clock: Date
): Promise<void>;
getPeerPulledRemoteClocks(id: string, peer: string): Promise<DocClock[]>;
getPeerPulledRemoteClock(
id: string,
peer: string,
docId: string
): Promise<DocClock>;
setPeerPulledRemoteClock(
id: string,
peer: string,
docId: string,
clock: Date
): Promise<void>;
getPeerPushedClocks(id: string, peer: string): Promise<DocClock[]>;
getPeerPushedClock(
id: string,
peer: string,
docId: string
): Promise<DocClock>;
setPeerPushedClock(
id: string,
peer: string,
docId: string,
clock: Date
): Promise<void>;
clearClocks(id: string): Promise<void>;
};
type NativeDBApisWrapper = NativeDBApis extends infer APIs
? { ? {
[K in keyof APIs]: APIs[K] extends (...args: any[]) => any [K in keyof APIs]: APIs[K] extends (...args: any[]) => any
? Parameters<APIs[K]> extends [string, ...infer Rest] ? Parameters<APIs[K]> extends [string, ...infer Rest]
@@ -13,49 +85,56 @@ type NativeDBApis = NonNullable<typeof apis>['nbstore'] extends infer APIs
} }
: never; : never;
export class NativeDBConnection extends AutoReconnectConnection<void> { let apis: NativeDBApis | null = null;
readonly apis: NativeDBApis;
constructor( export function bindNativeDBApis(a: NativeDBApis) {
private readonly peer: string, apis = a;
private readonly type: SpaceType, }
private readonly id: string
) { export class NativeDBConnection extends AutoReconnectConnection<void> {
readonly apis: NativeDBApisWrapper;
readonly flavour = this.options.flavour;
readonly type = this.options.type;
readonly id = this.options.id;
constructor(private readonly options: SqliteNativeDBOptions) {
super(); super();
if (!apis) { if (!apis) {
throw new Error('Not in electron context.'); throw new Error('Not in native context.');
} }
this.apis = this.bindApis(apis.nbstore); this.apis = this.warpApis(apis);
} }
override get shareId(): string { override get shareId(): string {
return `sqlite:${this.peer}:${this.type}:${this.id}`; return `sqlite:${this.flavour}:${this.type}:${this.id}`;
} }
bindApis(originalApis: NonNullable<typeof apis>['nbstore']): NativeDBApis { warpApis(originalApis: NativeDBApis): NativeDBApisWrapper {
const id = universalId({ const id = universalId({
peer: this.peer, peer: this.flavour,
type: this.type, type: this.type,
id: this.id, id: this.id,
}); });
return new Proxy(originalApis, { return new Proxy(
get: (target, key: keyof NativeDBApis) => { {},
const v = target[key]; {
if (typeof v !== 'function') { get: (_target, key: keyof NativeDBApisWrapper) => {
return v; const v = originalApis[key];
}
return async (...args: any[]) => { return async (...args: any[]) => {
return v.call( return v.call(
originalApis, originalApis,
id, id,
// @ts-expect-error I don't know why it complains ts(2556) // @ts-expect-error I don't know why it complains ts(2556)
...args ...args
); );
}; };
}, },
}) as unknown as NativeDBApis; }
) as unknown as NativeDBApisWrapper;
} }
override async doConnect() { override async doConnect() {
@@ -63,7 +142,7 @@ export class NativeDBConnection extends AutoReconnectConnection<void> {
} }
override doDisconnect() { override doDisconnect() {
this.apis.close().catch(err => { this.apis.disconnect().catch(err => {
console.error('NativeDBConnection close failed', err); console.error('NativeDBConnection close failed', err);
}); });
} }
+56 -28
View File
@@ -1,54 +1,82 @@
import { share } from '../../connection'; import { share } from '../../connection';
import { type DocClock, DocStorageBase, type DocUpdate } from '../../storage'; import {
import { NativeDBConnection } from './db'; type DocClocks,
type DocRecord,
DocStorageBase,
type DocUpdate,
} from '../../storage';
import { NativeDBConnection, type SqliteNativeDBOptions } from './db';
export class SqliteDocStorage extends DocStorageBase { export class SqliteDocStorage extends DocStorageBase<SqliteNativeDBOptions> {
override connection = share( static readonly identifier = 'SqliteDocStorage';
new NativeDBConnection(this.peer, this.spaceType, this.spaceId) override connection = share(new NativeDBConnection(this.options));
);
get db() { get db() {
return this.connection.apis; return this.connection.apis;
} }
override async getDoc(docId: string) {
return this.db.getDoc(docId);
}
override async pushDocUpdate(update: DocUpdate) { override async pushDocUpdate(update: DocUpdate) {
return this.db.pushDocUpdate(update); const timestamp = await this.db.pushUpdate(update.docId, update.bin);
this.emit(
'update',
{
docId: update.docId,
bin: update.bin,
timestamp,
editor: update.editor,
},
origin
);
return { docId: update.docId, timestamp };
} }
override async deleteDoc(docId: string) { override async deleteDoc(docId: string) {
return this.db.deleteDoc(docId); await this.db.deleteDoc(docId);
} }
override async getDocTimestamps(after?: Date) { override async getDocTimestamps(after?: Date) {
return this.db.getDocTimestamps(after ? new Date(after) : undefined); const clocks = await this.db.getDocClocks(after);
return clocks.reduce((ret, cur) => {
ret[cur.docId] = cur.timestamp;
return ret;
}, {} as DocClocks);
} }
override getDocTimestamp(docId: string): Promise<DocClock | null> { override async getDocTimestamp(docId: string) {
return this.db.getDocTimestamp(docId); return this.db.getDocClock(docId);
} }
protected override async getDocSnapshot() { protected override async getDocSnapshot(docId: string) {
// handled in db const snapshot = await this.db.getDocSnapshot(docId);
// see electron/src/helper/nbstore/doc.ts
return null; if (!snapshot) {
return null;
}
return snapshot;
} }
protected override async setDocSnapshot(): Promise<boolean> { protected override async setDocSnapshot(
// handled in db snapshot: DocRecord
return true; ): Promise<boolean> {
return this.db.setDocSnapshot({
docId: snapshot.docId,
bin: snapshot.bin,
timestamp: snapshot.timestamp,
});
} }
protected override async getDocUpdates() { protected override async getDocUpdates(docId: string) {
// handled in db return this.db.getDocUpdates(docId);
return [];
} }
protected override markUpdatesMerged() { protected override markUpdatesMerged(docId: string, updates: DocRecord[]) {
// handled in db return this.db.markUpdatesMerged(
return Promise.resolve(0); docId,
updates.map(update => update.timestamp)
);
} }
} }
@@ -1,3 +1,16 @@
import type { StorageConstructor } from '..';
import { SqliteBlobStorage } from './blob';
import { SqliteDocStorage } from './doc';
import { SqliteSyncStorage } from './sync';
export * from './blob'; export * from './blob';
export { bindNativeDBApis, type NativeDBApis } from './db';
export * from './doc'; export * from './doc';
export * from './sync'; export * from './sync';
export * from './v1';
export const sqliteStorages = [
SqliteDocStorage,
SqliteBlobStorage,
SqliteSyncStorage,
] satisfies StorageConstructor[];
@@ -1,18 +1,26 @@
import { share } from '../../connection'; import { share } from '../../connection';
import { BasicSyncStorage, type DocClock } from '../../storage'; import { type DocClock, SyncStorageBase } from '../../storage';
import { NativeDBConnection } from './db'; import { NativeDBConnection, type SqliteNativeDBOptions } from './db';
export class SqliteSyncStorage extends BasicSyncStorage { export class SqliteSyncStorage extends SyncStorageBase {
override connection = share( static readonly identifier = 'SqliteSyncStorage';
new NativeDBConnection(this.peer, this.spaceType, this.spaceId)
); override connection = share(new NativeDBConnection(this.options));
constructor(private readonly options: SqliteNativeDBOptions) {
super();
}
get db() { get db() {
return this.connection.apis; return this.connection.apis;
} }
override async getPeerRemoteClocks(peer: string) { override async getPeerRemoteClocks(peer: string) {
return this.db.getPeerRemoteClocks(peer); return this.db
.getPeerRemoteClocks(peer)
.then(clocks =>
Object.fromEntries(clocks.map(clock => [clock.docId, clock.timestamp]))
);
} }
override async getPeerRemoteClock(peer: string, docId: string) { override async getPeerRemoteClock(peer: string, docId: string) {
@@ -20,11 +28,15 @@ export class SqliteSyncStorage extends BasicSyncStorage {
} }
override async setPeerRemoteClock(peer: string, clock: DocClock) { override async setPeerRemoteClock(peer: string, clock: DocClock) {
await this.db.setPeerRemoteClock(peer, clock); await this.db.setPeerRemoteClock(peer, clock.docId, clock.timestamp);
} }
override async getPeerPulledRemoteClocks(peer: string) { override async getPeerPulledRemoteClocks(peer: string) {
return this.db.getPeerPulledRemoteClocks(peer); return this.db
.getPeerPulledRemoteClocks(peer)
.then(clocks =>
Object.fromEntries(clocks.map(clock => [clock.docId, clock.timestamp]))
);
} }
override async getPeerPulledRemoteClock(peer: string, docId: string) { override async getPeerPulledRemoteClock(peer: string, docId: string) {
@@ -32,11 +44,15 @@ export class SqliteSyncStorage extends BasicSyncStorage {
} }
override async setPeerPulledRemoteClock(peer: string, clock: DocClock) { override async setPeerPulledRemoteClock(peer: string, clock: DocClock) {
await this.db.setPeerPulledRemoteClock(peer, clock); await this.db.setPeerPulledRemoteClock(peer, clock.docId, clock.timestamp);
} }
override async getPeerPushedClocks(peer: string) { override async getPeerPushedClocks(peer: string) {
return this.db.getPeerPushedClocks(peer); return this.db
.getPeerPushedClocks(peer)
.then(clocks =>
Object.fromEntries(clocks.map(clock => [clock.docId, clock.timestamp]))
);
} }
override async getPeerPushedClock(peer: string, docId: string) { override async getPeerPushedClock(peer: string, docId: string) {
@@ -44,7 +60,7 @@ export class SqliteSyncStorage extends BasicSyncStorage {
} }
override async setPeerPushedClock(peer: string, clock: DocClock) { override async setPeerPushedClock(peer: string, clock: DocClock) {
await this.db.setPeerPushedClock(peer, clock); await this.db.setPeerPushedClock(peer, clock.docId, clock.timestamp);
} }
override async clearClocks() { override async clearClocks() {
@@ -1,7 +1,7 @@
import { apis } from '@affine/electron-api';
import { DummyConnection } from '../../../connection'; import { DummyConnection } from '../../../connection';
import { BlobStorageBase } from '../../../storage'; import { BlobStorageBase } from '../../../storage';
import type { SpaceType } from '../../../utils/universal-id';
import { apis } from './db';
/** /**
* @deprecated readonly * @deprecated readonly
@@ -9,18 +9,22 @@ import { BlobStorageBase } from '../../../storage';
export class SqliteV1BlobStorage extends BlobStorageBase { export class SqliteV1BlobStorage extends BlobStorageBase {
override connection = new DummyConnection(); override connection = new DummyConnection();
get db() { constructor(private readonly options: { type: SpaceType; id: string }) {
super();
}
private get db() {
if (!apis) { if (!apis) {
throw new Error('Not in electron context.'); throw new Error('Not in electron context.');
} }
return apis.db; return apis;
} }
override async get(key: string) { override async get(key: string) {
const data: Uint8Array | null = await this.db.getBlob( const data: Uint8Array | null = await this.db.getBlob(
this.spaceType, this.options.type,
this.spaceId, this.options.id,
key key
); );
@@ -38,12 +42,12 @@ export class SqliteV1BlobStorage extends BlobStorageBase {
override async delete(key: string, permanently: boolean) { override async delete(key: string, permanently: boolean) {
if (permanently) { if (permanently) {
await this.db.deleteBlob(this.spaceType, this.spaceId, key); await this.db.deleteBlob(this.options.type, this.options.id, key);
} }
} }
override async list() { override async list() {
const keys = await this.db.getBlobKeys(this.spaceType, this.spaceId); const keys = await this.db.getBlobKeys(this.options.type, this.options.id);
return keys.map(key => ({ return keys.map(key => ({
key, key,
@@ -0,0 +1,26 @@
import type { SpaceType } from '../../../utils/universal-id';
interface NativeDBV1Apis {
getBlob: (
spaceType: SpaceType,
workspaceId: string,
key: string
) => Promise<Buffer | null>;
deleteBlob: (
spaceType: SpaceType,
workspaceId: string,
key: string
) => Promise<void>;
getBlobKeys: (spaceType: SpaceType, workspaceId: string) => Promise<string[]>;
getDocAsUpdates: (
spaceType: SpaceType,
workspaceId: string,
subdocId: string
) => Promise<Uint8Array>;
}
export let apis: NativeDBV1Apis | null = null;
export function bindNativeDBV1Apis(a: NativeDBV1Apis) {
apis = a;
}
@@ -1,24 +1,27 @@
import { apis } from '@affine/electron-api';
import { DummyConnection } from '../../../connection'; import { DummyConnection } from '../../../connection';
import { import {
type DocRecord, type DocRecord,
DocStorageBase, DocStorageBase,
type DocUpdate, type DocUpdate,
} from '../../../storage'; } from '../../../storage';
import type { SpaceType } from '../../../utils/universal-id';
import { apis } from './db';
/** /**
* @deprecated readonly * @deprecated readonly
*/ */
export class SqliteV1DocStorage extends DocStorageBase { export class SqliteV1DocStorage extends DocStorageBase<{
type: SpaceType;
id: string;
}> {
override connection = new DummyConnection(); override connection = new DummyConnection();
get db() { private get db() {
if (!apis) { if (!apis) {
throw new Error('Not in electron context.'); throw new Error('Not in electron context.');
} }
return apis.db; return apis;
} }
override async pushDocUpdate(update: DocUpdate) { override async pushDocUpdate(update: DocUpdate) {
@@ -29,8 +32,8 @@ export class SqliteV1DocStorage extends DocStorageBase {
override async getDoc(docId: string) { override async getDoc(docId: string) {
const bin = await this.db.getDocAsUpdates( const bin = await this.db.getDocAsUpdates(
this.spaceType, this.options.type,
this.spaceId, this.options.id,
docId docId
); );
@@ -41,8 +44,8 @@ export class SqliteV1DocStorage extends DocStorageBase {
}; };
} }
override async deleteDoc(docId: string) { override async deleteDoc() {
await this.db.deleteDoc(this.spaceType, this.spaceId, docId); return;
} }
protected override async getDocSnapshot() { protected override async getDocSnapshot() {
@@ -1,2 +1,3 @@
export * from './blob'; export * from './blob';
export { bindNativeDBV1Apis } from './db';
export * from './doc'; export * from './doc';
+3
View File
@@ -1,2 +1,5 @@
export * from './connection'; export * from './connection';
export * from './frontend';
export * from './storage'; export * from './storage';
export * from './sync';
export * from './utils/universal-id';
@@ -1,6 +1,5 @@
import { type Storage, StorageBase, type StorageOptions } from './storage'; import type { Connection } from '../connection';
import { type Storage } from './storage';
export interface AwarenessStorageOptions extends StorageOptions {}
export type AwarenessRecord = { export type AwarenessRecord = {
docId: string; docId: string;
@@ -23,13 +22,9 @@ export interface AwarenessStorage extends Storage {
): () => void; ): () => void;
} }
export abstract class AwarenessStorageBase< export abstract class AwarenessStorageBase implements AwarenessStorage {
Options extends AwarenessStorageOptions = AwarenessStorageOptions, readonly storageType = 'awareness';
> abstract readonly connection: Connection;
extends StorageBase<Options>
implements AwarenessStorage
{
override readonly storageType = 'awareness';
abstract update(record: AwarenessRecord, origin?: string): Promise<void>; abstract update(record: AwarenessRecord, origin?: string): Promise<void>;
+5 -10
View File
@@ -1,6 +1,5 @@
import { type Storage, StorageBase, type StorageOptions } from './storage'; import type { Connection } from '../connection';
import { type Storage } from './storage';
export interface BlobStorageOptions extends StorageOptions {}
export interface BlobRecord { export interface BlobRecord {
key: string; key: string;
@@ -29,13 +28,9 @@ export interface BlobStorage extends Storage {
list(signal?: AbortSignal): Promise<ListedBlobRecord[]>; list(signal?: AbortSignal): Promise<ListedBlobRecord[]>;
} }
export abstract class BlobStorageBase< export abstract class BlobStorageBase implements BlobStorage {
Options extends BlobStorageOptions = BlobStorageOptions, readonly storageType = 'blob';
> abstract readonly connection: Connection;
extends StorageBase<Options>
implements BlobStorage
{
override readonly storageType = 'blob';
abstract get(key: string, signal?: AbortSignal): Promise<BlobRecord | null>; abstract get(key: string, signal?: AbortSignal): Promise<BlobRecord | null>;
abstract set(blob: BlobRecord, signal?: AbortSignal): Promise<void>; abstract set(blob: BlobRecord, signal?: AbortSignal): Promise<void>;
+28 -14
View File
@@ -1,10 +1,11 @@
import EventEmitter2 from 'eventemitter2'; import EventEmitter2 from 'eventemitter2';
import { diffUpdate, encodeStateVectorFromUpdate, mergeUpdates } from 'yjs'; import { diffUpdate, encodeStateVectorFromUpdate, mergeUpdates } from 'yjs';
import type { Connection } from '../connection';
import { isEmptyUpdate } from '../utils/is-empty-update'; import { isEmptyUpdate } from '../utils/is-empty-update';
import type { Locker } from './lock'; import type { Locker } from './lock';
import { SingletonLocker } from './lock'; import { SingletonLocker } from './lock';
import { type Storage, StorageBase, type StorageOptions } from './storage'; import { type Storage } from './storage';
export interface DocClock { export interface DocClock {
docId: string; docId: string;
@@ -33,13 +34,19 @@ export interface Editor {
avatarUrl: string | null; avatarUrl: string | null;
} }
export interface DocStorageOptions extends StorageOptions { export interface DocStorageOptions {
mergeUpdates?: (updates: Uint8Array[]) => Promise<Uint8Array> | Uint8Array; mergeUpdates?: (updates: Uint8Array[]) => Promise<Uint8Array> | Uint8Array;
id: string;
/**
* open as readonly mode.
*/
readonlyMode?: boolean;
} }
export interface DocStorage extends Storage { export interface DocStorage extends Storage {
readonly storageType: 'doc'; readonly storageType: 'doc';
readonly isReadonly: boolean;
/** /**
* Get a doc record with latest binary. * Get a doc record with latest binary.
*/ */
@@ -88,18 +95,22 @@ export interface DocStorage extends Storage {
): () => void; ): () => void;
} }
export abstract class DocStorageBase< export abstract class DocStorageBase<Opts = {}> implements DocStorage {
Opts extends DocStorageOptions = DocStorageOptions, get isReadonly(): boolean {
> return this.options.readonlyMode ?? false;
extends StorageBase<Opts> }
implements DocStorage
{
private readonly event = new EventEmitter2(); private readonly event = new EventEmitter2();
override readonly storageType = 'doc'; readonly storageType = 'doc';
abstract readonly connection: Connection;
protected readonly locker: Locker = new SingletonLocker(); protected readonly locker: Locker = new SingletonLocker();
protected readonly spaceId = this.options.id;
constructor(protected readonly options: Opts & DocStorageOptions) {}
async getDoc(docId: string) { async getDoc(docId: string) {
await using _lock = await this.lockDocForUpdate(docId); await using _lock = this.isReadonly
? undefined
: await this.lockDocForUpdate(docId);
const snapshot = await this.getDocSnapshot(docId); const snapshot = await this.getDocSnapshot(docId);
const updates = await this.getDocUpdates(docId); const updates = await this.getDocUpdates(docId);
@@ -117,10 +128,13 @@ export abstract class DocStorageBase<
editor, editor,
}; };
await this.setDocSnapshot(newSnapshot, snapshot); // if is readonly, we will not set the new snapshot
if (!this.isReadonly) {
await this.setDocSnapshot(newSnapshot, snapshot);
// always mark updates as merged unless throws // always mark updates as merged unless throws
await this.markUpdatesMerged(docId, updates); await this.markUpdatesMerged(docId, updates);
}
return newSnapshot; return newSnapshot;
} }
@@ -0,0 +1,16 @@
import { DummyConnection } from '../../connection';
import { type AwarenessRecord, AwarenessStorageBase } from '../awareness';
export class DummyAwarenessStorage extends AwarenessStorageBase {
override update(_record: AwarenessRecord, _origin?: string): Promise<void> {
return Promise.resolve();
}
override subscribeUpdate(
_id: string,
_onUpdate: (update: AwarenessRecord, origin?: string) => void,
_onCollect: () => Promise<AwarenessRecord | null>
): () => void {
return () => {};
}
override connection = new DummyConnection();
}
@@ -0,0 +1,32 @@
import { DummyConnection } from '../../connection';
import {
type BlobRecord,
BlobStorageBase,
type ListedBlobRecord,
} from '../blob';
export class DummyBlobStorage extends BlobStorageBase {
override get(
_key: string,
_signal?: AbortSignal
): Promise<BlobRecord | null> {
return Promise.resolve(null);
}
override set(_blob: BlobRecord, _signal?: AbortSignal): Promise<void> {
return Promise.resolve();
}
override delete(
_key: string,
_permanently: boolean,
_signal?: AbortSignal
): Promise<void> {
return Promise.resolve();
}
override release(_signal?: AbortSignal): Promise<void> {
return Promise.resolve();
}
override list(_signal?: AbortSignal): Promise<ListedBlobRecord[]> {
return Promise.resolve([]);
}
override connection = new DummyConnection();
}
@@ -0,0 +1,41 @@
import { DummyConnection } from '../../connection';
import {
type DocClock,
type DocClocks,
type DocDiff,
type DocRecord,
type DocStorage,
type DocUpdate,
} from '../doc';
export class DummyDocStorage implements DocStorage {
readonly storageType = 'doc';
readonly isReadonly = true;
getDoc(_docId: string): Promise<DocRecord | null> {
return Promise.resolve(null);
}
getDocDiff(_docId: string, _state?: Uint8Array): Promise<DocDiff | null> {
return Promise.resolve(null);
}
pushDocUpdate(update: DocUpdate, _origin?: string): Promise<DocClock> {
return Promise.resolve({
docId: update.docId,
timestamp: new Date(),
});
}
getDocTimestamp(_docId: string): Promise<DocClock | null> {
return Promise.resolve(null);
}
getDocTimestamps(_after?: Date): Promise<DocClocks> {
return Promise.resolve({});
}
deleteDoc(_docId: string): Promise<void> {
return Promise.resolve();
}
subscribeDocUpdate(
_callback: (update: DocRecord, origin?: string) => void
): () => void {
return () => {};
}
connection = new DummyConnection();
}
@@ -0,0 +1,49 @@
import { DummyConnection } from '../../connection';
import type { DocClock, DocClocks } from '../doc';
import { SyncStorageBase } from '../sync';
export class DummySyncStorage extends SyncStorageBase {
override getPeerRemoteClock(
_peer: string,
_docId: string
): Promise<DocClock | null> {
return Promise.resolve(null);
}
override getPeerRemoteClocks(_peer: string): Promise<DocClocks> {
return Promise.resolve({});
}
override setPeerRemoteClock(_peer: string, _clock: DocClock): Promise<void> {
return Promise.resolve();
}
override getPeerPulledRemoteClock(
_peer: string,
_docId: string
): Promise<DocClock | null> {
return Promise.resolve(null);
}
override getPeerPulledRemoteClocks(_peer: string): Promise<DocClocks> {
return Promise.resolve({});
}
override setPeerPulledRemoteClock(
_peer: string,
_clock: DocClock
): Promise<void> {
return Promise.resolve();
}
override getPeerPushedClock(
_peer: string,
_docId: string
): Promise<DocClock | null> {
return Promise.resolve(null);
}
override getPeerPushedClocks(_peer: string): Promise<DocClocks> {
return Promise.resolve({});
}
override setPeerPushedClock(_peer: string, _clock: DocClock): Promise<void> {
return Promise.resolve();
}
override clearClocks(): Promise<void> {
return Promise.resolve();
}
override connection = new DummyConnection();
}
@@ -0,0 +1 @@
export * from './over-capacity';
@@ -0,0 +1,5 @@
export class OverCapacityError extends Error {
constructor(public originError?: any) {
super('Storage over capacity. Origin error: ' + originError);
}
}
+25 -21
View File
@@ -3,56 +3,60 @@ import EventEmitter2 from 'eventemitter2';
import type { AwarenessStorage } from './awareness'; import type { AwarenessStorage } from './awareness';
import type { BlobStorage } from './blob'; import type { BlobStorage } from './blob';
import type { DocStorage } from './doc'; import type { DocStorage } from './doc';
import type { Storage, StorageType } from './storage'; import { DummyAwarenessStorage } from './dummy/awareness';
import { DummyBlobStorage } from './dummy/blob';
import { DummyDocStorage } from './dummy/doc';
import { DummySyncStorage } from './dummy/sync';
import type { StorageType } from './storage';
import type { SyncStorage } from './sync'; import type { SyncStorage } from './sync';
type Storages = DocStorage | BlobStorage | SyncStorage | AwarenessStorage; type Storages = DocStorage | BlobStorage | SyncStorage | AwarenessStorage;
export type SpaceStorageOptions = {
[K in StorageType]?: Storages & { storageType: K };
};
export class SpaceStorage { export class SpaceStorage {
protected readonly storages: Map<StorageType, Storage> = new Map(); protected readonly storages: {
[K in StorageType]: Storages & { storageType: K };
};
private readonly event = new EventEmitter2(); private readonly event = new EventEmitter2();
private readonly disposables: Set<() => void> = new Set(); private readonly disposables: Set<() => void> = new Set();
constructor(storages: Storage[] = []) { constructor(storages: SpaceStorageOptions) {
this.storages = new Map( this.storages = {
storages.map(storage => [storage.storageType, storage]) awareness: storages.awareness ?? new DummyAwarenessStorage(),
); blob: storages.blob ?? new DummyBlobStorage(),
} doc: storages.doc ?? new DummyDocStorage(),
sync: storages.sync ?? new DummySyncStorage(),
tryGet<T extends StorageType>( };
type: T
): Extract<Storages, { storageType: T }> | undefined {
return this.storages.get(type) as unknown as Extract<
Storages,
{ storageType: T }
>;
} }
get<T extends StorageType>(type: T): Extract<Storages, { storageType: T }> { get<T extends StorageType>(type: T): Extract<Storages, { storageType: T }> {
const storage = this.tryGet(type); const storage = this.storages[type];
if (!storage) { if (!storage) {
throw new Error(`Storage ${type} not registered.`); throw new Error(`Storage ${type} not registered.`);
} }
return storage as Extract<Storages, { storageType: T }>; return storage as unknown as Extract<Storages, { storageType: T }>;
} }
connect() { connect() {
Array.from(this.storages.values()).forEach(storage => { Object.values(this.storages).forEach(storage => {
storage.connection.connect(); storage.connection.connect();
}); });
} }
disconnect() { disconnect() {
Array.from(this.storages.values()).forEach(storage => { Object.values(this.storages).forEach(storage => {
storage.connection.disconnect(); storage.connection.disconnect();
}); });
} }
async waitForConnected(signal?: AbortSignal) { async waitForConnected(signal?: AbortSignal) {
await Promise.all( await Promise.all(
Array.from(this.storages.values()).map(storage => Object.values(this.storages).map(storage =>
storage.connection.waitForConnected(signal) storage.connection.waitForConnected(signal)
) )
); );
@@ -61,13 +65,13 @@ export class SpaceStorage {
async destroy() { async destroy() {
this.disposables.forEach(disposable => disposable()); this.disposables.forEach(disposable => disposable());
this.event.removeAllListeners(); this.event.removeAllListeners();
this.storages.clear();
} }
} }
export * from './awareness'; export * from './awareness';
export * from './blob'; export * from './blob';
export * from './doc'; export * from './doc';
export * from './errors';
export * from './history'; export * from './history';
export * from './storage'; export * from './storage';
export * from './sync'; export * from './sync';
@@ -1,120 +1,8 @@
import type { Connection } from '../connection'; import type { Connection } from '../connection';
export type SpaceType = 'workspace' | 'userspace';
export type StorageType = 'blob' | 'doc' | 'sync' | 'awareness'; export type StorageType = 'blob' | 'doc' | 'sync' | 'awareness';
export interface StorageOptions {
peer: string;
type: SpaceType;
id: string;
}
export function universalId({ peer, type, id }: StorageOptions) {
return `@peer(${peer});@type(${type});@id(${id});`;
}
export function isValidSpaceType(type: string): type is SpaceType {
return type === 'workspace' || type === 'userspace';
}
export function isValidUniversalId(opts: Record<string, string>): boolean {
const requiredKeys: Array<keyof StorageOptions> = [
'peer',
'type',
'id',
] as const;
for (const key of requiredKeys) {
if (!opts[key]) {
return false;
}
}
return isValidSpaceType(opts.type);
}
export function parseUniversalId(id: string) {
const result: Partial<StorageOptions> = {};
let key = '';
let value = '';
let isInValue = false;
let i = -1;
while (++i < id.length) {
const ch = id[i];
const nextCh = id[i + 1];
// when we are in value string, we only care about ch and next char to be [')', ';'] to end the id part
if (isInValue) {
if (ch === ')' && nextCh === ';') {
// @ts-expect-error we know the key is valid
result[key] = value;
key = '';
value = '';
isInValue = false;
i++;
continue;
}
value += ch;
continue;
}
if (ch === '@') {
const keyEnd = id.indexOf('(', i);
// we find '@' but no '(' in lookahead or '(' is immediately after '@', invalid id
if (keyEnd === -1 || keyEnd === i + 1) {
break;
}
key = id.slice(i + 1, keyEnd);
i = keyEnd;
isInValue = true;
} else {
break;
}
}
if (!isValidUniversalId(result)) {
throw new Error(
`Invalid universal storage id: ${id}. It should be in format of @peer(\${peer});@type(\${type});@id(\${id});`
);
}
return result as StorageOptions;
}
export interface Storage { export interface Storage {
readonly storageType: StorageType; readonly storageType: StorageType;
readonly connection: Connection; readonly connection: Connection;
readonly peer: string;
readonly spaceType: string;
readonly spaceId: string;
readonly universalId: string;
}
export abstract class StorageBase<Opts extends StorageOptions = StorageOptions>
implements Storage
{
abstract readonly storageType: StorageType;
abstract readonly connection: Connection;
get peer() {
return this.options.peer;
}
get spaceType() {
return this.options.type;
}
get spaceId() {
return this.options.id;
}
get universalId() {
return universalId(this.options);
}
constructor(public readonly options: Opts) {}
} }
+5 -10
View File
@@ -1,7 +1,6 @@
import type { Connection } from '../connection';
import type { DocClock, DocClocks } from './doc'; import type { DocClock, DocClocks } from './doc';
import { type Storage, StorageBase, type StorageOptions } from './storage'; import { type Storage } from './storage';
export interface SyncStorageOptions extends StorageOptions {}
export interface SyncStorage extends Storage { export interface SyncStorage extends Storage {
readonly storageType: 'sync'; readonly storageType: 'sync';
@@ -21,13 +20,9 @@ export interface SyncStorage extends Storage {
clearClocks(): Promise<void>; clearClocks(): Promise<void>;
} }
export abstract class BasicSyncStorage< export abstract class SyncStorageBase implements SyncStorage {
Opts extends SyncStorageOptions = SyncStorageOptions, readonly storageType = 'sync';
> abstract readonly connection: Connection;
extends StorageBase<Opts>
implements SyncStorage
{
override readonly storageType = 'sync';
abstract getPeerRemoteClock( abstract getPeerRemoteClock(
peer: string, peer: string,
@@ -2,6 +2,7 @@ import type {
AwarenessRecord, AwarenessRecord,
AwarenessStorage, AwarenessStorage,
} from '../../storage/awareness'; } from '../../storage/awareness';
import type { PeerStorageOptions } from '../types';
export interface AwarenessSync { export interface AwarenessSync {
update(record: AwarenessRecord, origin?: string): Promise<void>; update(record: AwarenessRecord, origin?: string): Promise<void>;
@@ -13,14 +14,13 @@ export interface AwarenessSync {
} }
export class AwarenessSyncImpl implements AwarenessSync { export class AwarenessSyncImpl implements AwarenessSync {
constructor( constructor(readonly storages: PeerStorageOptions<AwarenessStorage>) {}
readonly local: AwarenessStorage,
readonly remotes: AwarenessStorage[]
) {}
async update(record: AwarenessRecord, origin?: string) { async update(record: AwarenessRecord, origin?: string) {
await Promise.all( await Promise.all(
[this.local, ...this.remotes].map(peer => peer.update(record, origin)) [this.storages.local, ...Object.values(this.storages.remotes)].map(peer =>
peer.update(record, origin)
)
); );
} }
@@ -29,9 +29,10 @@ export class AwarenessSyncImpl implements AwarenessSync {
onUpdate: (update: AwarenessRecord, origin?: string) => void, onUpdate: (update: AwarenessRecord, origin?: string) => void,
onCollect: () => Promise<AwarenessRecord | null> onCollect: () => Promise<AwarenessRecord | null>
): () => void { ): () => void {
const unsubscribes = [this.local, ...this.remotes].map(peer => const unsubscribes = [
peer.subscribeUpdate(id, onUpdate, onCollect) this.storages.local,
); ...Object.values(this.storages.remotes),
].map(peer => peer.subscribeUpdate(id, onUpdate, onCollect));
return () => { return () => {
unsubscribes.forEach(unsubscribe => unsubscribe()); unsubscribes.forEach(unsubscribe => unsubscribe());
}; };
+57 -18
View File
@@ -1,34 +1,48 @@
import EventEmitter2 from 'eventemitter2';
import { difference } from 'lodash-es'; import { difference } from 'lodash-es';
import { BehaviorSubject, type Observable } from 'rxjs';
import type { BlobRecord, BlobStorage } from '../../storage'; import type { BlobRecord, BlobStorage } from '../../storage';
import { OverCapacityError } from '../../storage';
import { MANUALLY_STOP, throwIfAborted } from '../../utils/throw-if-aborted'; import { MANUALLY_STOP, throwIfAborted } from '../../utils/throw-if-aborted';
import type { PeerStorageOptions } from '../types';
export interface BlobSyncState {
isStorageOverCapacity: boolean;
}
export interface BlobSync { export interface BlobSync {
readonly state$: Observable<BlobSyncState>;
downloadBlob( downloadBlob(
blobId: string, blobId: string,
signal?: AbortSignal signal?: AbortSignal
): Promise<BlobRecord | null>; ): Promise<BlobRecord | null>;
uploadBlob(blob: BlobRecord, signal?: AbortSignal): Promise<void>; uploadBlob(blob: BlobRecord, signal?: AbortSignal): Promise<void>;
fullSync(signal?: AbortSignal): Promise<void>;
setMaxBlobSize(size: number): void;
onReachedMaxBlobSize(cb: (byteSize: number) => void): () => void;
} }
export class BlobSyncImpl implements BlobSync { export class BlobSyncImpl implements BlobSync {
readonly state$ = new BehaviorSubject<BlobSyncState>({
isStorageOverCapacity: false,
});
private abort: AbortController | null = null; private abort: AbortController | null = null;
private maxBlobSize: number = 1024 * 1024 * 100; // 100MB
readonly event = new EventEmitter2();
constructor( constructor(readonly storages: PeerStorageOptions<BlobStorage>) {}
readonly local: BlobStorage,
readonly remotes: BlobStorage[]
) {}
async downloadBlob(blobId: string, signal?: AbortSignal) { async downloadBlob(blobId: string, signal?: AbortSignal) {
const localBlob = await this.local.get(blobId, signal); const localBlob = await this.storages.local.get(blobId, signal);
if (localBlob) { if (localBlob) {
return localBlob; return localBlob;
} }
for (const storage of this.remotes) { for (const storage of Object.values(this.storages.remotes)) {
const data = await storage.get(blobId, signal); const data = await storage.get(blobId, signal);
if (data) { if (data) {
await this.local.set(data, signal); await this.storages.local.set(data, signal);
return data; return data;
} }
} }
@@ -36,21 +50,35 @@ export class BlobSyncImpl implements BlobSync {
} }
async uploadBlob(blob: BlobRecord, signal?: AbortSignal) { async uploadBlob(blob: BlobRecord, signal?: AbortSignal) {
await this.local.set(blob); if (blob.data.length > this.maxBlobSize) {
this.event.emit('abort-large-blob', blob.data.length);
console.error('blob over limit, abort set');
}
await this.storages.local.set(blob);
await Promise.allSettled( await Promise.allSettled(
this.remotes.map(remote => remote.set(blob, signal)) Object.values(this.storages.remotes).map(async remote => {
try {
return await remote.set(blob, signal);
} catch (err) {
if (err instanceof OverCapacityError) {
this.state$.next({ isStorageOverCapacity: true });
}
throw err;
}
})
); );
} }
private async sync(signal?: AbortSignal) { async fullSync(signal?: AbortSignal) {
throwIfAborted(signal); throwIfAborted(signal);
for (const remote of this.remotes) { for (const [remotePeer, remote] of Object.entries(this.storages.remotes)) {
let localList: string[] = []; let localList: string[] = [];
let remoteList: string[] = []; let remoteList: string[] = [];
try { try {
localList = (await this.local.list(signal)).map(b => b.key); localList = (await this.storages.local.list(signal)).map(b => b.key);
throwIfAborted(signal); throwIfAborted(signal);
remoteList = (await remote.list(signal)).map(b => b.key); remoteList = (await remote.list(signal)).map(b => b.key);
throwIfAborted(signal); throwIfAborted(signal);
@@ -65,7 +93,7 @@ export class BlobSyncImpl implements BlobSync {
const needUpload = difference(localList, remoteList); const needUpload = difference(localList, remoteList);
for (const key of needUpload) { for (const key of needUpload) {
try { try {
const data = await this.local.get(key, signal); const data = await this.storages.local.get(key, signal);
throwIfAborted(signal); throwIfAborted(signal);
if (data) { if (data) {
await remote.set(data, signal); await remote.set(data, signal);
@@ -76,7 +104,7 @@ export class BlobSyncImpl implements BlobSync {
throw err; throw err;
} }
console.error( console.error(
`error when sync ${key} from [${this.local.peer}] to [${remote.peer}]`, `error when sync ${key} from [local] to [${remotePeer}]`,
err err
); );
} }
@@ -89,7 +117,7 @@ export class BlobSyncImpl implements BlobSync {
const data = await remote.get(key, signal); const data = await remote.get(key, signal);
throwIfAborted(signal); throwIfAborted(signal);
if (data) { if (data) {
await this.local.set(data, signal); await this.storages.local.set(data, signal);
throwIfAborted(signal); throwIfAborted(signal);
} }
} catch (err) { } catch (err) {
@@ -97,7 +125,7 @@ export class BlobSyncImpl implements BlobSync {
throw err; throw err;
} }
console.error( console.error(
`error when sync ${key} from [${remote.peer}] to [${this.local.peer}]`, `error when sync ${key} from [${remotePeer}] to [local]`,
err err
); );
} }
@@ -107,13 +135,13 @@ export class BlobSyncImpl implements BlobSync {
start() { start() {
if (this.abort) { if (this.abort) {
this.abort.abort(); this.abort.abort(MANUALLY_STOP);
} }
const abort = new AbortController(); const abort = new AbortController();
this.abort = abort; this.abort = abort;
this.sync(abort.signal).catch(error => { this.fullSync(abort.signal).catch(error => {
if (error === MANUALLY_STOP) { if (error === MANUALLY_STOP) {
return; return;
} }
@@ -130,4 +158,15 @@ export class BlobSyncImpl implements BlobSync {
// TODO: implement // TODO: implement
return () => {}; return () => {};
} }
setMaxBlobSize(size: number): void {
this.maxBlobSize = size;
}
onReachedMaxBlobSize(cb: (byteSize: number) => void): () => void {
this.event.on('abort-large-blob', cb);
return () => {
this.event.off('abort-large-blob', cb);
};
}
} }
+59 -26
View File
@@ -1,17 +1,23 @@
import type { Observable } from 'rxjs'; import type { Observable } from 'rxjs';
import { combineLatest, map } from 'rxjs'; import { combineLatest, map, of } from 'rxjs';
import type { DocStorage, SyncStorage } from '../../storage'; import type { DocStorage, SyncStorage } from '../../storage';
import { DummyDocStorage } from '../../storage/dummy/doc';
import { DummySyncStorage } from '../../storage/dummy/sync';
import { MANUALLY_STOP } from '../../utils/throw-if-aborted';
import type { PeerStorageOptions } from '../types';
import { DocSyncPeer } from './peer'; import { DocSyncPeer } from './peer';
export interface DocSyncState { export interface DocSyncState {
total: number; total: number;
syncing: number; syncing: number;
synced: boolean;
retrying: boolean; retrying: boolean;
errorMessage: string | null; errorMessage: string | null;
} }
export interface DocSyncDocState { export interface DocSyncDocState {
synced: boolean;
syncing: boolean; syncing: boolean;
retrying: boolean; retrying: boolean;
errorMessage: string | null; errorMessage: string | null;
@@ -24,43 +30,70 @@ export interface DocSync {
} }
export class DocSyncImpl implements DocSync { export class DocSyncImpl implements DocSync {
private readonly peers: DocSyncPeer[] = this.remotes.map( private readonly peers: DocSyncPeer[] = Object.entries(
remote => new DocSyncPeer(this.local, this.sync, remote) this.storages.remotes
).map(
([peerId, remote]) =>
new DocSyncPeer(peerId, this.storages.local, this.sync, remote)
); );
private abort: AbortController | null = null; private abort: AbortController | null = null;
readonly state$: Observable<DocSyncState> = combineLatest( get state$() {
this.peers.map(peer => peer.peerState$) return combineLatest(this.peers.map(peer => peer.peerState$)).pipe(
).pipe(
map(allPeers => ({
total: allPeers.reduce((acc, peer) => acc + peer.total, 0),
syncing: allPeers.reduce((acc, peer) => acc + peer.syncing, 0),
retrying: allPeers.some(peer => peer.retrying),
errorMessage:
allPeers.find(peer => peer.errorMessage)?.errorMessage ?? null,
}))
);
constructor(
readonly local: DocStorage,
readonly sync: SyncStorage,
readonly remotes: DocStorage[]
) {}
docState$(docId: string): Observable<DocSyncDocState> {
return combineLatest(this.peers.map(peer => peer.docState$(docId))).pipe(
map(allPeers => ({ map(allPeers => ({
total: allPeers.reduce((acc, peer) => Math.max(acc, peer.total), 0),
syncing: allPeers.reduce((acc, peer) => Math.max(acc, peer.syncing), 0),
synced: allPeers.every(peer => peer.synced),
retrying: allPeers.some(peer => peer.retrying),
errorMessage: errorMessage:
allPeers.find(peer => peer.errorMessage)?.errorMessage ?? null, allPeers.find(peer => peer.errorMessage)?.errorMessage ?? null,
retrying: allPeers.some(peer => peer.retrying),
syncing: allPeers.some(peer => peer.syncing),
})) }))
) as Observable<DocSyncState>;
}
constructor(
readonly storages: PeerStorageOptions<DocStorage>,
readonly sync: SyncStorage
) {}
/**
* for testing
*/
static get dummy() {
return new DocSyncImpl(
{
local: new DummyDocStorage(),
remotes: {},
},
new DummySyncStorage()
);
}
docState$(docId: string): Observable<DocSyncDocState> {
if (this.peers.length === 0) {
return of({
errorMessage: null,
retrying: false,
syncing: false,
synced: true,
});
}
return combineLatest(this.peers.map(peer => peer.docState$(docId))).pipe(
map(allPeers => {
return {
errorMessage:
allPeers.find(peer => peer.errorMessage)?.errorMessage ?? null,
retrying: allPeers.some(peer => peer.retrying),
syncing: allPeers.some(peer => peer.syncing),
synced: allPeers.every(peer => peer.synced),
};
})
); );
} }
start() { start() {
if (this.abort) { if (this.abort) {
this.abort.abort(); this.abort.abort(MANUALLY_STOP);
} }
const abort = new AbortController(); const abort = new AbortController();
this.abort = abort; this.abort = abort;
+76 -45
View File
@@ -43,6 +43,7 @@ interface Status {
remoteClocks: ClockMap; remoteClocks: ClockMap;
syncing: boolean; syncing: boolean;
retrying: boolean; retrying: boolean;
skipped: boolean;
errorMessage: string | null; errorMessage: string | null;
} }
@@ -50,11 +51,13 @@ interface PeerState {
total: number; total: number;
syncing: number; syncing: number;
retrying: boolean; retrying: boolean;
synced: boolean;
errorMessage: string | null; errorMessage: string | null;
} }
interface PeerDocState { interface PeerDocState {
syncing: boolean; syncing: boolean;
synced: boolean;
retrying: boolean; retrying: boolean;
errorMessage: string | null; errorMessage: string | null;
} }
@@ -92,10 +95,11 @@ export class DocSyncPeer {
/** /**
* random unique id for recognize self in "update" event * random unique id for recognize self in "update" event
*/ */
private readonly uniqueId = `sync:${this.local.universalId}:${this.remote.universalId}:${nanoid()}`; private readonly uniqueId = `sync:${this.peerId}:${nanoid()}`;
private readonly prioritySettings = new Map<string, number>(); private readonly prioritySettings = new Map<string, number>();
constructor( constructor(
readonly peerId: string,
readonly local: DocStorage, readonly local: DocStorage,
readonly syncMetadata: SyncStorage, readonly syncMetadata: SyncStorage,
readonly remote: DocStorage, readonly remote: DocStorage,
@@ -110,43 +114,59 @@ export class DocSyncPeer {
remoteClocks: new ClockMap(new Map()), remoteClocks: new ClockMap(new Map()),
syncing: false, syncing: false,
retrying: false, retrying: false,
skipped: false,
errorMessage: null, errorMessage: null,
}; };
private readonly statusUpdatedSubject$ = new Subject<string | true>(); private readonly statusUpdatedSubject$ = new Subject<string | true>();
peerState$ = new Observable<PeerState>(subscribe => { get peerState$() {
const next = () => { return new Observable<PeerState>(subscribe => {
if (!this.status.syncing) { const next = () => {
// if syncing = false, jobMap is empty if (this.status.skipped) {
subscribe.next({ subscribe.next({
total: this.status.docs.size, total: 0,
syncing: this.status.docs.size, syncing: 0,
retrying: this.status.retrying, synced: true,
errorMessage: this.status.errorMessage, retrying: false,
}); errorMessage: null,
} else { });
const syncing = this.status.jobMap.size; } else if (!this.status.syncing) {
subscribe.next({ // if syncing = false, jobMap is empty
total: this.status.docs.size, subscribe.next({
syncing: syncing, total: this.status.docs.size,
retrying: this.status.retrying, syncing: this.status.docs.size,
errorMessage: this.status.errorMessage, synced: false,
}); retrying: this.status.retrying,
} errorMessage: this.status.errorMessage,
}; });
next(); } else {
return this.statusUpdatedSubject$.subscribe(() => { const syncing = this.status.jobMap.size;
subscribe.next({
total: this.status.docs.size,
syncing: syncing,
retrying: this.status.retrying,
errorMessage: this.status.errorMessage,
synced: syncing === 0,
});
}
};
next(); next();
return this.statusUpdatedSubject$.subscribe(() => {
next();
});
}); });
}); }
docState$(docId: string) { docState$(docId: string) {
return new Observable<PeerDocState>(subscribe => { return new Observable<PeerDocState>(subscribe => {
const next = () => { const next = () => {
const syncing =
!this.status.connectedDocs.has(docId) ||
this.status.jobMap.has(docId);
subscribe.next({ subscribe.next({
syncing: syncing: syncing,
!this.status.connectedDocs.has(docId) || synced: !syncing,
this.status.jobMap.has(docId),
retrying: this.status.retrying, retrying: this.status.retrying,
errorMessage: this.status.errorMessage, errorMessage: this.status.errorMessage,
}); });
@@ -161,22 +181,21 @@ export class DocSyncPeer {
private readonly jobs = createJobErrorCatcher({ private readonly jobs = createJobErrorCatcher({
connect: async (docId: string, signal?: AbortSignal) => { connect: async (docId: string, signal?: AbortSignal) => {
const pushedClock = const pushedClock =
(await this.syncMetadata.getPeerPushedClock(this.remote.peer, docId)) (await this.syncMetadata.getPeerPushedClock(this.peerId, docId))
?.timestamp ?? null; ?.timestamp ?? null;
const clock = await this.local.getDocTimestamp(docId); const clock = await this.local.getDocTimestamp(docId);
throwIfAborted(signal); throwIfAborted(signal);
if (pushedClock === null || pushedClock !== clock?.timestamp) { if (
!this.remote.isReadonly &&
(pushedClock === null || pushedClock !== clock?.timestamp)
) {
await this.jobs.pullAndPush(docId, signal); await this.jobs.pullAndPush(docId, signal);
} else { } else {
// no need to push // no need to push
const pulled = const pulled =
( (await this.syncMetadata.getPeerPulledRemoteClock(this.peerId, docId))
await this.syncMetadata.getPeerPulledRemoteClock( ?.timestamp ?? null;
this.remote.peer,
docId
)
)?.timestamp ?? null;
if (pulled === null || pulled !== this.status.remoteClocks.get(docId)) { if (pulled === null || pulled !== this.status.remoteClocks.get(docId)) {
await this.jobs.pull(docId, signal); await this.jobs.pull(docId, signal);
} }
@@ -214,7 +233,7 @@ export class DocSyncPeer {
}); });
} }
throwIfAborted(signal); throwIfAborted(signal);
await this.syncMetadata.setPeerPushedClock(this.remote.peer, { await this.syncMetadata.setPeerPushedClock(this.peerId, {
docId, docId,
timestamp: maxClock, timestamp: maxClock,
}); });
@@ -249,7 +268,7 @@ export class DocSyncPeer {
this.uniqueId this.uniqueId
); );
throwIfAborted(signal); throwIfAborted(signal);
await this.syncMetadata.setPeerPulledRemoteClock(this.remote.peer, { await this.syncMetadata.setPeerPulledRemoteClock(this.peerId, {
docId, docId,
timestamp: remoteClock, timestamp: remoteClock,
}); });
@@ -273,7 +292,7 @@ export class DocSyncPeer {
}); });
} }
throwIfAborted(signal); throwIfAborted(signal);
await this.syncMetadata.setPeerPushedClock(this.remote.peer, { await this.syncMetadata.setPeerPushedClock(this.peerId, {
docId, docId,
timestamp: localClock, timestamp: localClock,
}); });
@@ -294,7 +313,7 @@ export class DocSyncPeer {
remoteClock, remoteClock,
}); });
} }
await this.syncMetadata.setPeerPushedClock(this.remote.peer, { await this.syncMetadata.setPeerPushedClock(this.peerId, {
docId, docId,
timestamp: localDocRecord.timestamp, timestamp: localDocRecord.timestamp,
}); });
@@ -322,7 +341,7 @@ export class DocSyncPeer {
this.uniqueId this.uniqueId
); );
throwIfAborted(signal); throwIfAborted(signal);
await this.syncMetadata.setPeerPulledRemoteClock(this.remote.peer, { await this.syncMetadata.setPeerPulledRemoteClock(this.peerId, {
docId, docId,
timestamp: remoteClock, timestamp: remoteClock,
}); });
@@ -360,7 +379,7 @@ export class DocSyncPeer {
); );
throwIfAborted(signal); throwIfAborted(signal);
await this.syncMetadata.setPeerPulledRemoteClock(this.remote.peer, { await this.syncMetadata.setPeerPulledRemoteClock(this.peerId, {
docId, docId,
timestamp: remoteClock, timestamp: remoteClock,
}); });
@@ -372,7 +391,7 @@ export class DocSyncPeer {
updateRemoteClock: async (docId: string, remoteClock: Date) => { updateRemoteClock: async (docId: string, remoteClock: Date) => {
const updated = this.status.remoteClocks.setIfBigger(docId, remoteClock); const updated = this.status.remoteClocks.setIfBigger(docId, remoteClock);
if (updated) { if (updated) {
await this.syncMetadata.setPeerRemoteClock(this.remote.peer, { await this.syncMetadata.setPeerRemoteClock(this.peerId, {
docId, docId,
timestamp: remoteClock, timestamp: remoteClock,
}); });
@@ -455,6 +474,7 @@ export class DocSyncPeer {
jobMap: new Map(), jobMap: new Map(),
remoteClocks: new ClockMap(new Map()), remoteClocks: new ClockMap(new Map()),
syncing: false, syncing: false,
skipped: false,
// tell ui to show retrying status // tell ui to show retrying status
retrying: true, retrying: true,
// error message from last retry // error message from last retry
@@ -482,6 +502,17 @@ export class DocSyncPeer {
private async retryLoop(signal?: AbortSignal) { private async retryLoop(signal?: AbortSignal) {
throwIfAborted(signal); throwIfAborted(signal);
if (this.local.isReadonly) {
// Local is readonly, skip sync
this.status.skipped = true;
this.statusUpdatedSubject$.next(true);
await new Promise((_, reject) => {
signal?.addEventListener('abort', reason => {
reject(reason);
});
});
return;
}
const abort = new AbortController(); const abort = new AbortController();
signal?.addEventListener('abort', reason => { signal?.addEventListener('abort', reason => {
@@ -536,8 +567,8 @@ export class DocSyncPeer {
if ( if (
origin === this.uniqueId || origin === this.uniqueId ||
origin?.startsWith( origin?.startsWith(
`sync:${this.local.peer}:${this.remote.peer}:` `sync:${this.peerId}:`
// skip if local and remote is same // skip if peerId is same
) )
) { ) {
return; return;
@@ -572,7 +603,7 @@ export class DocSyncPeer {
// get cached clocks from metadata // get cached clocks from metadata
const cachedClocks = await this.syncMetadata.getPeerRemoteClocks( const cachedClocks = await this.syncMetadata.getPeerRemoteClocks(
this.remote.peer this.peerId
); );
throwIfAborted(signal); throwIfAborted(signal);
for (const [id, v] of Object.entries(cachedClocks)) { for (const [id, v] of Object.entries(cachedClocks)) {
+45 -47
View File
@@ -1,65 +1,63 @@
import { combineLatest, map, type Observable, of } from 'rxjs'; import { map, type Observable } from 'rxjs';
import type { import type { SpaceStorage } from '../storage';
AwarenessStorage,
BlobStorage,
DocStorage,
SpaceStorage,
} from '../storage';
import { AwarenessSyncImpl } from './awareness'; import { AwarenessSyncImpl } from './awareness';
import { BlobSyncImpl } from './blob'; import { BlobSyncImpl } from './blob';
import { DocSyncImpl, type DocSyncState } from './doc'; import { DocSyncImpl, type DocSyncState } from './doc';
import type { PeerStorageOptions } from './types';
export type { BlobSyncState } from './blob';
export type { DocSyncDocState, DocSyncState } from './doc';
export interface SyncState { export interface SyncState {
doc?: DocSyncState; doc?: DocSyncState;
} }
export class Sync { export class Sync {
readonly doc: DocSyncImpl | null; readonly doc: DocSyncImpl;
readonly blob: BlobSyncImpl | null; readonly blob: BlobSyncImpl;
readonly awareness: AwarenessSyncImpl | null; readonly awareness: AwarenessSyncImpl;
readonly state$: Observable<SyncState>; readonly state$: Observable<SyncState>;
constructor( constructor(readonly storages: PeerStorageOptions<SpaceStorage>) {
readonly local: SpaceStorage, const doc = storages.local.get('doc');
readonly peers: SpaceStorage[] const blob = storages.local.get('blob');
) { const sync = storages.local.get('sync');
const doc = local.tryGet('doc'); const awareness = storages.local.get('awareness');
const blob = local.tryGet('blob');
const sync = local.tryGet('sync');
const awareness = local.tryGet('awareness');
this.doc = this.doc = new DocSyncImpl(
doc && sync {
? new DocSyncImpl( local: doc,
doc, remotes: Object.fromEntries(
sync, Object.entries(storages.remotes).map(([peerId, remote]) => [
peers peerId,
.map(peer => peer.tryGet('doc')) remote.get('doc'),
.filter((v): v is DocStorage => !!v) ])
) ),
: null; },
this.blob = blob sync
? new BlobSyncImpl(
blob,
peers
.map(peer => peer.tryGet('blob'))
.filter((v): v is BlobStorage => !!v)
)
: null;
this.awareness = awareness
? new AwarenessSyncImpl(
awareness,
peers
.map(peer => peer.tryGet('awareness'))
.filter((v): v is AwarenessStorage => !!v)
)
: null;
this.state$ = combineLatest([this.doc?.state$ ?? of(undefined)]).pipe(
map(([doc]) => ({ doc }))
); );
this.blob = new BlobSyncImpl({
local: blob,
remotes: Object.fromEntries(
Object.entries(storages.remotes).map(([peerId, remote]) => [
peerId,
remote.get('blob'),
])
),
});
this.awareness = new AwarenessSyncImpl({
local: awareness,
remotes: Object.fromEntries(
Object.entries(storages.remotes).map(([peerId, remote]) => [
peerId,
remote.get('awareness'),
])
),
});
this.state$ = this.doc.state$.pipe(map(doc => ({ doc })));
} }
start() { start() {
@@ -0,0 +1,4 @@
export interface PeerStorageOptions<S> {
local: S;
remotes: Record<string, S>;
}
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { parseUniversalId, universalId } from '../storage'; import { parseUniversalId, universalId } from '../universal-id';
describe('parseUniversalId', () => { describe('parseUniversalId', () => {
it('should generate universal id', () => { it('should generate universal id', () => {
@@ -0,0 +1,93 @@
export type SpaceType = 'workspace' | 'userspace';
export function universalId({
peer,
type,
id,
}: {
peer: string;
type: SpaceType;
id: string;
}) {
return `@peer(${peer});@type(${type});@id(${id});`;
}
export function isValidSpaceType(type: string): type is SpaceType {
return type === 'workspace' || type === 'userspace';
}
export function isValidUniversalId(opts: Record<string, string>): boolean {
const requiredKeys = ['peer', 'type', 'id'] as const;
for (const key of requiredKeys) {
if (!opts[key]) {
return false;
}
}
return isValidSpaceType(opts.type);
}
export function parseUniversalId(id: string): {
peer: string;
type: SpaceType;
id: string;
} {
const result: Partial<{
peer: string;
type: SpaceType;
id: string;
}> = {};
let key = '';
let value = '';
let isInValue = false;
let i = -1;
while (++i < id.length) {
const ch = id[i];
const nextCh = id[i + 1];
// when we are in value string, we only care about ch and next char to be [')', ';'] to end the id part
if (isInValue) {
if (ch === ')' && nextCh === ';') {
// @ts-expect-error we know the key is valid
result[key] = value;
key = '';
value = '';
isInValue = false;
i++;
continue;
}
value += ch;
continue;
}
if (ch === '@') {
const keyEnd = id.indexOf('(', i);
// we find '@' but no '(' in lookahead or '(' is immediately after '@', invalid id
if (keyEnd === -1 || keyEnd === i + 1) {
break;
}
key = id.slice(i + 1, keyEnd);
i = keyEnd;
isInValue = true;
} else {
break;
}
}
if (!isValidUniversalId(result)) {
throw new Error(
`Invalid universal storage id: ${id}. It should be in format of @peer(\${peer});@type(\${type});@id(\${id});`
);
}
return result as {
peer: string;
type: SpaceType;
id: string;
};
}
+67 -90
View File
@@ -1,54 +1,58 @@
import type { OpClient } from '@toeverything/infra/op'; import type { OpClient } from '@toeverything/infra/op';
import { DummyConnection } from '../connection'; import { DummyConnection } from '../connection';
import { DocFrontend } from '../frontend/doc'; import { AwarenessFrontend, BlobFrontend, DocFrontend } from '../frontend';
import { import {
type AwarenessRecord, type AwarenessRecord,
type AwarenessStorage,
type BlobRecord, type BlobRecord,
type BlobStorage, type BlobStorage,
type DocRecord, type DocRecord,
type DocStorage, type DocStorage,
type DocUpdate, type DocUpdate,
type ListedBlobRecord, type ListedBlobRecord,
type StorageOptions,
universalId,
} from '../storage'; } from '../storage';
import type { AwarenessSync } from '../sync/awareness'; import type { AwarenessSync } from '../sync/awareness';
import type { BlobSync } from '../sync/blob'; import type { BlobSync } from '../sync/blob';
import type { DocSync } from '../sync/doc'; import type { DocSync } from '../sync/doc';
import type { WorkerOps } from './ops'; import type { WorkerInitOptions, WorkerOps } from './ops';
export type { WorkerInitOptions } from './ops';
export class WorkerClient { export class WorkerClient {
constructor( constructor(
private readonly client: OpClient<WorkerOps>, private readonly client: OpClient<WorkerOps>,
private readonly options: StorageOptions options: WorkerInitOptions
) {} ) {
client.listen();
this.client.call('worker.init', options).catch(err => {
console.error('error initializing worker', err);
});
this.docStorage = new WorkerDocStorage(this.client);
this.blobStorage = new WorkerBlobStorage(this.client);
this.docSync = new WorkerDocSync(this.client);
this.blobSync = new WorkerBlobSync(this.client);
this.awarenessSync = new WorkerAwarenessSync(this.client);
this.docFrontend = new DocFrontend(this.docStorage, this.docSync);
this.blobFrontend = new BlobFrontend(this.blobStorage, this.blobSync);
this.awarenessFrontend = new AwarenessFrontend(this.awarenessSync);
}
readonly docStorage = new WorkerDocStorage(this.client, this.options); private readonly docStorage: WorkerDocStorage;
readonly blobStorage = new WorkerBlobStorage(this.client, this.options); private readonly blobStorage: WorkerBlobStorage;
readonly awarenessStorage = new WorkerAwarenessStorage( private readonly docSync: WorkerDocSync;
this.client, private readonly blobSync: WorkerBlobSync;
this.options private readonly awarenessSync: WorkerAwarenessSync;
);
readonly docSync = new WorkerDocSync(this.client);
readonly blobSync = new WorkerBlobSync(this.client);
readonly awarenessSync = new WorkerAwarenessSync(this.client);
readonly docFrontend = new DocFrontend(this.docStorage, this.docSync); readonly docFrontend: DocFrontend;
readonly blobFrontend: BlobFrontend;
readonly awarenessFrontend: AwarenessFrontend;
} }
class WorkerDocStorage implements DocStorage { class WorkerDocStorage implements DocStorage {
constructor( constructor(private readonly client: OpClient<WorkerOps>) {}
private readonly client: OpClient<WorkerOps>,
private readonly options: StorageOptions
) {}
readonly peer = this.options.peer;
readonly spaceType = this.options.type;
readonly spaceId = this.options.id;
readonly universalId = universalId(this.options);
readonly storageType = 'doc'; readonly storageType = 'doc';
readonly isReadonly = false;
async getDoc(docId: string) { async getDoc(docId: string) {
return this.client.call('docStorage.getDoc', docId); return this.client.call('docStorage.getDoc', docId);
@@ -119,16 +123,9 @@ class WorkerDocConnection extends DummyConnection {
} }
class WorkerBlobStorage implements BlobStorage { class WorkerBlobStorage implements BlobStorage {
constructor( constructor(private readonly client: OpClient<WorkerOps>) {}
private readonly client: OpClient<WorkerOps>,
private readonly options: StorageOptions
) {}
readonly storageType = 'blob'; readonly storageType = 'blob';
readonly peer = this.options.peer;
readonly spaceType = this.options.type;
readonly spaceId = this.options.id;
readonly universalId = universalId(this.options);
get(key: string, _signal?: AbortSignal): Promise<BlobRecord | null> { get(key: string, _signal?: AbortSignal): Promise<BlobRecord | null> {
return this.client.call('blobStorage.getBlob', key); return this.client.call('blobStorage.getBlob', key);
@@ -156,63 +153,6 @@ class WorkerBlobStorage implements BlobStorage {
connection = new DummyConnection(); connection = new DummyConnection();
} }
class WorkerAwarenessStorage implements AwarenessStorage {
constructor(
private readonly client: OpClient<WorkerOps>,
private readonly options: StorageOptions
) {}
readonly storageType = 'awareness';
readonly peer = this.options.peer;
readonly spaceType = this.options.type;
readonly spaceId = this.options.id;
readonly universalId = universalId(this.options);
update(record: AwarenessRecord, origin?: string): Promise<void> {
return this.client.call('awarenessStorage.update', {
awareness: record,
origin,
});
}
subscribeUpdate(
id: string,
onUpdate: (update: AwarenessRecord, origin?: string) => void,
onCollect: () => Promise<AwarenessRecord | null>
): () => void {
const subscription = this.client
.ob$('awarenessStorage.subscribeUpdate', id)
.subscribe({
next: update => {
if (update.type === 'awareness-update') {
onUpdate(update.awareness, update.origin);
}
if (update.type === 'awareness-collect') {
onCollect()
.then(record => {
if (record) {
this.client
.call('awarenessStorage.collect', {
awareness: record,
collectId: update.collectId,
})
.catch(err => {
console.error('error feedback collected awareness', err);
});
}
})
.catch(err => {
console.error('error collecting awareness', err);
});
}
},
});
return () => {
subscription.unsubscribe();
};
}
connection = new DummyConnection();
}
class WorkerDocSync implements DocSync { class WorkerDocSync implements DocSync {
constructor(private readonly client: OpClient<WorkerOps>) {} constructor(private readonly client: OpClient<WorkerOps>) {}
@@ -234,6 +174,22 @@ class WorkerDocSync implements DocSync {
class WorkerBlobSync implements BlobSync { class WorkerBlobSync implements BlobSync {
constructor(private readonly client: OpClient<WorkerOps>) {} constructor(private readonly client: OpClient<WorkerOps>) {}
readonly state$ = this.client.ob$('blobSync.state');
setMaxBlobSize(size: number): void {
this.client.call('blobSync.setMaxBlobSize', size).catch(err => {
console.error('error setting max blob size', err);
});
}
onReachedMaxBlobSize(cb: (byteSize: number) => void): () => void {
const subscription = this.client
.ob$('blobSync.onReachedMaxBlobSize')
.subscribe(byteSize => {
cb(byteSize);
});
return () => {
subscription.unsubscribe();
};
}
downloadBlob( downloadBlob(
blobId: string, blobId: string,
_signal?: AbortSignal _signal?: AbortSignal
@@ -243,6 +199,27 @@ class WorkerBlobSync implements BlobSync {
uploadBlob(blob: BlobRecord, _signal?: AbortSignal): Promise<void> { uploadBlob(blob: BlobRecord, _signal?: AbortSignal): Promise<void> {
return this.client.call('blobSync.uploadBlob', blob); return this.client.call('blobSync.uploadBlob', blob);
} }
fullSync(signal?: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
const abortListener = () => {
reject(signal?.reason);
subscription.unsubscribe();
};
signal?.addEventListener('abort', abortListener);
const subscription = this.client.ob$('blobSync.fullSync').subscribe({
next() {
signal?.removeEventListener('abort', abortListener);
resolve();
},
error(err) {
signal?.removeEventListener('abort', abortListener);
reject(err);
},
});
});
}
} }
class WorkerAwarenessSync implements AwarenessSync { class WorkerAwarenessSync implements AwarenessSync {
+84 -50
View File
@@ -1,27 +1,29 @@
import type { OpConsumer } from '@toeverything/infra/op'; import type { OpConsumer } from '@toeverything/infra/op';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { getAvailableStorageImplementations } from '../impls'; import { type StorageConstructor } from '../impls';
import { SpaceStorage, type StorageOptions } from '../storage'; import { SpaceStorage } from '../storage';
import type { AwarenessRecord } from '../storage/awareness'; import type { AwarenessRecord } from '../storage/awareness';
import { Sync } from '../sync'; import { Sync } from '../sync';
import type { WorkerOps } from './ops'; import type { PeerStorageOptions } from '../sync/types';
import type { WorkerInitOptions, WorkerOps } from './ops';
export type { WorkerOps };
export class WorkerConsumer { export class WorkerConsumer {
private remotes: SpaceStorage[] = []; private storages: PeerStorageOptions<SpaceStorage> | null = null;
private local: SpaceStorage | null = null;
private sync: Sync | null = null; private sync: Sync | null = null;
get ensureLocal() { get ensureLocal() {
if (!this.local) { if (!this.storages) {
throw new Error('Not initialized'); throw new Error('Not initialized');
} }
return this.local; return this.storages.local;
} }
get ensureSync() { get ensureSync() {
if (!this.sync) { if (!this.sync) {
throw new Error('Not initialized'); throw new Error('Sync not initialized');
} }
return this.sync; return this.sync;
} }
@@ -31,11 +33,7 @@ export class WorkerConsumer {
} }
get docSync() { get docSync() {
const docSync = this.ensureSync.doc; return this.ensureSync.doc;
if (!docSync) {
throw new Error('Doc sync not initialized');
}
return docSync;
} }
get blobStorage() { get blobStorage() {
@@ -43,11 +41,7 @@ export class WorkerConsumer {
} }
get blobSync() { get blobSync() {
const blobSync = this.ensureSync.blob; return this.ensureSync.blob;
if (!blobSync) {
throw new Error('Blob sync not initialized');
}
return blobSync;
} }
get syncStorage() { get syncStorage() {
@@ -59,41 +53,58 @@ export class WorkerConsumer {
} }
get awarenessSync() { get awarenessSync() {
const awarenessSync = this.ensureSync.awareness; return this.ensureSync.awareness;
if (!awarenessSync) {
throw new Error('Awareness sync not initialized');
}
return awarenessSync;
} }
constructor(private readonly consumer: OpConsumer<WorkerOps>) {} constructor(
private readonly consumer: OpConsumer<WorkerOps>,
listen() { private readonly availableStorageImplementations: StorageConstructor[]
) {
this.registerHandlers(); this.registerHandlers();
this.consumer.listen(); this.consumer.listen();
} }
async init(init: { init(init: WorkerInitOptions) {
local: { name: string; opts: StorageOptions }[]; this.storages = {
remotes: { name: string; opts: StorageOptions }[][]; local: new SpaceStorage(
}) { Object.fromEntries(
this.local = new SpaceStorage( Object.entries(init.local).map(([type, opt]) => {
init.local.map(opt => { const Storage = this.availableStorageImplementations.find(
const Storage = getAvailableStorageImplementations(opt.name); impl => impl.identifier === opt.name
return new Storage(opt.opts); );
}) if (!Storage) {
); throw new Error(`Storage implementation ${opt.name} not found`);
this.remotes = init.remotes.map(opts => { }
return new SpaceStorage( return [type, new Storage(opt.opts as any)];
opts.map(opt => { })
const Storage = getAvailableStorageImplementations(opt.name); )
return new Storage(opt.opts); ),
remotes: Object.fromEntries(
Object.entries(init.remotes).map(([peer, opts]) => {
return [
peer,
new SpaceStorage(
Object.fromEntries(
Object.entries(opts).map(([type, opt]) => {
const Storage = this.availableStorageImplementations.find(
impl => impl.identifier === opt.name
);
if (!Storage) {
throw new Error(
`Storage implementation ${opt.name} not found`
);
}
return [type, new Storage(opt.opts as any)];
})
)
),
];
}) })
); ),
}); };
this.sync = new Sync(this.local, this.remotes); this.sync = new Sync(this.storages);
this.local.connect(); this.storages.local.connect();
for (const remote of this.remotes) { for (const remote of Object.values(this.storages.remotes)) {
remote.connect(); remote.connect();
} }
this.sync.start(); this.sync.start();
@@ -101,9 +112,9 @@ export class WorkerConsumer {
async destroy() { async destroy() {
this.sync?.stop(); this.sync?.stop();
this.local?.disconnect(); this.storages?.local.disconnect();
await this.local?.destroy(); await this.storages?.local.destroy();
for (const remote of this.remotes) { for (const remote of Object.values(this.storages?.remotes ?? {})) {
remote.disconnect(); remote.disconnect();
await remote.destroy(); await remote.destroy();
} }
@@ -144,7 +155,7 @@ export class WorkerConsumer {
subscriber.next(true); subscriber.next(true);
subscriber.complete(); subscriber.complete();
}) })
.catch(error => { .catch((error: any) => {
subscriber.error(error); subscriber.error(error);
}); });
return () => abortController.abort(); return () => abortController.abort();
@@ -224,6 +235,29 @@ export class WorkerConsumer {
}), }),
'blobSync.downloadBlob': key => this.blobSync.downloadBlob(key), 'blobSync.downloadBlob': key => this.blobSync.downloadBlob(key),
'blobSync.uploadBlob': blob => this.blobSync.uploadBlob(blob), 'blobSync.uploadBlob': blob => this.blobSync.uploadBlob(blob),
'blobSync.fullSync': () =>
new Observable(subscriber => {
const abortController = new AbortController();
this.blobSync
.fullSync(abortController.signal)
.then(() => {
subscriber.next(true);
subscriber.complete();
})
.catch(error => {
subscriber.error(error);
});
return () => abortController.abort();
}),
'blobSync.state': () => this.blobSync.state$,
'blobSync.setMaxBlobSize': size => this.blobSync.setMaxBlobSize(size),
'blobSync.onReachedMaxBlobSize': () =>
new Observable(subscriber => {
const undo = this.blobSync.onReachedMaxBlobSize(byteSize => {
subscriber.next(byteSize);
});
return () => undo();
}),
'awarenessSync.update': ({ awareness, origin }) => 'awarenessSync.update': ({ awareness, origin }) =>
this.awarenessSync.update(awareness, origin), this.awarenessSync.update(awareness, origin),
'awarenessSync.subscribeUpdate': docId => 'awarenessSync.subscribeUpdate': docId =>
+20 -8
View File
@@ -1,3 +1,4 @@
import type { AvailableStorageImplementations } from '../impls';
import type { import type {
BlobRecord, BlobRecord,
DocClock, DocClock,
@@ -6,20 +7,27 @@ import type {
DocRecord, DocRecord,
DocUpdate, DocUpdate,
ListedBlobRecord, ListedBlobRecord,
StorageOptions, StorageType,
} from '../storage'; } from '../storage';
import type { AwarenessRecord } from '../storage/awareness'; import type { AwarenessRecord } from '../storage/awareness';
import type { BlobSyncState } from '../sync/blob';
import type { DocSyncDocState, DocSyncState } from '../sync/doc'; import type { DocSyncDocState, DocSyncState } from '../sync/doc';
type StorageInitOptions = Values<{
[key in keyof AvailableStorageImplementations]: {
name: key;
opts: ConstructorParameters<AvailableStorageImplementations[key]>[0];
};
}>;
export interface WorkerInitOptions {
local: { [key in StorageType]?: StorageInitOptions };
remotes: Record<string, { [key in StorageType]?: StorageInitOptions }>;
}
interface GroupedWorkerOps { interface GroupedWorkerOps {
worker: { worker: {
init: [ init: [WorkerInitOptions, void];
{
local: { name: string; opts: StorageOptions }[];
remotes: { name: string; opts: StorageOptions }[][];
},
void,
];
destroy: [void, void]; destroy: [void, void];
}; };
@@ -83,6 +91,10 @@ interface GroupedWorkerOps {
blobSync: { blobSync: {
downloadBlob: [string, BlobRecord | null]; downloadBlob: [string, BlobRecord | null];
uploadBlob: [BlobRecord, void]; uploadBlob: [BlobRecord, void];
fullSync: [void, boolean];
setMaxBlobSize: [number, void];
onReachedMaxBlobSize: [void, number];
state: [void, BlobSyncState];
}; };
awarenessSync: { awarenessSync: {
-3
View File
@@ -10,9 +10,6 @@
{ {
"path": "../../frontend/graphql" "path": "../../frontend/graphql"
}, },
{
"path": "../../frontend/electron-api"
},
{ {
"path": "../infra" "path": "../infra"
}, },
@@ -1,33 +0,0 @@
import { type BlobRecord, BlobStorageBase, share } from '@affine/nbstore';
import { NativeDBConnection } from './db';
export class SqliteBlobStorage extends BlobStorageBase {
override connection = share(
new NativeDBConnection(this.peer, this.spaceType, this.spaceId)
);
get db() {
return this.connection.inner;
}
override async get(key: string) {
return this.db.getBlob(key);
}
override async set(blob: BlobRecord) {
await this.db.setBlob(blob);
}
override async delete(key: string, permanently: boolean) {
await this.db.deleteBlob(key, permanently);
}
override async release() {
await this.db.releaseBlobs();
}
override async list() {
return this.db.listBlobs();
}
}
@@ -1,46 +0,0 @@
import path from 'node:path';
import { DocStorage as NativeDocStorage } from '@affine/native';
import { AutoReconnectConnection, type SpaceType } from '@affine/nbstore';
import fs from 'fs-extra';
import { logger } from '../logger';
import { getSpaceDBPath } from '../workspace/meta';
export class NativeDBConnection extends AutoReconnectConnection<NativeDocStorage> {
constructor(
private readonly peer: string,
private readonly type: SpaceType,
private readonly id: string
) {
super();
}
async getDBPath() {
return await getSpaceDBPath(this.peer, this.type, this.id);
}
override get shareId(): string {
return `sqlite:${this.peer}:${this.type}:${this.id}`;
}
override async doConnect() {
const dbPath = await this.getDBPath();
await fs.ensureDir(path.dirname(dbPath));
const conn = new NativeDocStorage(dbPath);
await conn.connect();
logger.info('[nbstore] connection established', this.shareId);
return conn;
}
override doDisconnect(conn: NativeDocStorage) {
conn
.close()
.then(() => {
logger.info('[nbstore] connection closed', this.shareId);
})
.catch(err => {
logger.error('[nbstore] connection close failed', this.shareId, err);
});
}
}
@@ -1,83 +0,0 @@
import {
type DocClocks,
type DocRecord,
DocStorageBase,
type DocUpdate,
share,
} from '@affine/nbstore';
import { NativeDBConnection } from './db';
export class SqliteDocStorage extends DocStorageBase {
override connection = share(
new NativeDBConnection(this.peer, this.spaceType, this.spaceId)
);
get db() {
return this.connection.inner;
}
override async pushDocUpdate(update: DocUpdate) {
const timestamp = await this.db.pushUpdate(update.docId, update.bin);
return { docId: update.docId, timestamp };
}
override async deleteDoc(docId: string) {
await this.db.deleteDoc(docId);
}
override async getDocTimestamps(after?: Date) {
const clocks = await this.db.getDocClocks(after);
return clocks.reduce((ret, cur) => {
ret[cur.docId] = cur.timestamp;
return ret;
}, {} as DocClocks);
}
override async getDocTimestamp(docId: string) {
return this.db.getDocClock(docId);
}
protected override async getDocSnapshot(docId: string) {
const snapshot = await this.db.getDocSnapshot(docId);
if (!snapshot) {
return null;
}
return {
docId,
bin: snapshot.data,
timestamp: snapshot.timestamp,
};
}
protected override async setDocSnapshot(
snapshot: DocRecord
): Promise<boolean> {
return this.db.setDocSnapshot({
docId: snapshot.docId,
data: Buffer.from(snapshot.bin),
timestamp: new Date(snapshot.timestamp),
});
}
protected override async getDocUpdates(docId: string) {
return this.db.getDocUpdates(docId).then(updates =>
updates.map(update => ({
docId,
bin: update.data,
timestamp: update.createdAt,
}))
);
}
protected override markUpdatesMerged(docId: string, updates: DocRecord[]) {
return this.db.markUpdatesMerged(
docId,
updates.map(update => update.timestamp)
);
}
}
@@ -1,128 +1,43 @@
import { import path from 'node:path';
type BlobRecord,
type DocClock,
type DocUpdate,
} from '@affine/nbstore';
import { ensureStorage, getStorage } from './storage'; import { DocStoragePool } from '@affine/native';
import { parseUniversalId } from '@affine/nbstore';
import type { NativeDBApis } from '@affine/nbstore/sqlite';
import fs from 'fs-extra';
export const nbstoreHandlers = { import { getSpaceDBPath } from '../workspace/meta';
connect: async (id: string) => {
await ensureStorage(id); const POOL = new DocStoragePool();
},
export const nbstoreHandlers: NativeDBApis = {
close: async (id: string) => { connect: async (universalId: string) => {
const store = getStorage(id); const { peer, type, id } = parseUniversalId(universalId);
const dbPath = await getSpaceDBPath(peer, type, id);
if (store) { await fs.ensureDir(path.dirname(dbPath));
store.disconnect(); await POOL.connect(universalId, dbPath);
// The store may be shared with other tabs, so we don't delete it from cache
// the underlying connection will handle the close correctly
// STORE_CACHE.delete(`${spaceType}:${spaceId}`);
}
},
pushDocUpdate: async (id: string, update: DocUpdate) => {
const store = await ensureStorage(id);
return store.get('doc').pushDocUpdate(update);
},
getDoc: async (id: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('doc').getDoc(docId);
},
deleteDoc: async (id: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('doc').deleteDoc(docId);
},
getDocTimestamps: async (id: string, after?: Date) => {
const store = await ensureStorage(id);
return store.get('doc').getDocTimestamps(after);
},
getDocTimestamp: async (id: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('doc').getDocTimestamp(docId);
},
setBlob: async (id: string, blob: BlobRecord) => {
const store = await ensureStorage(id);
return store.get('blob').set(blob);
},
getBlob: async (id: string, key: string) => {
const store = await ensureStorage(id);
return store.get('blob').get(key);
},
deleteBlob: async (id: string, key: string, permanently: boolean) => {
const store = await ensureStorage(id);
return store.get('blob').delete(key, permanently);
},
listBlobs: async (id: string) => {
const store = await ensureStorage(id);
return store.get('blob').list();
},
releaseBlobs: async (id: string) => {
const store = await ensureStorage(id);
return store.get('blob').release();
},
getPeerRemoteClocks: async (id: string, peer: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerRemoteClocks(peer);
},
getPeerRemoteClock: async (id: string, peer: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerRemoteClock(peer, docId);
},
setPeerRemoteClock: async (id: string, peer: string, clock: DocClock) => {
const store = await ensureStorage(id);
return store.get('sync').setPeerRemoteClock(peer, clock);
},
getPeerPulledRemoteClocks: async (id: string, peer: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerPulledRemoteClocks(peer);
},
getPeerPulledRemoteClock: async (id: string, peer: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerPulledRemoteClock(peer, docId);
},
setPeerPulledRemoteClock: async (
id: string,
peer: string,
clock: DocClock
) => {
const store = await ensureStorage(id);
return store.get('sync').setPeerPulledRemoteClock(peer, clock);
},
getPeerPushedClocks: async (id: string, peer: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerPushedClocks(peer);
},
getPeerPushedClock: async (id: string, peer: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerPushedClock(peer, docId);
},
setPeerPushedClock: async (id: string, peer: string, clock: DocClock) => {
const store = await ensureStorage(id);
return store.get('sync').setPeerPushedClock(peer, clock);
},
clearClocks: async (id: string) => {
const store = await ensureStorage(id);
return store.get('sync').clearClocks();
}, },
disconnect: POOL.disconnect.bind(POOL),
pushUpdate: POOL.pushUpdate.bind(POOL),
getDocSnapshot: POOL.getDocSnapshot.bind(POOL),
setDocSnapshot: POOL.setDocSnapshot.bind(POOL),
getDocUpdates: POOL.getDocUpdates.bind(POOL),
markUpdatesMerged: POOL.markUpdatesMerged.bind(POOL),
deleteDoc: POOL.deleteDoc.bind(POOL),
getDocClocks: POOL.getDocClocks.bind(POOL),
getDocClock: POOL.getDocClock.bind(POOL),
getBlob: POOL.getBlob.bind(POOL),
setBlob: POOL.setBlob.bind(POOL),
deleteBlob: POOL.deleteBlob.bind(POOL),
releaseBlobs: POOL.releaseBlobs.bind(POOL),
listBlobs: POOL.listBlobs.bind(POOL),
getPeerRemoteClocks: POOL.getPeerRemoteClocks.bind(POOL),
getPeerRemoteClock: POOL.getPeerRemoteClock.bind(POOL),
setPeerRemoteClock: POOL.setPeerRemoteClock.bind(POOL),
getPeerPulledRemoteClocks: POOL.getPeerPulledRemoteClocks.bind(POOL),
getPeerPulledRemoteClock: POOL.getPeerPulledRemoteClock.bind(POOL),
setPeerPulledRemoteClock: POOL.setPeerPulledRemoteClock.bind(POOL),
getPeerPushedClocks: POOL.getPeerPushedClocks.bind(POOL),
getPeerPushedClock: POOL.getPeerPushedClock.bind(POOL),
setPeerPushedClock: POOL.setPeerPushedClock.bind(POOL),
clearClocks: POOL.clearClocks.bind(POOL),
}; };
@@ -1,4 +1,3 @@
export { nbstoreHandlers } from './handlers'; export { nbstoreHandlers } from './handlers';
export * from './storage';
export { dbEvents as dbEventsV1, dbHandlers as dbHandlersV1 } from './v1'; export { dbEvents as dbEventsV1, dbHandlers as dbHandlersV1 } from './v1';
export { universalId } from '@affine/nbstore'; export { universalId } from '@affine/nbstore';
@@ -1,92 +0,0 @@
import { parseUniversalId, SpaceStorage } from '@affine/nbstore';
import { applyUpdate, Doc as YDoc } from 'yjs';
import { logger } from '../logger';
import { SqliteBlobStorage } from './blob';
import { NativeDBConnection } from './db';
import { SqliteDocStorage } from './doc';
import { SqliteSyncStorage } from './sync';
export class SqliteSpaceStorage extends SpaceStorage {
get connection() {
const docStore = this.get('doc');
if (!docStore) {
throw new Error('doc store not found');
}
const connection = docStore.connection;
if (!(connection instanceof NativeDBConnection)) {
throw new Error('doc store connection is not a Sqlite connection');
}
return connection;
}
async getDBPath() {
return this.connection.getDBPath();
}
async getWorkspaceName() {
const docStore = this.tryGet('doc');
if (!docStore) {
return null;
}
const doc = await docStore.getDoc(docStore.spaceId);
if (!doc) {
return null;
}
const ydoc = new YDoc();
applyUpdate(ydoc, doc.bin);
return ydoc.getMap('meta').get('name') as string;
}
async checkpoint() {
await this.connection.inner.checkpoint();
}
}
const STORE_CACHE = new Map<string, SqliteSpaceStorage>();
process.on('beforeExit', () => {
STORE_CACHE.forEach(store => {
store.destroy().catch(err => {
logger.error('[nbstore] destroy store failed', err);
});
});
});
export function getStorage(universalId: string) {
return STORE_CACHE.get(universalId);
}
export async function ensureStorage(universalId: string) {
const { peer, type, id } = parseUniversalId(universalId);
let store = STORE_CACHE.get(universalId);
if (!store) {
const opts = {
peer,
type,
id,
};
store = new SqliteSpaceStorage([
new SqliteDocStorage(opts),
new SqliteBlobStorage(opts),
new SqliteSyncStorage(opts),
]);
store.connect();
await store.waitForConnected();
STORE_CACHE.set(universalId, store);
}
return store;
}
@@ -1,70 +0,0 @@
import {
BasicSyncStorage,
type DocClock,
type DocClocks,
share,
} from '@affine/nbstore';
import { NativeDBConnection } from './db';
export class SqliteSyncStorage extends BasicSyncStorage {
override connection = share(
new NativeDBConnection(this.peer, this.spaceType, this.spaceId)
);
get db() {
return this.connection.inner;
}
override async getPeerRemoteClocks(peer: string) {
const records = await this.db.getPeerRemoteClocks(peer);
return records.reduce((clocks, { docId, timestamp }) => {
clocks[docId] = timestamp;
return clocks;
}, {} as DocClocks);
}
override async getPeerRemoteClock(peer: string, docId: string) {
return this.db.getPeerRemoteClock(peer, docId);
}
override async setPeerRemoteClock(peer: string, clock: DocClock) {
await this.db.setPeerRemoteClock(peer, clock.docId, clock.timestamp);
}
override async getPeerPulledRemoteClock(peer: string, docId: string) {
return this.db.getPeerPulledRemoteClock(peer, docId);
}
override async getPeerPulledRemoteClocks(peer: string) {
const records = await this.db.getPeerPulledRemoteClocks(peer);
return records.reduce((clocks, { docId, timestamp }) => {
clocks[docId] = timestamp;
return clocks;
}, {} as DocClocks);
}
override async setPeerPulledRemoteClock(peer: string, clock: DocClock) {
await this.db.setPeerPulledRemoteClock(peer, clock.docId, clock.timestamp);
}
override async getPeerPushedClocks(peer: string) {
const records = await this.db.getPeerPushedClocks(peer);
return records.reduce((clocks, { docId, timestamp }) => {
clocks[docId] = timestamp;
return clocks;
}, {} as DocClocks);
}
override async getPeerPushedClock(peer: string, docId: string) {
return this.db.getPeerPushedClock(peer, docId);
}
override async setPeerPushedClock(peer: string, clock: DocClock) {
await this.db.setPeerPushedClock(peer, clock.docId, clock.timestamp);
}
override async clearClocks() {
await this.db.clearClocks();
}
}
@@ -13,6 +13,7 @@
50A285D72D112A5E000D5A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */; }; 50A285D72D112A5E000D5A6D /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */; };
50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; }; 50A285D82D112A5E000D5A6D /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */; };
50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50A285DB2D112B24000D5A6D /* Intelligents */; }; 50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */ = {isa = PBXBuildFile; productRef = 50A285DB2D112B24000D5A6D /* Intelligents */; };
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */; };
9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; }; 9D6A85332CCF6DA700DAB35F /* HashcashPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */; };
9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; }; 9D90BE252CCB9876006677DB /* CookieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE172CCB9876006677DB /* CookieManager.swift */; };
9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; }; 9D90BE262CCB9876006677DB /* CookiePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D90BE182CCB9876006677DB /* CookiePlugin.swift */; };
@@ -23,8 +24,8 @@
9D90BE2B2CCB9876006677DB /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1F2CCB9876006677DB /* config.xml */; }; 9D90BE2B2CCB9876006677DB /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE1F2CCB9876006677DB /* config.xml */; };
9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; }; 9D90BE2D2CCB9876006677DB /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE222CCB9876006677DB /* Main.storyboard */; };
9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; }; 9D90BE2E2CCB9876006677DB /* public in Resources */ = {isa = PBXBuildFile; fileRef = 9D90BE232CCB9876006677DB /* public */; };
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 */; }; C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
C4C97C6E2D0304D100BC2AD1 /* libaffine_mobile_native.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C4C97C6D2D0304D100BC2AD1 /* libaffine_mobile_native.a */; };
C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C6F2D0307B700BC2AD1 /* affine_mobile_native.swift */; }; C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C6F2D0307B700BC2AD1 /* affine_mobile_native.swift */; };
C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C702D0307B700BC2AD1 /* affine_mobile_nativeFFI.h */; }; C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C702D0307B700BC2AD1 /* affine_mobile_nativeFFI.h */; };
C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C712D0307B700BC2AD1 /* affine_mobile_nativeFFI.modulemap */; }; C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */ = {isa = PBXBuildFile; fileRef = C4C97C712D0307B700BC2AD1 /* affine_mobile_nativeFFI.modulemap */; };
@@ -39,6 +40,7 @@
50802D5E2D112F7D00694021 /* Intelligents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intelligents; sourceTree = "<group>"; }; 50802D5E2D112F7D00694021 /* Intelligents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Intelligents; sourceTree = "<group>"; };
50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; }; 50A285D52D112A5E000D5A6D /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
50A285D62D112A5E000D5A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; }; 50A285D62D112A5E000D5A6D /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSValueContainerExt.swift; sourceTree = "<group>"; };
9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = "<group>"; }; 9D6A85322CCF6DA700DAB35F /* HashcashPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashcashPlugin.swift; sourceTree = "<group>"; };
9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; }; 9D90BE172CCB9876006677DB /* CookieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookieManager.swift; sourceTree = "<group>"; };
9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = "<group>"; }; 9D90BE182CCB9876006677DB /* CookiePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CookiePlugin.swift; sourceTree = "<group>"; };
@@ -50,10 +52,10 @@
9D90BE202CCB9876006677DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 9D90BE232CCB9876006677DB /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; 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; }; 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>"; }; 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>"; };
C4C97C6B2D03027900BC2AD1 /* libaffine_mobile_native.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libaffine_mobile_native.a; path = "../../../../../target/aarch64-apple-ios-sim/release/libaffine_mobile_native.a"; sourceTree = "<group>"; }; C4C97C6B2D03027900BC2AD1 /* libaffine_mobile_native.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libaffine_mobile_native.a; path = "../../../../../target/aarch64-apple-ios-sim/debug/libaffine_mobile_native.a"; sourceTree = "<group>"; };
C4C97C6D2D0304D100BC2AD1 /* libaffine_mobile_native.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libaffine_mobile_native.a; sourceTree = "<group>"; };
C4C97C6F2D0307B700BC2AD1 /* affine_mobile_native.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = affine_mobile_native.swift; sourceTree = "<group>"; }; C4C97C6F2D0307B700BC2AD1 /* affine_mobile_native.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = affine_mobile_native.swift; sourceTree = "<group>"; };
C4C97C702D0307B700BC2AD1 /* affine_mobile_nativeFFI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = affine_mobile_nativeFFI.h; sourceTree = "<group>"; }; C4C97C702D0307B700BC2AD1 /* affine_mobile_nativeFFI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = affine_mobile_nativeFFI.h; sourceTree = "<group>"; };
C4C97C712D0307B700BC2AD1 /* affine_mobile_nativeFFI.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = affine_mobile_nativeFFI.modulemap; sourceTree = "<group>"; }; C4C97C712D0307B700BC2AD1 /* affine_mobile_nativeFFI.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = affine_mobile_nativeFFI.modulemap; sourceTree = "<group>"; };
@@ -70,8 +72,8 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
C4C97C6E2D0304D100BC2AD1 /* libaffine_mobile_native.a in Frameworks */,
50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */, 50A285DC2D112B24000D5A6D /* Intelligents in Frameworks */,
9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */,
50802D612D112F8700694021 /* Intelligents in Frameworks */, 50802D612D112F8700694021 /* Intelligents in Frameworks */,
C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */, C4C413792CBE705D00337889 /* Pods_App.framework in Frameworks */,
); );
@@ -84,8 +86,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C4C97C6B2D03027900BC2AD1 /* libaffine_mobile_native.a */, C4C97C6B2D03027900BC2AD1 /* libaffine_mobile_native.a */,
C4C97C6D2D0304D100BC2AD1 /* libaffine_mobile_native.a */,
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */, AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -154,6 +156,7 @@
9D90BE242CCB9876006677DB /* App */ = { 9D90BE242CCB9876006677DB /* App */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */,
9D90BE1A2CCB9876006677DB /* Plugins */, 9D90BE1A2CCB9876006677DB /* Plugins */,
9D90BE1C2CCB9876006677DB /* AppDelegate.swift */, 9D90BE1C2CCB9876006677DB /* AppDelegate.swift */,
507513692D1924C600AD60C0 /* RootViewController.swift */, 507513692D1924C600AD60C0 /* RootViewController.swift */,
@@ -325,6 +328,7 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
9D52FC432D26CDBF00105D0A /* JSValueContainerExt.swift in Sources */,
5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */, 5075136E2D1925BC00AD60C0 /* IntelligentsPlugin.swift in Sources */,
5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */, 5075136A2D1924C600AD60C0 /* RootViewController.swift in Sources */,
C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */, C4C97C7C2D030BE000BC2AD1 /* affine_mobile_native.swift in Sources */,
@@ -0,0 +1,55 @@
//
// JSValueContainerExt.swift
// App
//
// Created by EYHN on 2025/1/2.
//
import Capacitor
enum RequestParamError: Error {
case request(key: String)
}
extension JSValueContainer {
public func getStringEnsure(_ key: String) throws -> String {
guard let str = self.getString(key) else {
throw RequestParamError.request(key: key)
}
return str
}
public func getIntEnsure(_ key: String) throws -> Int {
guard let int = self.getInt(key) else {
throw RequestParamError.request(key: key)
}
return int
}
public func getDoubleEnsure(_ key: String) throws -> Double {
guard let doub = self.getDouble(key) else {
throw RequestParamError.request(key: key)
}
return doub
}
public func getBoolEnsure(_ key: String) throws -> Bool {
guard let bool = self.getBool(key) else {
throw RequestParamError.request(key: key)
}
return bool
}
public func getArrayEnsure(_ key: String) throws -> JSArray {
guard let arr = self.getArray(key) else {
throw RequestParamError.request(key: key)
}
return arr
}
public func getArrayEnsure<T>(_ key: String, _ ofType: T.Type) throws -> [T] {
guard let arr = self.getArray(key, ofType) else {
throw RequestParamError.request(key: key)
}
return arr
}
}
@@ -3,400 +3,475 @@ import Foundation
@objc(NbStorePlugin) @objc(NbStorePlugin)
public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin { public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
private let docStoragePool: DocStoragePool = .init(noPointer: DocStoragePool.NoPointer()) private let docStoragePool: DocStoragePool = newDocStoragePool()
public let identifier = "NbStorePlugin" public let identifier = "NbStorePlugin"
public let jsName = "NbStoreDocStorage" public let jsName = "NbStoreDocStorage"
public let pluginMethods: [CAPPluginMethod] = [ public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "getSpaceDBPath", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "disconnect", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "close", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "setSpaceId", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "isClosed", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "pushUpdate", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "checkpoint", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getDocSnapshot", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "validate", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "setDocSnapshot", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setSpaceId", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getDocUpdates", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "pushUpdate", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "markUpdatesMerged", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getDocSnapshot", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "deleteDoc", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setDocSnapshot", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getDocClocks", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getDocUpdates", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getDocClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "markUpdatesMerged", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getBlob", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "deleteDoc", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "setBlob", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getDocClocks", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "deleteBlob", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getDocClock", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "releaseBlobs", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getBlob", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "listBlobs", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setBlob", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getPeerRemoteClocks", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "deleteBlob", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getPeerRemoteClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "releaseBlobs", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "setPeerRemoteClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "listBlobs", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getPeerPulledRemoteClocks", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getPeerRemoteClocks", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getPeerPulledRemoteClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getPeerRemoteClock", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "setPeerPulledRemoteClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setPeerRemoteClock", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getPeerPushedClocks", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getPeerPulledRemoteClocks", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "setPeerPushedClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getPeerPulledRemoteClock", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "clearClocks", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setPeerPulledRemoteClock", returnType: CAPPluginReturnPromise), ]
CAPPluginMethod(name: "getPeerPushedClocks", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setPeerPushedClock", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "clearClocks", returnType: CAPPluginReturnPromise),
]
@objc func getSpaceDBPath(_ call: CAPPluginCall) { @objc func connect(_ call: CAPPluginCall) {
let peer = call.getString("peer") ?? "" Task {
let spaceType = call.getString("spaceType") ?? "" do {
let id = call.getString("id") ?? "" let id = try call.getStringEnsure("id")
let spaceId = try call.getStringEnsure("spaceId")
do { let spaceType = try call.getStringEnsure("spaceType")
let path = try getDbPath(peer: peer, spaceType: spaceType, id: id) let peer = try call.getStringEnsure("peer")
call.resolve(["path": path]) guard let documentDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
} catch { call.reject("Failed to get document file urls")
call.reject("Failed to get space DB path", nil, error) return
} }
let peerDir = documentDir.appending(path: "workspaces")
.appending(path: spaceType)
.appending(path:
peer
.replacing(#/[\/!@#$%^&*()+~`"':;,?<>|]/#, with: "_")
.replacing(/_+/, with: "_")
.replacing(/_+$/, with: ""))
try FileManager.default.createDirectory(atPath: peerDir.path(), withIntermediateDirectories: true)
let db = peerDir.appending(path: spaceId + ".db")
try await docStoragePool.connect(universalId: id, path: db.path())
call.resolve()
} catch {
call.reject("Failed to connect storage, \(error)", nil, error)
}
} }
}
@objc func connect(_ call: CAPPluginCall) async { @objc func disconnect(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
try? await docStoragePool.connect(universalId: id) do {
let id = try call.getStringEnsure("id")
try await docStoragePool.disconnect(universalId: id)
call.resolve()
} catch {
call.reject("Failed to disconnect, \(error)", nil, error)
}
} }
}
@objc func close(_ call: CAPPluginCall) async { @objc func setSpaceId(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
try? await docStoragePool.close(universalId: id) do {
let id = try call.getStringEnsure("id")
let spaceId = try call.getStringEnsure("spaceId")
try await docStoragePool.setSpaceId(universalId: id, spaceId: spaceId)
call.resolve()
} catch {
call.reject("Failed to set space id, \(error)", nil, error)
}
} }
}
@objc func isClosed(_ call: CAPPluginCall) { @objc func pushUpdate(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
call.resolve(["isClosed": docStoragePool.isClosed(universalId: id)]) do {
let id = try call.getStringEnsure("id")
let docId = try call.getStringEnsure("docId")
let data = try call.getStringEnsure("data")
let timestamp = try await docStoragePool.pushUpdate(universalId: id, docId: docId, update: data)
call.resolve(["timestamp": timestamp])
} catch {
call.reject("Failed to push update, \(error)", nil, error)
}
} }
}
@objc func checkpoint(_ call: CAPPluginCall) async { @objc func getDocSnapshot(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
try? await docStoragePool.checkpoint(universalId: id) do {
} let id = try call.getStringEnsure("id")
let docId = try call.getStringEnsure("docId")
@objc func validate(_ call: CAPPluginCall) async { if let record = try await docStoragePool.getDocSnapshot(universalId: id, docId: docId) {
let id = call.getString("id") ?? "" call.resolve([
let validate = (try? await docStoragePool.validate(universalId: id)) ?? false "docId": record.docId,
call.resolve(["isValidate": validate]) "bin": record.bin,
} "timestamp": record.timestamp,
])
@objc func setSpaceId(_ call: CAPPluginCall) async { } else {
let id = call.getString("id") ?? "" call.resolve()
let spaceId = call.getString("spaceId") ?? ""
do {
try await docStoragePool.setSpaceId(universalId: id, spaceId: spaceId)
call.resolve()
} catch {
call.reject("Failed to set space id", nil, error)
} }
} catch {
call.reject("Failed to get doc snapshot, \(error)", nil, error)
}
} }
}
@objc func pushUpdate(_ call: CAPPluginCall) async { @objc func setDocSnapshot(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let docId = call.getString("docId") ?? "" do {
let data = call.getString("data") ?? "" let id = try call.getStringEnsure("id")
do { let docId = try call.getStringEnsure("docId")
let timestamp = try await docStoragePool.pushUpdate(universalId: id, docId: docId, update: data) let bin = try call.getStringEnsure("bin")
call.resolve(["timestamp": timestamp.timeIntervalSince1970]) let timestamp = try call.getIntEnsure("timestamp")
let success = try await docStoragePool.setDocSnapshot(
} catch { universalId: id,
call.reject("Failed to push update", nil, error) snapshot: DocRecord(docId: docId, bin: bin, timestamp: Int64(timestamp))
} )
call.resolve(["success": success])
} catch {
call.reject("Failed to set doc snapshot, \(error)", nil, error)
}
} }
}
@objc func getDocSnapshot(_ call: CAPPluginCall) async { @objc func getDocUpdates(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let docId = call.getString("docId") ?? "" do {
do { let id = try call.getStringEnsure("id")
if let record = try await docStoragePool.getDocSnapshot(universalId: id, docId: docId) { let docId = try call.getStringEnsure("docId")
call.resolve([ let updates = try await docStoragePool.getDocUpdates(universalId: id, docId: docId)
"docId": record.docId, let mapped = updates.map { [
"data": record.data, "docId": $0.docId,
"timestamp": record.timestamp.timeIntervalSince1970, "timestamp": $0.timestamp,
]) "bin": $0.bin,
} else { ] }
call.resolve() call.resolve(["updates": mapped])
} } catch {
} catch { call.reject("Failed to get doc updates, \(error)", nil, error)
call.reject("Failed to get doc snapshot", nil, error) }
}
} }
}
@objc func setDocSnapshot(_ call: CAPPluginCall) async { @objc func markUpdatesMerged(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let docId = call.getString("docId") ?? "" do {
let data = call.getString("data") ?? "" let id = try call.getStringEnsure("id")
let timestamp = Date() let docId = try call.getStringEnsure("docId")
do { let times = try call.getArrayEnsure("timestamps", Int64.self)
let success = try await docStoragePool.setDocSnapshot(
universalId: id, let count = try await docStoragePool.markUpdatesMerged(universalId: id, docId: docId, updates: times)
snapshot: DocRecord(docId: docId, data: data, timestamp: timestamp) call.resolve(["count": count])
) } catch {
call.resolve(["success": success]) call.reject("Failed to mark updates merged, \(error)", nil, error)
} catch { }
call.reject("Failed to set doc snapshot", nil, error)
}
} }
}
@objc func getDocUpdates(_ call: CAPPluginCall) async { @objc func deleteDoc(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let docId = call.getString("docId") ?? "" do {
do { let id = try call.getStringEnsure("id")
let updates = try await docStoragePool.getDocUpdates(universalId: id, docId: docId) let docId = try call.getStringEnsure("docId")
let mapped = updates.map { [
"docId": $0.docId, try await docStoragePool.deleteDoc(universalId: id, docId: docId)
"createdAt": $0.createdAt.timeIntervalSince1970, call.resolve()
"data": $0.data, } catch {
] } call.reject("Failed to delete doc, \(error)", nil, error)
call.resolve(["updates": mapped]) }
} catch {
call.reject("Failed to get doc updates", nil, error)
}
} }
}
@objc func markUpdatesMerged(_ call: CAPPluginCall) async { @objc func getDocClocks(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let docId = call.getString("docId") ?? "" do {
let times = call.getArray("timestamps", Double.self) ?? [] let id = try call.getStringEnsure("id")
let dateArray = times.map { Date(timeIntervalSince1970: $0) }
do {
let count = try await docStoragePool.markUpdatesMerged(universalId: id, docId: docId, updates: dateArray)
call.resolve(["count": count])
} catch {
call.reject("Failed to mark updates merged", nil, error)
}
}
@objc func deleteDoc(_ call: CAPPluginCall) async {
let id = call.getString("id") ?? ""
let docId = call.getString("docId") ?? ""
do {
try await docStoragePool.deleteDoc(universalId: id, docId: docId)
call.resolve()
} catch {
call.reject("Failed to delete doc", nil, error)
}
}
@objc func getDocClocks(_ call: CAPPluginCall) async {
let id = call.getString("id") ?? ""
let after = call.getInt("after") let after = call.getInt("after")
do {
let docClocks = try await docStoragePool.getDocClocks(
universalId: id,
after: after != nil ? Date(timeIntervalSince1970: TimeInterval(after!)) : nil
)
let mapped = docClocks.map { [
"docId": $0.docId,
"timestamp": $0.timestamp.timeIntervalSince1970,
] }
call.resolve(["clocks": mapped])
} catch {
call.reject("Failed to get doc clocks", nil, error)
}
}
@objc func getDocClock(_ call: CAPPluginCall) async { let docClocks = try await docStoragePool.getDocClocks(
let id = call.getString("id") ?? "" universalId: id,
let docId = call.getString("docId") ?? "" after: after != nil ? Int64(after!) : nil
do { )
if let docClock = try await docStoragePool.getDocClock(universalId: id, docId: docId) { let mapped = docClocks.map { [
call.resolve([ "docId": $0.docId,
"docId": docClock.docId, "timestamp": $0.timestamp,
"timestamp": docClock.timestamp.timeIntervalSince1970, ] }
]) call.resolve(["clocks": mapped])
} else { } catch {
call.resolve() call.reject("Failed to get doc clocks, \(error)", nil, error)
} }
} catch {
call.reject("Failed to get doc clock for docId: \(docId)", nil, error)
}
} }
}
@objc func getBlob(_ call: CAPPluginCall) async { @objc func getDocClock(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let key = call.getString("key") ?? "" do {
if let blob = try? await docStoragePool.getBlob(universalId: id, key: key) { let id = try call.getStringEnsure("id")
call.resolve(["blob": blob]) let docId = try call.getStringEnsure("docId")
if let docClock = try await docStoragePool.getDocClock(universalId: id, docId: docId) {
call.resolve([
"docId": docClock.docId,
"timestamp": docClock.timestamp,
])
} else { } else {
call.resolve() call.resolve()
} }
} catch {
call.reject("Failed to get doc clock, \(error)", nil, error)
}
} }
}
@objc func setBlob(_ call: CAPPluginCall) async { @objc func getBlob(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let key = call.getString("key") ?? "" do {
let data = call.getString("data") ?? "" let id = try call.getStringEnsure("id")
let mime = call.getString("mime") ?? "" let key = try call.getStringEnsure("key")
try? await docStoragePool.setBlob(universalId: id, blob: SetBlob(key: key, data: data, mime: mime)) if let blob = try await docStoragePool.getBlob(universalId: id, key: key) {
call.resolve(["blob":[
"key": blob.key,
"data": blob.data,
"mime": blob.mime,
"size": blob.size,
"createdAt": blob.createdAt
]])
} else {
call.resolve()
}
} catch {
call.reject("Failed to get blob, \(error)", nil, error)
}
} }
}
@objc func deleteBlob(_ call: CAPPluginCall) async { @objc func setBlob(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let key = call.getString("key") ?? "" do {
let id = try call.getStringEnsure("id")
let key = try call.getStringEnsure("key")
let data = try call.getStringEnsure("data")
let mime = try call.getStringEnsure("mime")
try await docStoragePool.setBlob(universalId: id, blob: SetBlob(key: key, data: data, mime: mime))
call.resolve()
} catch {
call.reject("Failed to set blob, \(error)", nil, error)
}
}
}
@objc func deleteBlob(_ call: CAPPluginCall) {
Task {
do {
let id = try call.getStringEnsure("id")
let key = try call.getStringEnsure("key")
let permanently = call.getBool("permanently") ?? false let permanently = call.getBool("permanently") ?? false
try? await docStoragePool.deleteBlob(universalId: id, key: key, permanently: permanently) try await docStoragePool.deleteBlob(universalId: id, key: key, permanently: permanently)
call.resolve()
} catch {
call.reject("Failed to delete blob, \(error)", nil, error)
}
} }
}
@objc func releaseBlobs(_ call: CAPPluginCall) async { @objc func releaseBlobs(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
try? await docStoragePool.releaseBlobs(universalId: id) do {
let id = try call.getStringEnsure("id")
try await docStoragePool.releaseBlobs(universalId: id)
call.resolve()
} catch {
call.reject("Failed to release blobs, \(error)", nil, error)
}
} }
}
@objc func listBlobs(_ call: CAPPluginCall) async { @objc func listBlobs(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
if let blobs = try? await docStoragePool.listBlobs(universalId: id) { do {
let mapped = blobs.map { [ let id = try call.getStringEnsure("id")
"key": $0.key, let blobs = try await docStoragePool.listBlobs(universalId: id)
"size": $0.size, let mapped = blobs.map { [
"mime": $0.mime, "key": $0.key,
"createdAt": $0.createdAt.timeIntervalSince1970, "size": $0.size,
] } "mime": $0.mime,
call.resolve(["blobs": mapped]) "createdAt": $0.createdAt,
} else { ] }
call.resolve() call.resolve(["blobs": mapped])
} } catch {
call.reject("Failed to list blobs, \(error)", nil, error)
}
} }
}
@objc func getPeerRemoteClocks(_ call: CAPPluginCall) async { @objc func getPeerRemoteClocks(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let peer = call.getString("peer") ?? "" do {
do { let id = try call.getStringEnsure("id")
let clocks = try await docStoragePool.getPeerRemoteClocks(universalId: id, peer: peer) let peer = try call.getStringEnsure("peer")
let mapped = clocks.map { [
"docId": $0.docId,
"timestamp": $0.timestamp.timeIntervalSince1970,
] }
call.resolve(["clocks": mapped])
} catch { let clocks = try await docStoragePool.getPeerRemoteClocks(universalId: id, peer: peer)
call.reject("Failed to get peer remote clocks", nil, error) let mapped = clocks.map { [
} "docId": $0.docId,
"timestamp": $0.timestamp,
] }
call.resolve(["clocks": mapped])
} catch {
call.reject("Failed to get peer remote clocks, \(error)", nil, error)
}
} }
}
@objc func getPeerRemoteClock(_ call: CAPPluginCall) async { @objc func getPeerRemoteClock(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let peer = call.getString("peer") ?? "" do {
let docId = call.getString("docId") ?? "" let id = try call.getStringEnsure("id")
do { let peer = try call.getStringEnsure("peer")
let clock = try await docStoragePool.getPeerRemoteClock(universalId: id, peer: peer, docId: docId) let docId = try call.getStringEnsure("docId")
call.resolve([
"docId": clock.docId,
"timestamp": clock.timestamp.timeIntervalSince1970,
])
} catch { let clock = try await docStoragePool.getPeerRemoteClock(universalId: id, peer: peer, docId: docId)
call.reject("Failed to get peer remote clock", nil, error) call.resolve([
} "docId": clock.docId,
"timestamp": clock.timestamp,
])
} catch {
call.reject("Failed to get peer remote clock, \(error)", nil, error)
}
} }
}
@objc func setPeerRemoteClock(_ call: CAPPluginCall) async { @objc func setPeerRemoteClock(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let peer = call.getString("peer") ?? "" do {
let docId = call.getString("docId") ?? "" let id = try call.getStringEnsure("id")
let timestamp = call.getDouble("timestamp") ?? 0 let peer = try call.getStringEnsure("peer")
do { let docId = try call.getStringEnsure("docId")
try await docStoragePool.setPeerRemoteClock( let timestamp = try call.getIntEnsure("timestamp")
universalId: id, try await docStoragePool.setPeerRemoteClock(
peer: peer, universalId: id,
docId: docId, peer: peer,
clock: Date(timeIntervalSince1970: timestamp) docId: docId,
) clock: Int64(timestamp)
call.resolve() )
} catch { call.resolve()
call.reject("Failed to set peer remote clock", nil, error) } catch {
} call.reject("Failed to set peer remote clock, \(error)", nil, error)
}
} }
}
@objc func getPeerPulledRemoteClocks(_ call: CAPPluginCall) async { @objc func getPeerPulledRemoteClocks(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let peer = call.getString("peer") ?? "" do {
do { let id = try call.getStringEnsure("id")
let clocks = try await docStoragePool.getPeerPulledRemoteClocks(universalId: id, peer: peer) let peer = try call.getStringEnsure("peer")
let mapped = clocks.map { [
"docId": $0.docId,
"timestamp": $0.timestamp.timeIntervalSince1970,
] }
call.resolve(["clocks": mapped])
} catch { let clocks = try await docStoragePool.getPeerPulledRemoteClocks(universalId: id, peer: peer)
call.reject("Failed to get peer pulled remote clocks", nil, error) let mapped = clocks.map { [
} "docId": $0.docId,
"timestamp": $0.timestamp,
] }
call.resolve(["clocks": mapped])
} catch {
call.reject("Failed to get peer pulled remote clocks, \(error)", nil, error)
}
} }
}
@objc func getPeerPulledRemoteClock(_ call: CAPPluginCall) async { @objc func getPeerPulledRemoteClock(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let peer = call.getString("peer") ?? "" do {
let docId = call.getString("docId") ?? "" let id = try call.getStringEnsure("id")
do { let peer = try call.getStringEnsure("peer")
let clock = try await docStoragePool.getPeerPulledRemoteClock(universalId: id, peer: peer, docId: docId) let docId = try call.getStringEnsure("docId")
call.resolve([
"docId": clock.docId,
"timestamp": clock.timestamp.timeIntervalSince1970,
])
} catch { let clock = try await docStoragePool.getPeerPulledRemoteClock(universalId: id, peer: peer, docId: docId)
call.reject("Failed to get peer pulled remote clock", nil, error) call.resolve([
} "docId": clock.docId,
"timestamp": clock.timestamp,
])
} catch {
call.reject("Failed to get peer pulled remote clock, \(error)", nil, error)
}
} }
}
@objc func setPeerPulledRemoteClock(_ call: CAPPluginCall) async { @objc func setPeerPulledRemoteClock(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let peer = call.getString("peer") ?? "" do {
let docId = call.getString("docId") ?? "" let id = try call.getStringEnsure("id")
let timestamp = call.getDouble("timestamp") ?? 0 let peer = try call.getStringEnsure("peer")
do { let docId = try call.getStringEnsure("docId")
try await docStoragePool.setPeerPulledRemoteClock( let timestamp = try call.getIntEnsure("timestamp")
universalId: id,
peer: peer, try await docStoragePool.setPeerPulledRemoteClock(
docId: docId, universalId: id,
clock: Date(timeIntervalSince1970: timestamp) peer: peer,
) docId: docId,
call.resolve() clock: Int64(timestamp)
} catch { )
call.reject("Failed to set peer pulled remote clock", nil, error) call.resolve()
} } catch {
call.reject("Failed to set peer pulled remote clock, \(error)", nil, error)
}
} }
}
@objc func getPeerPushedClocks(_ call: CAPPluginCall) async { @objc func getPeerPushedClocks(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let peer = call.getString("peer") ?? "" do {
do { let id = try call.getStringEnsure("id")
let clocks = try await docStoragePool.getPeerPushedClocks(universalId: id, peer: peer) let peer = try call.getStringEnsure("peer")
let mapped = clocks.map { [ let clocks = try await docStoragePool.getPeerPushedClocks(universalId: id, peer: peer)
"docId": $0.docId, let mapped = clocks.map { [
"timestamp": $0.timestamp.timeIntervalSince1970, "docId": $0.docId,
] } "timestamp": $0.timestamp,
call.resolve(["clocks": mapped]) ] }
call.resolve(["clocks": mapped])
} catch { } catch {
call.reject("Failed to get peer pushed clocks", nil, error) call.reject("Failed to get peer pushed clocks, \(error)", nil, error)
} }
} }
}
@objc func setPeerPushedClock(_ call: CAPPluginCall) async { @objc func setPeerPushedClock(_ call: CAPPluginCall) {
let id = call.getString("id") ?? "" Task {
let peer = call.getString("peer") ?? "" do {
let docId = call.getString("docId") ?? "" let id = try call.getStringEnsure("id")
let timestamp = call.getDouble("timestamp") ?? 0 let peer = try call.getStringEnsure("peer")
do { let docId = try call.getStringEnsure("docId")
try await docStoragePool.setPeerPushedClock( let timestamp = try call.getIntEnsure("timestamp")
universalId: id,
peer: peer,
docId: docId,
clock: Date(timeIntervalSince1970: timestamp)
)
call.resolve()
} catch {
call.reject("Failed to set peer pushed clock", nil, error)
}
}
@objc func clearClocks(_ call: CAPPluginCall) async { try await docStoragePool.setPeerPushedClock(
let id = call.getString("id") ?? "" universalId: id,
do { peer: peer,
try await docStoragePool.clearClocks(universalId: id) docId: docId,
call.resolve() clock: Int64(timestamp)
} catch { )
call.reject("Failed to clear clocks", nil, error) call.resolve()
} } catch {
call.reject("Failed to set peer pushed clock, \(error)", nil, error)
}
} }
}
@objc func clearClocks(_ call: CAPPluginCall) {
Task {
do {
let id = try call.getStringEnsure("id")
try await docStoragePool.clearClocks(universalId: id)
call.resolve()
} catch {
call.reject("Failed to clear clocks, \(error)", nil, error)
}
}
}
} }
@@ -492,65 +492,25 @@ private struct FfiConverterString: FfiConverter {
} }
} }
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
private struct FfiConverterTimestamp: FfiConverterRustBuffer {
typealias SwiftType = Date
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Date {
let seconds: Int64 = try readInt(&buf)
let nanoseconds: UInt32 = try readInt(&buf)
if seconds >= 0 {
let delta = Double(seconds) + (Double(nanoseconds) / 1.0e9)
return Date(timeIntervalSince1970: delta)
} else {
let delta = Double(seconds) - (Double(nanoseconds) / 1.0e9)
return Date(timeIntervalSince1970: delta)
}
}
public static func write(_ value: Date, into buf: inout [UInt8]) {
var delta = value.timeIntervalSince1970
var sign: Int64 = 1
if delta < 0 {
// The nanoseconds portion of the epoch offset must always be
// positive, to simplify the calculation we will use the absolute
// value of the offset.
sign = -1
delta = -delta
}
if delta.rounded(.down) > Double(Int64.max) {
fatalError("Timestamp overflow, exceeds max bounds supported by Uniffi")
}
let seconds = Int64(delta)
let nanoseconds = UInt32((delta - Double(seconds)) * 1.0e9)
writeInt(&buf, sign * seconds)
writeInt(&buf, nanoseconds)
}
}
public protocol DocStoragePoolProtocol: AnyObject { public protocol DocStoragePoolProtocol: AnyObject {
func checkpoint(universalId: String) async throws
func clearClocks(universalId: String) async throws func clearClocks(universalId: String) async throws
func close(universalId: String) async throws
/** /**
* Initialize the database and run migrations. * Initialize the database and run migrations.
*/ */
func connect(universalId: String) async throws func connect(universalId: String, path: String) async throws
func deleteBlob(universalId: String, key: String, permanently: Bool) async throws func deleteBlob(universalId: String, key: String, permanently: Bool) async throws
func deleteDoc(universalId: String, docId: String) async throws func deleteDoc(universalId: String, docId: String) async throws
func disconnect(universalId: String) async throws
func getBlob(universalId: String, key: String) async throws -> Blob? func getBlob(universalId: String, key: String) async throws -> Blob?
func getDocClock(universalId: String, docId: String) async throws -> DocClock? func getDocClock(universalId: String, docId: String) async throws -> DocClock?
func getDocClocks(universalId: String, after: Date?) async throws -> [DocClock] func getDocClocks(universalId: String, after: Int64?) async throws -> [DocClock]
func getDocSnapshot(universalId: String, docId: String) async throws -> DocRecord? func getDocSnapshot(universalId: String, docId: String) async throws -> DocRecord?
@@ -566,13 +526,11 @@ public protocol DocStoragePoolProtocol: AnyObject {
func getPeerRemoteClocks(universalId: String, peer: String) async throws -> [DocClock] func getPeerRemoteClocks(universalId: String, peer: String) async throws -> [DocClock]
func isClosed(universalId: String) -> Bool
func listBlobs(universalId: String) async throws -> [ListedBlob] func listBlobs(universalId: String) async throws -> [ListedBlob]
func markUpdatesMerged(universalId: String, docId: String, updates: [Date]) async throws -> UInt32 func markUpdatesMerged(universalId: String, docId: String, updates: [Int64]) async throws -> UInt32
func pushUpdate(universalId: String, docId: String, update: String) async throws -> Date func pushUpdate(universalId: String, docId: String, update: String) async throws -> Int64
func releaseBlobs(universalId: String) async throws func releaseBlobs(universalId: String) async throws
@@ -580,15 +538,13 @@ public protocol DocStoragePoolProtocol: AnyObject {
func setDocSnapshot(universalId: String, snapshot: DocRecord) async throws -> Bool func setDocSnapshot(universalId: String, snapshot: DocRecord) async throws -> Bool
func setPeerPulledRemoteClock(universalId: String, peer: String, docId: String, clock: Date) async throws func setPeerPulledRemoteClock(universalId: String, peer: String, docId: String, clock: Int64) async throws
func setPeerPushedClock(universalId: String, peer: String, docId: String, clock: Date) async throws func setPeerPushedClock(universalId: String, peer: String, docId: String, clock: Int64) async throws
func setPeerRemoteClock(universalId: String, peer: String, docId: String, clock: Date) async throws func setPeerRemoteClock(universalId: String, peer: String, docId: String, clock: Int64) async throws
func setSpaceId(universalId: String, spaceId: String) async throws func setSpaceId(universalId: String, spaceId: String) async throws
func validate(universalId: String) async throws -> Bool
} }
open class DocStoragePool: open class DocStoragePool:
@@ -640,23 +596,6 @@ open class DocStoragePool:
try! rustCall { uniffi_affine_mobile_native_fn_free_docstoragepool(pointer, $0) } try! rustCall { uniffi_affine_mobile_native_fn_free_docstoragepool(pointer, $0) }
} }
open func checkpoint(universalId: String) async throws {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_checkpoint(
self.uniffiClonePointer(),
FfiConverterString.lower(universalId)
)
},
pollFunc: ffi_affine_mobile_native_rust_future_poll_void,
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
)
}
open func clearClocks(universalId: String) async throws { open func clearClocks(universalId: String) async throws {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
@@ -674,33 +613,16 @@ open class DocStoragePool:
) )
} }
open func close(universalId: String) async throws {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_close(
self.uniffiClonePointer(),
FfiConverterString.lower(universalId)
)
},
pollFunc: ffi_affine_mobile_native_rust_future_poll_void,
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
)
}
/** /**
* Initialize the database and run migrations. * Initialize the database and run migrations.
*/ */
open func connect(universalId: String) async throws { open func connect(universalId: String, path: String) async throws {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_connect( uniffi_affine_mobile_native_fn_method_docstoragepool_connect(
self.uniffiClonePointer(), self.uniffiClonePointer(),
FfiConverterString.lower(universalId) FfiConverterString.lower(universalId), FfiConverterString.lower(path)
) )
}, },
pollFunc: ffi_affine_mobile_native_rust_future_poll_void, pollFunc: ffi_affine_mobile_native_rust_future_poll_void,
@@ -745,6 +667,23 @@ open class DocStoragePool:
) )
} }
open func disconnect(universalId: String) async throws {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_disconnect(
self.uniffiClonePointer(),
FfiConverterString.lower(universalId)
)
},
pollFunc: ffi_affine_mobile_native_rust_future_poll_void,
completeFunc: ffi_affine_mobile_native_rust_future_complete_void,
freeFunc: ffi_affine_mobile_native_rust_future_free_void,
liftFunc: { $0 },
errorHandler: FfiConverterTypeUniffiError.lift
)
}
open func getBlob(universalId: String, key: String) async throws -> Blob? { open func getBlob(universalId: String, key: String) async throws -> Blob? {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
@@ -779,13 +718,13 @@ open class DocStoragePool:
) )
} }
open func getDocClocks(universalId: String, after: Date?) async throws -> [DocClock] { open func getDocClocks(universalId: String, after: Int64?) async throws -> [DocClock] {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_clocks( uniffi_affine_mobile_native_fn_method_docstoragepool_get_doc_clocks(
self.uniffiClonePointer(), self.uniffiClonePointer(),
FfiConverterString.lower(universalId), FfiConverterOptionTimestamp.lower(after) FfiConverterString.lower(universalId), FfiConverterOptionInt64.lower(after)
) )
}, },
pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer,
@@ -915,13 +854,6 @@ open class DocStoragePool:
) )
} }
open func isClosed(universalId: String) -> Bool {
return try! FfiConverterBool.lift(try! rustCall {
uniffi_affine_mobile_native_fn_method_docstoragepool_is_closed(self.uniffiClonePointer(),
FfiConverterString.lower(universalId), $0)
})
}
open func listBlobs(universalId: String) async throws -> [ListedBlob] { open func listBlobs(universalId: String) async throws -> [ListedBlob] {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
@@ -939,13 +871,13 @@ open class DocStoragePool:
) )
} }
open func markUpdatesMerged(universalId: String, docId: String, updates: [Date]) async throws -> UInt32 { open func markUpdatesMerged(universalId: String, docId: String, updates: [Int64]) async throws -> UInt32 {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_mark_updates_merged( uniffi_affine_mobile_native_fn_method_docstoragepool_mark_updates_merged(
self.uniffiClonePointer(), self.uniffiClonePointer(),
FfiConverterString.lower(universalId), FfiConverterString.lower(docId), FfiConverterSequenceTimestamp.lower(updates) FfiConverterString.lower(universalId), FfiConverterString.lower(docId), FfiConverterSequenceInt64.lower(updates)
) )
}, },
pollFunc: ffi_affine_mobile_native_rust_future_poll_u32, pollFunc: ffi_affine_mobile_native_rust_future_poll_u32,
@@ -956,7 +888,7 @@ open class DocStoragePool:
) )
} }
open func pushUpdate(universalId: String, docId: String, update: String) async throws -> Date { open func pushUpdate(universalId: String, docId: String, update: String) async throws -> Int64 {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
@@ -965,10 +897,10 @@ open class DocStoragePool:
FfiConverterString.lower(universalId), FfiConverterString.lower(docId), FfiConverterString.lower(update) FfiConverterString.lower(universalId), FfiConverterString.lower(docId), FfiConverterString.lower(update)
) )
}, },
pollFunc: ffi_affine_mobile_native_rust_future_poll_rust_buffer, pollFunc: ffi_affine_mobile_native_rust_future_poll_i64,
completeFunc: ffi_affine_mobile_native_rust_future_complete_rust_buffer, completeFunc: ffi_affine_mobile_native_rust_future_complete_i64,
freeFunc: ffi_affine_mobile_native_rust_future_free_rust_buffer, freeFunc: ffi_affine_mobile_native_rust_future_free_i64,
liftFunc: FfiConverterTimestamp.lift, liftFunc: FfiConverterInt64.lift,
errorHandler: FfiConverterTypeUniffiError.lift errorHandler: FfiConverterTypeUniffiError.lift
) )
} }
@@ -1024,13 +956,13 @@ open class DocStoragePool:
) )
} }
open func setPeerPulledRemoteClock(universalId: String, peer: String, docId: String, clock: Date) async throws { open func setPeerPulledRemoteClock(universalId: String, peer: String, docId: String, clock: Int64) async throws {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pulled_remote_clock( uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pulled_remote_clock(
self.uniffiClonePointer(), self.uniffiClonePointer(),
FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId), FfiConverterTimestamp.lower(clock) FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId), FfiConverterInt64.lower(clock)
) )
}, },
pollFunc: ffi_affine_mobile_native_rust_future_poll_void, pollFunc: ffi_affine_mobile_native_rust_future_poll_void,
@@ -1041,13 +973,13 @@ open class DocStoragePool:
) )
} }
open func setPeerPushedClock(universalId: String, peer: String, docId: String, clock: Date) async throws { open func setPeerPushedClock(universalId: String, peer: String, docId: String, clock: Int64) async throws {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pushed_clock( uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pushed_clock(
self.uniffiClonePointer(), self.uniffiClonePointer(),
FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId), FfiConverterTimestamp.lower(clock) FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId), FfiConverterInt64.lower(clock)
) )
}, },
pollFunc: ffi_affine_mobile_native_rust_future_poll_void, pollFunc: ffi_affine_mobile_native_rust_future_poll_void,
@@ -1058,13 +990,13 @@ open class DocStoragePool:
) )
} }
open func setPeerRemoteClock(universalId: String, peer: String, docId: String, clock: Date) async throws { open func setPeerRemoteClock(universalId: String, peer: String, docId: String, clock: Int64) async throws {
return return
try await uniffiRustCallAsync( try await uniffiRustCallAsync(
rustFutureFunc: { rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_remote_clock( uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_remote_clock(
self.uniffiClonePointer(), self.uniffiClonePointer(),
FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId), FfiConverterTimestamp.lower(clock) FfiConverterString.lower(universalId), FfiConverterString.lower(peer), FfiConverterString.lower(docId), FfiConverterInt64.lower(clock)
) )
}, },
pollFunc: ffi_affine_mobile_native_rust_future_poll_void, pollFunc: ffi_affine_mobile_native_rust_future_poll_void,
@@ -1091,23 +1023,6 @@ open class DocStoragePool:
errorHandler: FfiConverterTypeUniffiError.lift errorHandler: FfiConverterTypeUniffiError.lift
) )
} }
open func validate(universalId: String) async throws -> Bool {
return
try await uniffiRustCallAsync(
rustFutureFunc: {
uniffi_affine_mobile_native_fn_method_docstoragepool_validate(
self.uniffiClonePointer(),
FfiConverterString.lower(universalId)
)
},
pollFunc: ffi_affine_mobile_native_rust_future_poll_i8,
completeFunc: ffi_affine_mobile_native_rust_future_complete_i8,
freeFunc: ffi_affine_mobile_native_rust_future_free_i8,
liftFunc: FfiConverterBool.lift,
errorHandler: FfiConverterTypeUniffiError.lift
)
}
} }
#if swift(>=5.8) #if swift(>=5.8)
@@ -1162,11 +1077,11 @@ public struct Blob {
public var data: String public var data: String
public var mime: String public var mime: String
public var size: Int64 public var size: Int64
public var createdAt: Date public var createdAt: Int64
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(key: String, data: String, mime: String, size: Int64, createdAt: Date) { public init(key: String, data: String, mime: String, size: Int64, createdAt: Int64) {
self.key = key self.key = key
self.data = data self.data = data
self.mime = mime self.mime = mime
@@ -1215,7 +1130,7 @@ public struct FfiConverterTypeBlob: FfiConverterRustBuffer {
data: FfiConverterString.read(from: &buf), data: FfiConverterString.read(from: &buf),
mime: FfiConverterString.read(from: &buf), mime: FfiConverterString.read(from: &buf),
size: FfiConverterInt64.read(from: &buf), size: FfiConverterInt64.read(from: &buf),
createdAt: FfiConverterTimestamp.read(from: &buf) createdAt: FfiConverterInt64.read(from: &buf)
) )
} }
@@ -1224,7 +1139,7 @@ public struct FfiConverterTypeBlob: FfiConverterRustBuffer {
FfiConverterString.write(value.data, into: &buf) FfiConverterString.write(value.data, into: &buf)
FfiConverterString.write(value.mime, into: &buf) FfiConverterString.write(value.mime, into: &buf)
FfiConverterInt64.write(value.size, into: &buf) FfiConverterInt64.write(value.size, into: &buf)
FfiConverterTimestamp.write(value.createdAt, into: &buf) FfiConverterInt64.write(value.createdAt, into: &buf)
} }
} }
@@ -1244,11 +1159,11 @@ public func FfiConverterTypeBlob_lower(_ value: Blob) -> RustBuffer {
public struct DocClock { public struct DocClock {
public var docId: String public var docId: String
public var timestamp: Date public var timestamp: Int64
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(docId: String, timestamp: Date) { public init(docId: String, timestamp: Int64) {
self.docId = docId self.docId = docId
self.timestamp = timestamp self.timestamp = timestamp
} }
@@ -1279,13 +1194,13 @@ public struct FfiConverterTypeDocClock: FfiConverterRustBuffer {
return return
try DocClock( try DocClock(
docId: FfiConverterString.read(from: &buf), docId: FfiConverterString.read(from: &buf),
timestamp: FfiConverterTimestamp.read(from: &buf) timestamp: FfiConverterInt64.read(from: &buf)
) )
} }
public static func write(_ value: DocClock, into buf: inout [UInt8]) { public static func write(_ value: DocClock, into buf: inout [UInt8]) {
FfiConverterString.write(value.docId, into: &buf) FfiConverterString.write(value.docId, into: &buf)
FfiConverterTimestamp.write(value.timestamp, into: &buf) FfiConverterInt64.write(value.timestamp, into: &buf)
} }
} }
@@ -1305,14 +1220,14 @@ public func FfiConverterTypeDocClock_lower(_ value: DocClock) -> RustBuffer {
public struct DocRecord { public struct DocRecord {
public var docId: String public var docId: String
public var data: String public var bin: String
public var timestamp: Date public var timestamp: Int64
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(docId: String, data: String, timestamp: Date) { public init(docId: String, bin: String, timestamp: Int64) {
self.docId = docId self.docId = docId
self.data = data self.bin = bin
self.timestamp = timestamp self.timestamp = timestamp
} }
} }
@@ -1322,7 +1237,7 @@ extension DocRecord: Equatable, Hashable {
if lhs.docId != rhs.docId { if lhs.docId != rhs.docId {
return false return false
} }
if lhs.data != rhs.data { if lhs.bin != rhs.bin {
return false return false
} }
if lhs.timestamp != rhs.timestamp { if lhs.timestamp != rhs.timestamp {
@@ -1333,7 +1248,7 @@ extension DocRecord: Equatable, Hashable {
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(docId) hasher.combine(docId)
hasher.combine(data) hasher.combine(bin)
hasher.combine(timestamp) hasher.combine(timestamp)
} }
} }
@@ -1346,15 +1261,15 @@ public struct FfiConverterTypeDocRecord: FfiConverterRustBuffer {
return return
try DocRecord( try DocRecord(
docId: FfiConverterString.read(from: &buf), docId: FfiConverterString.read(from: &buf),
data: FfiConverterString.read(from: &buf), bin: FfiConverterString.read(from: &buf),
timestamp: FfiConverterTimestamp.read(from: &buf) timestamp: FfiConverterInt64.read(from: &buf)
) )
} }
public static func write(_ value: DocRecord, into buf: inout [UInt8]) { public static func write(_ value: DocRecord, into buf: inout [UInt8]) {
FfiConverterString.write(value.docId, into: &buf) FfiConverterString.write(value.docId, into: &buf)
FfiConverterString.write(value.data, into: &buf) FfiConverterString.write(value.bin, into: &buf)
FfiConverterTimestamp.write(value.timestamp, into: &buf) FfiConverterInt64.write(value.timestamp, into: &buf)
} }
} }
@@ -1374,15 +1289,15 @@ public func FfiConverterTypeDocRecord_lower(_ value: DocRecord) -> RustBuffer {
public struct DocUpdate { public struct DocUpdate {
public var docId: String public var docId: String
public var createdAt: Date public var timestamp: Int64
public var data: String public var bin: String
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(docId: String, createdAt: Date, data: String) { public init(docId: String, timestamp: Int64, bin: String) {
self.docId = docId self.docId = docId
self.createdAt = createdAt self.timestamp = timestamp
self.data = data self.bin = bin
} }
} }
@@ -1391,10 +1306,10 @@ extension DocUpdate: Equatable, Hashable {
if lhs.docId != rhs.docId { if lhs.docId != rhs.docId {
return false return false
} }
if lhs.createdAt != rhs.createdAt { if lhs.timestamp != rhs.timestamp {
return false return false
} }
if lhs.data != rhs.data { if lhs.bin != rhs.bin {
return false return false
} }
return true return true
@@ -1402,8 +1317,8 @@ extension DocUpdate: Equatable, Hashable {
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(docId) hasher.combine(docId)
hasher.combine(createdAt) hasher.combine(timestamp)
hasher.combine(data) hasher.combine(bin)
} }
} }
@@ -1415,15 +1330,15 @@ public struct FfiConverterTypeDocUpdate: FfiConverterRustBuffer {
return return
try DocUpdate( try DocUpdate(
docId: FfiConverterString.read(from: &buf), docId: FfiConverterString.read(from: &buf),
createdAt: FfiConverterTimestamp.read(from: &buf), timestamp: FfiConverterInt64.read(from: &buf),
data: FfiConverterString.read(from: &buf) bin: FfiConverterString.read(from: &buf)
) )
} }
public static func write(_ value: DocUpdate, into buf: inout [UInt8]) { public static func write(_ value: DocUpdate, into buf: inout [UInt8]) {
FfiConverterString.write(value.docId, into: &buf) FfiConverterString.write(value.docId, into: &buf)
FfiConverterTimestamp.write(value.createdAt, into: &buf) FfiConverterInt64.write(value.timestamp, into: &buf)
FfiConverterString.write(value.data, into: &buf) FfiConverterString.write(value.bin, into: &buf)
} }
} }
@@ -1445,11 +1360,11 @@ public struct ListedBlob {
public var key: String public var key: String
public var size: Int64 public var size: Int64
public var mime: String public var mime: String
public var createdAt: Date public var createdAt: Int64
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(key: String, size: Int64, mime: String, createdAt: Date) { public init(key: String, size: Int64, mime: String, createdAt: Int64) {
self.key = key self.key = key
self.size = size self.size = size
self.mime = mime self.mime = mime
@@ -1492,7 +1407,7 @@ public struct FfiConverterTypeListedBlob: FfiConverterRustBuffer {
key: FfiConverterString.read(from: &buf), key: FfiConverterString.read(from: &buf),
size: FfiConverterInt64.read(from: &buf), size: FfiConverterInt64.read(from: &buf),
mime: FfiConverterString.read(from: &buf), mime: FfiConverterString.read(from: &buf),
createdAt: FfiConverterTimestamp.read(from: &buf) createdAt: FfiConverterInt64.read(from: &buf)
) )
} }
@@ -1500,7 +1415,7 @@ public struct FfiConverterTypeListedBlob: FfiConverterRustBuffer {
FfiConverterString.write(value.key, into: &buf) FfiConverterString.write(value.key, into: &buf)
FfiConverterInt64.write(value.size, into: &buf) FfiConverterInt64.write(value.size, into: &buf)
FfiConverterString.write(value.mime, into: &buf) FfiConverterString.write(value.mime, into: &buf)
FfiConverterTimestamp.write(value.createdAt, into: &buf) FfiConverterInt64.write(value.createdAt, into: &buf)
} }
} }
@@ -1588,21 +1503,11 @@ public func FfiConverterTypeSetBlob_lower(_ value: SetBlob) -> RustBuffer {
} }
public enum UniffiError { public enum UniffiError {
case GetUserDocumentDirectoryFailed case Err(String
case CreateAffineDirFailed(String
)
case EmptyDocStoragePath
case EmptySpaceId
case SqlxError(String
) )
case Base64DecodingError(String case Base64DecodingError(String
) )
case InvalidUniversalId(String case TimestampDecodingError
)
case InvalidSpaceType(String
)
case ConcatSpaceDirFailed(String
)
} }
#if swift(>=5.8) #if swift(>=5.8)
@@ -1614,65 +1519,29 @@ public struct FfiConverterTypeUniffiError: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UniffiError { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> UniffiError {
let variant: Int32 = try readInt(&buf) let variant: Int32 = try readInt(&buf)
switch variant { switch variant {
case 1: return .GetUserDocumentDirectoryFailed case 1: return try .Err(
case 2: return try .CreateAffineDirFailed(
FfiConverterString.read(from: &buf) FfiConverterString.read(from: &buf)
) )
case 3: return .EmptyDocStoragePath case 2: return try .Base64DecodingError(
case 4: return .EmptySpaceId
case 5: return try .SqlxError(
FfiConverterString.read(from: &buf)
)
case 6: return try .Base64DecodingError(
FfiConverterString.read(from: &buf)
)
case 7: return try .InvalidUniversalId(
FfiConverterString.read(from: &buf)
)
case 8: return try .InvalidSpaceType(
FfiConverterString.read(from: &buf)
)
case 9: return try .ConcatSpaceDirFailed(
FfiConverterString.read(from: &buf) FfiConverterString.read(from: &buf)
) )
case 3: return .TimestampDecodingError
default: throw UniffiInternalError.unexpectedEnumCase default: throw UniffiInternalError.unexpectedEnumCase
} }
} }
public static func write(_ value: UniffiError, into buf: inout [UInt8]) { public static func write(_ value: UniffiError, into buf: inout [UInt8]) {
switch value { switch value {
case .GetUserDocumentDirectoryFailed: case let .Err(v1):
writeInt(&buf, Int32(1)) writeInt(&buf, Int32(1))
case let .CreateAffineDirFailed(v1):
writeInt(&buf, Int32(2))
FfiConverterString.write(v1, into: &buf)
case .EmptyDocStoragePath:
writeInt(&buf, Int32(3))
case .EmptySpaceId:
writeInt(&buf, Int32(4))
case let .SqlxError(v1):
writeInt(&buf, Int32(5))
FfiConverterString.write(v1, into: &buf) FfiConverterString.write(v1, into: &buf)
case let .Base64DecodingError(v1): case let .Base64DecodingError(v1):
writeInt(&buf, Int32(6)) writeInt(&buf, Int32(2))
FfiConverterString.write(v1, into: &buf) FfiConverterString.write(v1, into: &buf)
case let .InvalidUniversalId(v1): case .TimestampDecodingError:
writeInt(&buf, Int32(7)) writeInt(&buf, Int32(3))
FfiConverterString.write(v1, into: &buf)
case let .InvalidSpaceType(v1):
writeInt(&buf, Int32(8))
FfiConverterString.write(v1, into: &buf)
case let .ConcatSpaceDirFailed(v1):
writeInt(&buf, Int32(9))
FfiConverterString.write(v1, into: &buf)
} }
} }
} }
@@ -1688,8 +1557,8 @@ extension UniffiError: Foundation.LocalizedError {
#if swift(>=5.8) #if swift(>=5.8)
@_documentation(visibility: private) @_documentation(visibility: private)
#endif #endif
private struct FfiConverterOptionTimestamp: FfiConverterRustBuffer { private struct FfiConverterOptionInt64: FfiConverterRustBuffer {
typealias SwiftType = Date? typealias SwiftType = Int64?
public static func write(_ value: SwiftType, into buf: inout [UInt8]) { public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
guard let value = value else { guard let value = value else {
@@ -1697,13 +1566,13 @@ private struct FfiConverterOptionTimestamp: FfiConverterRustBuffer {
return return
} }
writeInt(&buf, Int8(1)) writeInt(&buf, Int8(1))
FfiConverterTimestamp.write(value, into: &buf) FfiConverterInt64.write(value, into: &buf)
} }
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
switch try readInt(&buf) as Int8 { switch try readInt(&buf) as Int8 {
case 0: return nil case 0: return nil
case 1: return try FfiConverterTimestamp.read(from: &buf) case 1: return try FfiConverterInt64.read(from: &buf)
default: throw UniffiInternalError.unexpectedOptionalTag default: throw UniffiInternalError.unexpectedOptionalTag
} }
} }
@@ -1784,23 +1653,23 @@ private struct FfiConverterOptionTypeDocRecord: FfiConverterRustBuffer {
#if swift(>=5.8) #if swift(>=5.8)
@_documentation(visibility: private) @_documentation(visibility: private)
#endif #endif
private struct FfiConverterSequenceTimestamp: FfiConverterRustBuffer { private struct FfiConverterSequenceInt64: FfiConverterRustBuffer {
typealias SwiftType = [Date] typealias SwiftType = [Int64]
public static func write(_ value: [Date], into buf: inout [UInt8]) { public static func write(_ value: [Int64], into buf: inout [UInt8]) {
let len = Int32(value.count) let len = Int32(value.count)
writeInt(&buf, len) writeInt(&buf, len)
for item in value { for item in value {
FfiConverterTimestamp.write(item, into: &buf) FfiConverterInt64.write(item, into: &buf)
} }
} }
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [Date] { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [Int64] {
let len: Int32 = try readInt(&buf) let len: Int32 = try readInt(&buf)
var seq = [Date]() var seq = [Int64]()
seq.reserveCapacity(Int(len)) seq.reserveCapacity(Int(len))
for _ in 0 ..< len { for _ in 0 ..< len {
try seq.append(FfiConverterTimestamp.read(from: &buf)) try seq.append(FfiConverterInt64.read(from: &buf))
} }
return seq return seq
} }
@@ -1928,16 +1797,6 @@ private func uniffiFutureContinuationCallback(handle: UInt64, pollResult: Int8)
} }
} }
public func getDbPath(peer: String, spaceType: String, id: String) throws -> String {
return try FfiConverterString.lift(rustCallWithError(FfiConverterTypeUniffiError.lift) {
uniffi_affine_mobile_native_fn_func_get_db_path(
FfiConverterString.lower(peer),
FfiConverterString.lower(spaceType),
FfiConverterString.lower(id), $0
)
})
}
public func hashcashMint(resource: String, bits: UInt32) -> String { public func hashcashMint(resource: String, bits: UInt32) -> String {
return try! FfiConverterString.lift(try! rustCall { return try! FfiConverterString.lift(try! rustCall {
uniffi_affine_mobile_native_fn_func_hashcash_mint( uniffi_affine_mobile_native_fn_func_hashcash_mint(
@@ -1947,6 +1806,13 @@ public func hashcashMint(resource: String, bits: UInt32) -> String {
}) })
} }
public func newDocStoragePool() -> DocStoragePool {
return try! FfiConverterTypeDocStoragePool.lift(try! rustCall {
uniffi_affine_mobile_native_fn_func_new_doc_storage_pool($0
)
})
}
private enum InitializationResult { private enum InitializationResult {
case ok case ok
case contractVersionMismatch case contractVersionMismatch
@@ -1963,22 +1829,16 @@ private var initializationResult: InitializationResult = {
if bindings_contract_version != scaffolding_contract_version { if bindings_contract_version != scaffolding_contract_version {
return InitializationResult.contractVersionMismatch return InitializationResult.contractVersionMismatch
} }
if uniffi_affine_mobile_native_checksum_func_get_db_path() != 65350 {
return InitializationResult.apiChecksumMismatch
}
if uniffi_affine_mobile_native_checksum_func_hashcash_mint() != 23633 { if uniffi_affine_mobile_native_checksum_func_hashcash_mint() != 23633 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_checkpoint() != 36299 { if uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool() != 32882 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks() != 51151 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks() != 51151 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_close() != 46846 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_connect() != 19047 {
return InitializationResult.apiChecksumMismatch
}
if uniffi_affine_mobile_native_checksum_method_docstoragepool_connect() != 57961 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_blob() != 53695 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_blob() != 53695 {
@@ -1987,13 +1847,16 @@ private var initializationResult: InitializationResult = {
if uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_doc() != 4005 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_doc() != 4005 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_disconnect() != 20410 {
return InitializationResult.apiChecksumMismatch
}
if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_blob() != 56927 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_blob() != 56927 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_clock() != 48394 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_clock() != 48394 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_clocks() != 23822 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_clocks() != 46082 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_snapshot() != 31220 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_doc_snapshot() != 31220 {
@@ -2017,16 +1880,13 @@ private var initializationResult: InitializationResult = {
if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_remote_clocks() != 14523 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_remote_clocks() != 14523 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_is_closed() != 40091 {
return InitializationResult.apiChecksumMismatch
}
if uniffi_affine_mobile_native_checksum_method_docstoragepool_list_blobs() != 6777 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_list_blobs() != 6777 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_mark_updates_merged() != 26982 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_mark_updates_merged() != 42713 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_push_update() != 54572 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_push_update() != 20688 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_release_blobs() != 2203 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_release_blobs() != 2203 {
@@ -2038,21 +1898,18 @@ private var initializationResult: InitializationResult = {
if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_doc_snapshot() != 5287 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_doc_snapshot() != 5287 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_pulled_remote_clock() != 40733 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_pulled_remote_clock() != 33923 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_pushed_clock() != 15697 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_pushed_clock() != 16565 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_remote_clock() != 57108 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_remote_clock() != 46506 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_space_id() != 21955 { if uniffi_affine_mobile_native_checksum_method_docstoragepool_set_space_id() != 21955 {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if uniffi_affine_mobile_native_checksum_method_docstoragepool_validate() != 17232 {
return InitializationResult.apiChecksumMismatch
}
return InitializationResult.ok return InitializationResult.ok
}() }()
@@ -261,24 +261,14 @@ void*_Nonnull uniffi_affine_mobile_native_fn_clone_docstoragepool(void*_Nonnull
void uniffi_affine_mobile_native_fn_free_docstoragepool(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status void uniffi_affine_mobile_native_fn_free_docstoragepool(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CHECKPOINT
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CHECKPOINT
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_checkpoint(void*_Nonnull ptr, RustBuffer universal_id
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_clear_clocks(void*_Nonnull ptr, RustBuffer universal_id uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_clear_clocks(void*_Nonnull ptr, RustBuffer universal_id
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLOSE
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CLOSE
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_close(void*_Nonnull ptr, RustBuffer universal_id
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CONNECT #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CONNECT
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CONNECT #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_CONNECT
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_connect(void*_Nonnull ptr, RustBuffer universal_id uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_connect(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer path
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_DELETE_BLOB #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_DELETE_BLOB
@@ -291,6 +281,11 @@ uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_delete_blob(void*_
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_delete_doc(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer doc_id uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_delete_doc(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer doc_id
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_DISCONNECT
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_DISCONNECT
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_disconnect(void*_Nonnull ptr, RustBuffer universal_id
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_BLOB #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_BLOB
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_BLOB #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_GET_BLOB
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_blob(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer key uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_blob(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer key
@@ -341,11 +336,6 @@ uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_remote_cl
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_remote_clocks(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_get_peer_remote_clocks(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_IS_CLOSED
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_IS_CLOSED
int8_t uniffi_affine_mobile_native_fn_method_docstoragepool_is_closed(void*_Nonnull ptr, RustBuffer universal_id, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_LIST_BLOBS #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_LIST_BLOBS
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_LIST_BLOBS #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_LIST_BLOBS
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_list_blobs(void*_Nonnull ptr, RustBuffer universal_id uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_list_blobs(void*_Nonnull ptr, RustBuffer universal_id
@@ -378,17 +368,17 @@ uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_doc_snapshot(v
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PULLED_REMOTE_CLOCK #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PULLED_REMOTE_CLOCK
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PULLED_REMOTE_CLOCK #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PULLED_REMOTE_CLOCK
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pulled_remote_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id, RustBuffer clock uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pulled_remote_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id, int64_t clock
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PUSHED_CLOCK #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PUSHED_CLOCK
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PUSHED_CLOCK #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_PUSHED_CLOCK
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pushed_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id, RustBuffer clock uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_pushed_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id, int64_t clock
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_REMOTE_CLOCK #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_REMOTE_CLOCK
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_REMOTE_CLOCK #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_PEER_REMOTE_CLOCK
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_remote_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id, RustBuffer clock uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_remote_clock(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer peer, RustBuffer doc_id, int64_t clock
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_SPACE_ID #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_SET_SPACE_ID
@@ -396,19 +386,15 @@ uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_peer_remote_cl
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_space_id(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer space_id uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_set_space_id(void*_Nonnull ptr, RustBuffer universal_id, RustBuffer space_id
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_VALIDATE
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_METHOD_DOCSTORAGEPOOL_VALIDATE
uint64_t uniffi_affine_mobile_native_fn_method_docstoragepool_validate(void*_Nonnull ptr, RustBuffer universal_id
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_GET_DB_PATH
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_GET_DB_PATH
RustBuffer uniffi_affine_mobile_native_fn_func_get_db_path(RustBuffer peer, RustBuffer space_type, RustBuffer id, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_HASHCASH_MINT #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_HASHCASH_MINT
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_HASHCASH_MINT #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_HASHCASH_MINT
RustBuffer uniffi_affine_mobile_native_fn_func_hashcash_mint(RustBuffer resource, uint32_t bits, RustCallStatus *_Nonnull out_status RustBuffer uniffi_affine_mobile_native_fn_func_hashcash_mint(RustBuffer resource, uint32_t bits, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_NEW_DOC_STORAGE_POOL
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_FN_FUNC_NEW_DOC_STORAGE_POOL
void*_Nonnull uniffi_affine_mobile_native_fn_func_new_doc_storage_pool(RustCallStatus *_Nonnull out_status
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUSTBUFFER_ALLOC #ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUSTBUFFER_ALLOC
@@ -689,12 +675,6 @@ void ffi_affine_mobile_native_rust_future_free_void(uint64_t handle
#ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUST_FUTURE_COMPLETE_VOID #ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUST_FUTURE_COMPLETE_VOID
#define UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUST_FUTURE_COMPLETE_VOID #define UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_RUST_FUTURE_COMPLETE_VOID
void ffi_affine_mobile_native_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status void ffi_affine_mobile_native_rust_future_complete_void(uint64_t handle, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_GET_DB_PATH
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_GET_DB_PATH
uint16_t uniffi_affine_mobile_native_checksum_func_get_db_path(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_HASHCASH_MINT #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_HASHCASH_MINT
@@ -703,9 +683,9 @@ uint16_t uniffi_affine_mobile_native_checksum_func_hashcash_mint(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CHECKPOINT #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_NEW_DOC_STORAGE_POOL
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CHECKPOINT #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_FUNC_NEW_DOC_STORAGE_POOL
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_checkpoint(void uint16_t uniffi_affine_mobile_native_checksum_func_new_doc_storage_pool(void
); );
#endif #endif
@@ -713,12 +693,6 @@ uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_checkpoint(v
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLEAR_CLOCKS
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks(void uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_clear_clocks(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLOSE
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CLOSE
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_close(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CONNECT #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_CONNECT
@@ -737,6 +711,12 @@ uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_blob(
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_DELETE_DOC #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_DELETE_DOC
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_doc(void uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_delete_doc(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_DISCONNECT
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_DISCONNECT
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_disconnect(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_BLOB #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_BLOB
@@ -797,12 +777,6 @@ uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_rem
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCKS #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_GET_PEER_REMOTE_CLOCKS
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_remote_clocks(void uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_get_peer_remote_clocks(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_IS_CLOSED
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_IS_CLOSED
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_is_closed(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_LIST_BLOBS #ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_LIST_BLOBS
@@ -863,12 +837,6 @@ uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_set_peer_rem
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_SPACE_ID #define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_SET_SPACE_ID
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_set_space_id(void uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_set_space_id(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_VALIDATE
#define UNIFFI_FFIDEF_UNIFFI_AFFINE_MOBILE_NATIVE_CHECKSUM_METHOD_DOCSTORAGEPOOL_VALIDATE
uint16_t uniffi_affine_mobile_native_checksum_method_docstoragepool_validate(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_UNIFFI_CONTRACT_VERSION #ifndef UNIFFI_FFIDEF_FFI_AFFINE_MOBILE_NATIVE_UNIFFI_CONTRACT_VERSION
@@ -1,33 +0,0 @@
import { type BlobRecord, BlobStorageBase, share } from '@affine/nbstore';
import { NativeDBConnection } from './db';
export class SqliteBlobStorage extends BlobStorageBase {
override connection = share(
new NativeDBConnection(this.peer, this.spaceType, this.spaceId)
);
get db() {
return this.connection.inner;
}
override async get(key: string) {
return this.db.getBlob(key);
}
override async set(blob: BlobRecord) {
await this.db.setBlob(blob);
}
override async delete(key: string, permanently: boolean) {
await this.db.deleteBlob(key, permanently);
}
override async release() {
await this.db.releaseBlobs();
}
override async list() {
return this.db.listBlobs();
}
}
@@ -1,60 +0,0 @@
import type { DocStorage } from '@affine/native';
import {
AutoReconnectConnection,
isValidSpaceType,
type SpaceType,
universalId,
} from '@affine/nbstore';
import { NativeDocStorage, NbStoreDocStorage } from './plugin';
export class NativeDBConnection extends AutoReconnectConnection<DocStorage> {
private readonly universalId: string;
constructor(
private readonly peer: string,
private readonly type: SpaceType,
private readonly id: string
) {
super();
if (!isValidSpaceType(type)) {
throw new TypeError(`Invalid space type: ${type}`);
}
this.universalId = universalId({
peer: peer,
type: type,
id: id,
});
}
async getDBPath() {
const { path } = await NbStoreDocStorage.getSpaceDBPath({
peer: this.peer,
spaceType: this.type,
id: this.id,
});
return path;
}
override get shareId(): string {
return `sqlite:${this.peer}:${this.type}:${this.id}`;
}
override async doConnect() {
const conn = new NativeDocStorage(this.universalId);
await conn.connect();
console.info('[nbstore] connection established', this.shareId);
return conn;
}
override doDisconnect(conn: NativeDocStorage) {
conn
.close()
.then(() => {
console.info('[nbstore] connection closed', this.shareId);
})
.catch(err => {
console.error('[nbstore] connection close failed', this.shareId, err);
});
}
}
@@ -27,17 +27,13 @@ export interface DocClock {
} }
export interface NbStorePlugin { export interface NbStorePlugin {
getSpaceDBPath: (options: { connect: (options: {
peer: string;
spaceType: string;
id: string; id: string;
}) => Promise<{ path: string }>; spaceId: string;
create: (options: { id: string; path: string }) => Promise<void>; spaceType: string;
connect: (options: { id: string }) => Promise<void>; peer: string;
close: (options: { id: string }) => Promise<void>; }) => Promise<void>;
isClosed: (options: { id: string }) => Promise<{ isClosed: boolean }>; disconnect: (options: { id: string }) => Promise<void>;
checkpoint: (options: { id: string }) => Promise<void>;
validate: (options: { id: string }) => Promise<{ isValidate: boolean }>;
setSpaceId: (options: { id: string; spaceId: string }) => Promise<void>; setSpaceId: (options: { id: string; spaceId: string }) => Promise<void>;
pushUpdate: (options: { pushUpdate: (options: {
@@ -49,7 +45,7 @@ export interface NbStorePlugin {
| { | {
docId: string; docId: string;
// base64 encoded data // base64 encoded data
data: string; bin: string;
timestamp: number; timestamp: number;
} }
| undefined | undefined
@@ -57,23 +53,24 @@ export interface NbStorePlugin {
setDocSnapshot: (options: { setDocSnapshot: (options: {
id: string; id: string;
docId: string; docId: string;
data: string; bin: string;
timestamp: number;
}) => Promise<{ success: boolean }>; }) => Promise<{ success: boolean }>;
getDocUpdates: (options: { id: string; docId: string }) => Promise< getDocUpdates: (options: { id: string; docId: string }) => Promise<{
{ updates: {
docId: string; docId: string;
createdAt: number; timestamp: number;
// base64 encoded data // base64 encoded data
data: string; bin: string;
}[] }[];
>; }>;
markUpdatesMerged: (options: { markUpdatesMerged: (options: {
id: string; id: string;
docId: string; docId: string;
timestamps: number[]; timestamps: number[];
}) => Promise<{ count: number }>; }) => Promise<{ count: number }>;
deleteDoc: (options: { id: string; docId: string }) => Promise<void>; deleteDoc: (options: { id: string; docId: string }) => Promise<void>;
getDocClocks: (options: { id: string; after: number }) => Promise< getDocClocks: (options: { id: string; after?: number | null }) => Promise<
{ {
docId: string; docId: string;
timestamp: number; timestamp: number;
@@ -1,83 +0,0 @@
import {
type DocClocks,
type DocRecord,
DocStorageBase,
type DocUpdate,
share,
} from '@affine/nbstore';
import { NativeDBConnection } from './db';
export class SqliteDocStorage extends DocStorageBase {
override connection = share(
new NativeDBConnection(this.peer, this.spaceType, this.spaceId)
);
get db() {
return this.connection.inner;
}
override async pushDocUpdate(update: DocUpdate) {
const timestamp = await this.db.pushUpdate(update.docId, update.bin);
return { docId: update.docId, timestamp };
}
override async deleteDoc(docId: string) {
await this.db.deleteDoc(docId);
}
override async getDocTimestamps(after?: Date) {
const clocks = await this.db.getDocClocks(after);
return clocks.reduce((ret, cur) => {
ret[cur.docId] = cur.timestamp;
return ret;
}, {} as DocClocks);
}
override async getDocTimestamp(docId: string) {
return this.db.getDocClock(docId);
}
protected override async getDocSnapshot(docId: string) {
const snapshot = await this.db.getDocSnapshot(docId);
if (!snapshot) {
return null;
}
return {
docId,
bin: snapshot.data,
timestamp: snapshot.timestamp,
};
}
protected override async setDocSnapshot(
snapshot: DocRecord
): Promise<boolean> {
return this.db.setDocSnapshot({
docId: snapshot.docId,
data: Buffer.from(snapshot.bin),
timestamp: new Date(snapshot.timestamp),
});
}
protected override async getDocUpdates(docId: string) {
return this.db.getDocUpdates(docId).then(updates =>
updates.map(update => ({
docId,
bin: update.data,
timestamp: update.createdAt,
}))
);
}
protected override markUpdatesMerged(docId: string, updates: DocRecord[]) {
return this.db.markUpdatesMerged(
docId,
updates.map(update => update.timestamp)
);
}
}
@@ -1,128 +0,0 @@
import {
type BlobRecord,
type DocClock,
type DocUpdate,
} from '@affine/nbstore';
import { ensureStorage, getStorage } from './storage';
export const nbstoreHandlers = {
connect: async (id: string) => {
await ensureStorage(id);
},
close: async (id: string) => {
const store = getStorage(id);
if (store) {
store.disconnect();
// The store may be shared with other tabs, so we don't delete it from cache
// the underlying connection will handle the close correctly
// STORE_CACHE.delete(`${spaceType}:${spaceId}`);
}
},
pushDocUpdate: async (id: string, update: DocUpdate) => {
const store = await ensureStorage(id);
return store.get('doc').pushDocUpdate(update);
},
getDoc: async (id: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('doc').getDoc(docId);
},
deleteDoc: async (id: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('doc').deleteDoc(docId);
},
getDocTimestamps: async (id: string, after?: Date) => {
const store = await ensureStorage(id);
return store.get('doc').getDocTimestamps(after);
},
getDocTimestamp: async (id: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('doc').getDocTimestamp(docId);
},
setBlob: async (id: string, blob: BlobRecord) => {
const store = await ensureStorage(id);
return store.get('blob').set(blob);
},
getBlob: async (id: string, key: string) => {
const store = await ensureStorage(id);
return store.get('blob').get(key);
},
deleteBlob: async (id: string, key: string, permanently: boolean) => {
const store = await ensureStorage(id);
return store.get('blob').delete(key, permanently);
},
listBlobs: async (id: string) => {
const store = await ensureStorage(id);
return store.get('blob').list();
},
releaseBlobs: async (id: string) => {
const store = await ensureStorage(id);
return store.get('blob').release();
},
getPeerRemoteClocks: async (id: string, peer: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerRemoteClocks(peer);
},
getPeerRemoteClock: async (id: string, peer: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerRemoteClock(peer, docId);
},
setPeerRemoteClock: async (id: string, peer: string, clock: DocClock) => {
const store = await ensureStorage(id);
return store.get('sync').setPeerRemoteClock(peer, clock);
},
getPeerPulledRemoteClocks: async (id: string, peer: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerPulledRemoteClocks(peer);
},
getPeerPulledRemoteClock: async (id: string, peer: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerPulledRemoteClock(peer, docId);
},
setPeerPulledRemoteClock: async (
id: string,
peer: string,
clock: DocClock
) => {
const store = await ensureStorage(id);
return store.get('sync').setPeerPulledRemoteClock(peer, clock);
},
getPeerPushedClocks: async (id: string, peer: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerPushedClocks(peer);
},
getPeerPushedClock: async (id: string, peer: string, docId: string) => {
const store = await ensureStorage(id);
return store.get('sync').getPeerPushedClock(peer, docId);
},
setPeerPushedClock: async (id: string, peer: string, clock: DocClock) => {
const store = await ensureStorage(id);
return store.get('sync').setPeerPushedClock(peer, clock);
},
clearClocks: async (id: string) => {
const store = await ensureStorage(id);
return store.get('sync').clearClocks();
},
};
@@ -1,5 +1,304 @@
import {
base64ToUint8Array,
uint8ArrayToBase64,
} from '@affine/core/modules/workspace-engine';
import {
type BlobRecord,
type DocClock,
type DocRecord,
type ListedBlobRecord,
parseUniversalId,
} from '@affine/nbstore';
import { type NativeDBApis } from '@affine/nbstore/sqlite';
import { registerPlugin } from '@capacitor/core';
import type { NbStorePlugin } from './definitions';
export * from './definitions'; export * from './definitions';
export { nbstoreHandlers } from './handlers';
export { NbStoreDocStorage } from './plugin'; export const NbStore = registerPlugin<NbStorePlugin>('NbStoreDocStorage');
export * from './storage';
export { universalId } from '@affine/nbstore'; export const NbStoreNativeDBApis: NativeDBApis = {
connect: async function (id: string): Promise<void> {
const { peer, type, id: spaceId } = parseUniversalId(id);
return await NbStore.connect({ id, spaceId, spaceType: type, peer });
},
disconnect: function (id: string): Promise<void> {
return NbStore.disconnect({ id });
},
pushUpdate: async function (
id: string,
docId: string,
update: Uint8Array
): Promise<Date> {
const { timestamp } = await NbStore.pushUpdate({
id,
docId,
data: await uint8ArrayToBase64(update),
});
return new Date(timestamp);
},
getDocSnapshot: async function (
id: string,
docId: string
): Promise<DocRecord | null> {
const snapshot = await NbStore.getDocSnapshot({ id, docId });
return snapshot
? {
bin: base64ToUint8Array(snapshot.bin),
docId: snapshot.docId,
timestamp: new Date(snapshot.timestamp),
}
: null;
},
setDocSnapshot: async function (
id: string,
snapshot: DocRecord
): Promise<boolean> {
const { success } = await NbStore.setDocSnapshot({
id,
docId: snapshot.docId,
bin: await uint8ArrayToBase64(snapshot.bin),
timestamp: snapshot.timestamp.getTime(),
});
return success;
},
getDocUpdates: async function (
id: string,
docId: string
): Promise<DocRecord[]> {
const { updates } = await NbStore.getDocUpdates({ id, docId });
return updates.map(update => ({
bin: base64ToUint8Array(update.bin),
docId: update.docId,
timestamp: new Date(update.timestamp),
}));
},
markUpdatesMerged: async function (
id: string,
docId: string,
updates: Date[]
): Promise<number> {
const { count } = await NbStore.markUpdatesMerged({
id,
docId,
timestamps: updates.map(t => t.getTime()),
});
return count;
},
deleteDoc: async function (id: string, docId: string): Promise<void> {
await NbStore.deleteDoc({
id,
docId,
});
},
getDocClocks: async function (
id: string,
after?: Date | undefined | null
): Promise<DocClock[]> {
const clocks = await NbStore.getDocClocks({
id,
after: after?.getTime(),
});
return clocks.map(c => ({
docId: c.docId,
timestamp: new Date(c.timestamp),
}));
},
getDocClock: async function (
id: string,
docId: string
): Promise<DocClock | null> {
const clock = await NbStore.getDocClock({
id,
docId,
});
return clock
? {
timestamp: new Date(clock.timestamp),
docId: clock.docId,
}
: null;
},
getBlob: async function (
id: string,
key: string
): Promise<BlobRecord | null> {
const record = await NbStore.getBlob({
id,
key,
});
return record
? {
data: base64ToUint8Array(record.data),
key: record.key,
mime: record.mime,
createdAt: new Date(record.createdAt),
}
: null;
},
setBlob: async function (id: string, blob: BlobRecord): Promise<void> {
await NbStore.setBlob({
id,
data: await uint8ArrayToBase64(blob.data),
key: blob.key,
mime: blob.mime,
});
},
deleteBlob: async function (
id: string,
key: string,
permanently: boolean
): Promise<void> {
await NbStore.deleteBlob({
id,
key,
permanently,
});
},
releaseBlobs: async function (id: string): Promise<void> {
await NbStore.releaseBlobs({
id,
});
},
listBlobs: async function (id: string): Promise<ListedBlobRecord[]> {
const listed = await NbStore.listBlobs({
id,
});
return listed.map(b => ({
key: b.key,
mime: b.mime,
size: b.size,
createdAt: new Date(b.createdAt),
}));
},
getPeerRemoteClocks: async function (
id: string,
peer: string
): Promise<DocClock[]> {
const clocks = await NbStore.getPeerRemoteClocks({
id,
peer,
});
return clocks.map(c => ({
docId: c.docId,
timestamp: new Date(c.timestamp),
}));
},
getPeerRemoteClock: async function (
id: string,
peer: string,
docId: string
): Promise<DocClock> {
const clock = await NbStore.getPeerRemoteClock({
id,
peer,
docId,
});
return {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
};
},
setPeerRemoteClock: async function (
id: string,
peer: string,
docId: string,
clock: Date
): Promise<void> {
await NbStore.setPeerRemoteClock({
id,
peer,
docId,
clock: clock.getTime(),
});
},
getPeerPulledRemoteClocks: async function (
id: string,
peer: string
): Promise<DocClock[]> {
const clocks = await NbStore.getPeerPulledRemoteClocks({
id,
peer,
});
return clocks.map(c => ({
docId: c.docId,
timestamp: new Date(c.timestamp),
}));
},
getPeerPulledRemoteClock: async function (
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),
};
},
setPeerPulledRemoteClock: async function (
id: string,
peer: string,
docId: string,
clock: Date
): Promise<void> {
await NbStore.setPeerPulledRemoteClock({
id,
peer,
docId,
clock: clock.getTime(),
});
},
getPeerPushedClocks: async function (
id: string,
peer: string
): Promise<DocClock[]> {
const clocks = await NbStore.getPeerPushedClocks({
id,
peer,
});
return clocks.map(c => ({
docId: c.docId,
timestamp: new Date(c.timestamp),
}));
},
getPeerPushedClock: async function (
id: string,
peer: string,
docId: string
): Promise<DocClock> {
const clock = await NbStore.getPeerPushedClock({
id,
peer,
docId,
});
return {
docId: clock.docId,
timestamp: new Date(clock.timestamp),
};
},
setPeerPushedClock: async function (
id: string,
peer: string,
docId: string,
clock: Date
): Promise<void> {
await NbStore.setPeerPushedClock({
id,
peer,
docId,
clock: clock.getTime(),
});
},
clearClocks: async function (id: string): Promise<void> {
await NbStore.clearClocks({
id,
});
},
};
@@ -1,312 +0,0 @@
import {
base64ToUint8Array,
uint8ArrayToBase64,
} from '@affine/core/modules/workspace-engine';
import {
type Blob,
type DocClock,
type DocRecord,
type DocStorage,
type DocUpdate,
type ListedBlob,
} from '@affine/native';
import { registerPlugin } from '@capacitor/core';
import type { NbStorePlugin } from './definitions';
export const NbStoreDocStorage =
registerPlugin<NbStorePlugin>('NbStoreDocStorage');
export interface SetBlob {
key: string;
data: Uint8Array;
mime: string;
}
export class NativeDocStorage implements DocStorage {
constructor(private readonly universalId: string) {}
/** Initialize the database and run migrations. */
connect(): Promise<void> {
return NbStoreDocStorage.connect({
id: this.universalId,
});
}
close(): Promise<void> {
return NbStoreDocStorage.close({
id: this.universalId,
});
}
get isClosed(): Promise<boolean> {
return NbStoreDocStorage.isClosed({
id: this.universalId,
}).then(result => result.isClosed);
}
/**
* Flush the WAL file to the database file.
* See https://www.sqlite.org/pragma.html#pragma_wal_checkpoint:~:text=PRAGMA%20schema.wal_checkpoint%3B
*/
checkpoint(): Promise<void> {
return NbStoreDocStorage.checkpoint({
id: this.universalId,
});
}
validate(): Promise<boolean> {
return NbStoreDocStorage.validate({
id: this.universalId,
}).then(result => result.isValidate);
}
setSpaceId(spaceId: string): Promise<void> {
return NbStoreDocStorage.setSpaceId({
id: this.universalId,
spaceId,
});
}
async pushUpdate(docId: string, update: Uint8Array): Promise<Date> {
return NbStoreDocStorage.pushUpdate({
id: this.universalId,
docId,
data: await uint8ArrayToBase64(update),
}).then(result => new Date(result.timestamp));
}
getDocSnapshot(docId: string): Promise<DocRecord | null> {
return NbStoreDocStorage.getDocSnapshot({
id: this.universalId,
docId,
}).then(result => {
if (result) {
return {
...result,
data: base64ToUint8Array(result.data),
timestamp: new Date(result.timestamp),
};
}
return null;
});
}
async setDocSnapshot(snapshot: DocRecord): Promise<boolean> {
return NbStoreDocStorage.setDocSnapshot({
id: this.universalId,
docId: snapshot.docId,
data: await uint8ArrayToBase64(snapshot.data),
}).then(result => result.success);
}
getDocUpdates(docId: string): Promise<Array<DocUpdate>> {
return NbStoreDocStorage.getDocUpdates({
id: this.universalId,
docId,
}).then(result =>
result.map(update => ({
...update,
data: base64ToUint8Array(update.data),
createdAt: new Date(update.createdAt),
}))
);
}
markUpdatesMerged(docId: string, updates: Array<Date>): Promise<number> {
return NbStoreDocStorage.markUpdatesMerged({
id: this.universalId,
docId,
timestamps: updates.map(date => date.getTime()),
}).then(result => result.count);
}
deleteDoc(docId: string): Promise<void> {
return NbStoreDocStorage.deleteDoc({
id: this.universalId,
docId,
});
}
getDocClocks(after: Date): Promise<Array<DocClock>> {
return NbStoreDocStorage.getDocClocks({
id: this.universalId,
after: after.getTime(),
}).then(result =>
result.map(clock => ({
...clock,
timestamp: new Date(clock.timestamp),
}))
);
}
getDocClock(docId: string): Promise<DocClock | null> {
return NbStoreDocStorage.getDocClock({
id: this.universalId,
docId,
}).then(result => {
if (result) {
return {
...result,
timestamp: new Date(result.timestamp),
};
}
return null;
});
}
getBlob(key: string): Promise<Blob | null> {
return NbStoreDocStorage.getBlob({
id: this.universalId,
key,
}).then(result => {
if (result) {
return {
...result,
data: base64ToUint8Array(result.data),
createdAt: new Date(result.createdAt),
};
}
return null;
});
}
async setBlob(blob: SetBlob): Promise<void> {
return NbStoreDocStorage.setBlob({
id: this.universalId,
key: blob.key,
data: await uint8ArrayToBase64(blob.data),
mime: blob.mime,
});
}
deleteBlob(key: string, permanently: boolean): Promise<void> {
return NbStoreDocStorage.deleteBlob({
id: this.universalId,
key,
permanently,
});
}
releaseBlobs(): Promise<void> {
return NbStoreDocStorage.releaseBlobs({
id: this.universalId,
});
}
async listBlobs(): Promise<Array<ListedBlob>> {
return (
await NbStoreDocStorage.listBlobs({
id: this.universalId,
})
).map(blob => ({
...blob,
createdAt: new Date(blob.createdAt),
}));
}
getPeerRemoteClocks(peer: string): Promise<Array<DocClock>> {
return NbStoreDocStorage.getPeerRemoteClocks({
id: this.universalId,
peer,
}).then(result =>
result.map(clock => ({
...clock,
timestamp: new Date(clock.timestamp),
}))
);
}
getPeerRemoteClock(peer: string, docId: string): Promise<DocClock> {
return NbStoreDocStorage.getPeerRemoteClock({
id: this.universalId,
peer,
docId,
}).then(result => ({
...result,
timestamp: new Date(result.timestamp),
}));
}
setPeerRemoteClock(peer: string, docId: string, clock: Date): Promise<void> {
return NbStoreDocStorage.setPeerRemoteClock({
id: this.universalId,
peer,
docId,
clock: clock.getTime(),
});
}
getPeerPulledRemoteClocks(peer: string): Promise<Array<DocClock>> {
return NbStoreDocStorage.getPeerPulledRemoteClocks({
id: this.universalId,
peer,
}).then(result =>
result.map(clock => ({
...clock,
timestamp: new Date(clock.timestamp),
}))
);
}
getPeerPulledRemoteClock(peer: string, docId: string): Promise<DocClock> {
return NbStoreDocStorage.getPeerPulledRemoteClock({
id: this.universalId,
peer,
docId,
}).then(result => ({
...result,
timestamp: new Date(result.timestamp),
}));
}
setPeerPulledRemoteClock(
peer: string,
docId: string,
clock: Date
): Promise<void> {
return NbStoreDocStorage.setPeerPulledRemoteClock({
id: this.universalId,
peer,
docId,
clock: clock.getTime(),
});
}
getPeerPushedClocks(peer: string): Promise<Array<DocClock>> {
return NbStoreDocStorage.getPeerPushedClocks({
id: this.universalId,
peer,
}).then(result =>
result.map(clock => ({
...clock,
timestamp: new Date(clock.timestamp),
}))
);
}
getPeerPushedClock(peer: string, docId: string): Promise<DocClock> {
return NbStoreDocStorage.getPeerPushedClock({
id: this.universalId,
peer,
docId,
}).then(result => ({
...result,
timestamp: new Date(result.timestamp),
}));
}
setPeerPushedClock(peer: string, docId: string, clock: Date): Promise<void> {
return NbStoreDocStorage.setPeerPushedClock({
id: this.universalId,
peer,
docId,
clock: clock.getTime(),
});
}
clearClocks(): Promise<void> {
return NbStoreDocStorage.clearClocks({
id: this.universalId,
});
}
}
@@ -1,83 +0,0 @@
import { parseUniversalId, SpaceStorage } from '@affine/nbstore';
import { applyUpdate, Doc as YDoc } from 'yjs';
import { SqliteBlobStorage } from './blob';
import { NativeDBConnection } from './db';
import { SqliteDocStorage } from './doc';
import { SqliteSyncStorage } from './sync';
export class SqliteSpaceStorage extends SpaceStorage {
get connection() {
const docStore = this.get('doc');
if (!docStore) {
throw new Error('doc store not found');
}
const connection = docStore.connection;
if (!(connection instanceof NativeDBConnection)) {
throw new Error('doc store connection is not a Sqlite connection');
}
return connection;
}
async getDBPath() {
return this.connection.getDBPath();
}
async getWorkspaceName() {
const docStore = this.tryGet('doc');
if (!docStore) {
return null;
}
const doc = await docStore.getDoc(docStore.spaceId);
if (!doc) {
return null;
}
const ydoc = new YDoc();
applyUpdate(ydoc, doc.bin);
return ydoc.getMap('meta').get('name') as string;
}
async checkpoint() {
await this.connection.inner.checkpoint();
}
}
const STORE_CACHE = new Map<string, SqliteSpaceStorage>();
export function getStorage(universalId: string) {
return STORE_CACHE.get(universalId);
}
export async function ensureStorage(universalId: string) {
const { peer, type, id } = parseUniversalId(universalId);
let store = STORE_CACHE.get(universalId);
if (!store) {
const opts = {
peer,
type,
id,
};
store = new SqliteSpaceStorage([
new SqliteDocStorage(opts),
new SqliteBlobStorage(opts),
new SqliteSyncStorage(opts),
]);
store.connect();
await store.waitForConnected();
STORE_CACHE.set(universalId, store);
}
return store;
}
@@ -1,70 +0,0 @@
import {
BasicSyncStorage,
type DocClock,
type DocClocks,
share,
} from '@affine/nbstore';
import { NativeDBConnection } from './db';
export class SqliteSyncStorage extends BasicSyncStorage {
override connection = share(
new NativeDBConnection(this.peer, this.spaceType, this.spaceId)
);
get db() {
return this.connection.inner;
}
override async getPeerRemoteClocks(peer: string) {
const records = await this.db.getPeerRemoteClocks(peer);
return records.reduce((clocks, { docId, timestamp }) => {
clocks[docId] = timestamp;
return clocks;
}, {} as DocClocks);
}
override async getPeerRemoteClock(peer: string, docId: string) {
return this.db.getPeerRemoteClock(peer, docId);
}
override async setPeerRemoteClock(peer: string, clock: DocClock) {
await this.db.setPeerRemoteClock(peer, clock.docId, clock.timestamp);
}
override async getPeerPulledRemoteClock(peer: string, docId: string) {
return this.db.getPeerPulledRemoteClock(peer, docId);
}
override async getPeerPulledRemoteClocks(peer: string) {
const records = await this.db.getPeerPulledRemoteClocks(peer);
return records.reduce((clocks, { docId, timestamp }) => {
clocks[docId] = timestamp;
return clocks;
}, {} as DocClocks);
}
override async setPeerPulledRemoteClock(peer: string, clock: DocClock) {
await this.db.setPeerPulledRemoteClock(peer, clock.docId, clock.timestamp);
}
override async getPeerPushedClocks(peer: string) {
const records = await this.db.getPeerPushedClocks(peer);
return records.reduce((clocks, { docId, timestamp }) => {
clocks[docId] = timestamp;
return clocks;
}, {} as DocClocks);
}
override async getPeerPushedClock(peer: string, docId: string) {
return this.db.getPeerPushedClock(peer, docId);
}
override async setPeerPushedClock(peer: string, clock: DocClock) {
await this.db.setPeerPushedClock(peer, clock.docId, clock.timestamp);
}
override async clearClocks() {
await this.db.clearClocks();
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ chrono = { workspace = true }
dashmap = { workspace = true } dashmap = { workspace = true }
sqlx = { workspace = true } sqlx = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
uniffi = { workspace = true, features = ["cli"] } uniffi = { workspace = true, features = ["cli", "tokio"] }
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies] [target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
objc2 = { workspace = true } objc2 = { workspace = true }
@@ -1,29 +0,0 @@
use thiserror::Error;
#[derive(uniffi::Error, Error, Debug)]
pub enum UniffiError {
#[error("Get user document directory failed")]
GetUserDocumentDirectoryFailed,
#[error("Create affine dir failed: {0}")]
CreateAffineDirFailed(String),
#[error("Empty doc storage path")]
EmptyDocStoragePath,
#[error("Empty space id")]
EmptySpaceId,
#[error("Sqlx error: {0}")]
SqlxError(String),
#[error("Base64 decoding error: {0}")]
Base64DecodingError(String),
#[error("Invalid universal storage id: {0}. It should be in format of @peer($peer);@type($type);@id($id);")]
InvalidUniversalId(String),
#[error("Invalid space type: {0}")]
InvalidSpaceType(String),
#[error("Concat space dir failed: {0}")]
ConcatSpaceDirFailed(String),
}
impl From<sqlx::Error> for UniffiError {
fn from(err: sqlx::Error) -> Self {
UniffiError::SqlxError(err.to_string())
}
}
+243 -363
View File
@@ -1,15 +1,23 @@
use std::fmt::Display;
use std::str::FromStr;
use std::time::SystemTime;
use affine_common::hashcash::Stamp; use affine_common::hashcash::Stamp;
use affine_nbstore::storage; use affine_nbstore::pool::SqliteDocStoragePool;
use dashmap::{mapref::one::RefMut, DashMap, Entry};
use crate::error::UniffiError; #[derive(uniffi::Error, thiserror::Error, Debug)]
pub enum UniffiError {
#[error("Error: {0}")]
Err(String),
#[error("Base64 decoding error: {0}")]
Base64DecodingError(String),
#[error("Timestamp decoding error")]
TimestampDecodingError,
}
mod error; impl From<affine_nbstore::error::Error> for UniffiError {
mod utils; fn from(err: affine_nbstore::error::Error) -> Self {
Self::Err(err.to_string())
}
}
type Result<T> = std::result::Result<T, UniffiError>;
uniffi::setup_scaffolding!("affine_mobile_native"); uniffi::setup_scaffolding!("affine_mobile_native");
@@ -22,16 +30,16 @@ pub fn hashcash_mint(resource: String, bits: u32) -> String {
pub struct DocRecord { pub struct DocRecord {
pub doc_id: String, pub doc_id: String,
// base64 encoded data // base64 encoded data
pub data: String, pub bin: String,
pub timestamp: SystemTime, pub timestamp: i64,
} }
impl From<affine_nbstore::DocRecord> for DocRecord { impl From<affine_nbstore::DocRecord> for DocRecord {
fn from(record: affine_nbstore::DocRecord) -> Self { fn from(record: affine_nbstore::DocRecord) -> Self {
Self { Self {
doc_id: record.doc_id, doc_id: record.doc_id,
data: base64_simd::STANDARD.encode_to_string(&record.data), bin: base64_simd::STANDARD.encode_to_string(&record.bin),
timestamp: record.timestamp.and_utc().into(), timestamp: record.timestamp.and_utc().timestamp_millis(),
} }
} }
} }
@@ -39,13 +47,15 @@ impl From<affine_nbstore::DocRecord> for DocRecord {
impl TryFrom<DocRecord> for affine_nbstore::DocRecord { impl TryFrom<DocRecord> for affine_nbstore::DocRecord {
type Error = UniffiError; type Error = UniffiError;
fn try_from(record: DocRecord) -> Result<Self, Self::Error> { fn try_from(record: DocRecord) -> Result<Self> {
Ok(Self { Ok(Self {
doc_id: record.doc_id, doc_id: record.doc_id,
data: base64_simd::STANDARD bin: base64_simd::STANDARD
.decode_to_vec(record.data) .decode_to_vec(record.bin)
.map_err(|e| UniffiError::Base64DecodingError(e.to_string()))?, .map_err(|e| UniffiError::Base64DecodingError(e.to_string()))?,
timestamp: chrono::DateTime::<chrono::Utc>::from(record.timestamp).naive_utc(), timestamp: chrono::DateTime::<chrono::Utc>::from_timestamp_millis(record.timestamp)
.ok_or(UniffiError::TimestampDecodingError)?
.naive_utc(),
}) })
} }
} }
@@ -53,52 +63,60 @@ impl TryFrom<DocRecord> for affine_nbstore::DocRecord {
#[derive(uniffi::Record)] #[derive(uniffi::Record)]
pub struct DocUpdate { pub struct DocUpdate {
pub doc_id: String, pub doc_id: String,
pub created_at: SystemTime, pub timestamp: i64,
// base64 encoded data // base64 encoded data
pub data: String, pub bin: String,
} }
impl From<affine_nbstore::DocUpdate> for DocUpdate { impl From<affine_nbstore::DocUpdate> for DocUpdate {
fn from(update: affine_nbstore::DocUpdate) -> Self { fn from(update: affine_nbstore::DocUpdate) -> Self {
Self { Self {
doc_id: update.doc_id, doc_id: update.doc_id,
created_at: update.created_at.and_utc().into(), timestamp: update.timestamp.and_utc().timestamp_millis(),
data: base64_simd::STANDARD.encode_to_string(&update.data), bin: base64_simd::STANDARD.encode_to_string(&update.bin),
} }
} }
} }
impl From<DocUpdate> for affine_nbstore::DocUpdate { impl TryFrom<DocUpdate> for affine_nbstore::DocUpdate {
fn from(update: DocUpdate) -> Self { type Error = UniffiError;
Self {
fn try_from(update: DocUpdate) -> Result<Self> {
Ok(Self {
doc_id: update.doc_id, doc_id: update.doc_id,
created_at: chrono::DateTime::<chrono::Utc>::from(update.created_at).naive_utc(), timestamp: chrono::DateTime::<chrono::Utc>::from_timestamp_millis(update.timestamp)
data: update.data.into(), .ok_or(UniffiError::TimestampDecodingError)?
} .naive_utc(),
bin: update.bin.into(),
})
} }
} }
#[derive(uniffi::Record)] #[derive(uniffi::Record)]
pub struct DocClock { pub struct DocClock {
pub doc_id: String, pub doc_id: String,
pub timestamp: SystemTime, pub timestamp: i64,
} }
impl From<affine_nbstore::DocClock> for DocClock { impl From<affine_nbstore::DocClock> for DocClock {
fn from(clock: affine_nbstore::DocClock) -> Self { fn from(clock: affine_nbstore::DocClock) -> Self {
Self { Self {
doc_id: clock.doc_id, doc_id: clock.doc_id,
timestamp: clock.timestamp.and_utc().into(), timestamp: clock.timestamp.and_utc().timestamp_millis(),
} }
} }
} }
impl From<DocClock> for affine_nbstore::DocClock { impl TryFrom<DocClock> for affine_nbstore::DocClock {
fn from(clock: DocClock) -> Self { type Error = UniffiError;
Self {
fn try_from(clock: DocClock) -> Result<Self> {
Ok(Self {
doc_id: clock.doc_id, doc_id: clock.doc_id,
timestamp: chrono::DateTime::<chrono::Utc>::from(clock.timestamp).naive_utc(), timestamp: chrono::DateTime::<chrono::Utc>::from_timestamp_millis(clock.timestamp)
} .ok_or(UniffiError::TimestampDecodingError)?
.naive_utc(),
})
} }
} }
@@ -109,7 +127,7 @@ pub struct Blob {
pub data: String, pub data: String,
pub mime: String, pub mime: String,
pub size: i64, pub size: i64,
pub created_at: SystemTime, pub created_at: i64,
} }
impl From<affine_nbstore::Blob> for Blob { impl From<affine_nbstore::Blob> for Blob {
@@ -119,7 +137,7 @@ impl From<affine_nbstore::Blob> for Blob {
data: base64_simd::STANDARD.encode_to_string(&blob.data), data: base64_simd::STANDARD.encode_to_string(&blob.data),
mime: blob.mime, mime: blob.mime,
size: blob.size, size: blob.size,
created_at: blob.created_at.and_utc().into(), created_at: blob.created_at.and_utc().timestamp_millis(),
} }
} }
} }
@@ -135,7 +153,7 @@ pub struct SetBlob {
impl TryFrom<SetBlob> for affine_nbstore::SetBlob { impl TryFrom<SetBlob> for affine_nbstore::SetBlob {
type Error = UniffiError; type Error = UniffiError;
fn try_from(blob: SetBlob) -> Result<Self, Self::Error> { fn try_from(blob: SetBlob) -> Result<Self> {
Ok(Self { Ok(Self {
key: blob.key, key: blob.key,
data: base64_simd::STANDARD data: base64_simd::STANDARD
@@ -151,7 +169,7 @@ pub struct ListedBlob {
pub key: String, pub key: String,
pub size: i64, pub size: i64,
pub mime: String, pub mime: String,
pub created_at: SystemTime, pub created_at: i64,
} }
impl From<affine_nbstore::ListedBlob> for ListedBlob { impl From<affine_nbstore::ListedBlob> for ListedBlob {
@@ -160,76 +178,43 @@ impl From<affine_nbstore::ListedBlob> for ListedBlob {
key: blob.key, key: blob.key,
size: blob.size, size: blob.size,
mime: blob.mime, mime: blob.mime,
created_at: blob.created_at.and_utc().into(), created_at: blob.created_at.and_utc().timestamp_millis(),
} }
} }
} }
#[derive(uniffi::Object)] #[derive(uniffi::Object)]
pub struct DocStoragePool { pub struct DocStoragePool {
inner: DashMap<String, storage::SqliteDocStorage>, inner: SqliteDocStoragePool,
}
impl DocStoragePool {
fn ensure_storage<'a>(
&'a self,
universal_id: &str,
) -> Result<RefMut<'a, String, storage::SqliteDocStorage>, UniffiError> {
let entry = self.inner.entry(universal_id.to_string());
if let Entry::Occupied(storage) = entry {
return Ok(storage.into_ref());
}
let options = parse_universal_id(entry.key())?;
let db_path = utils::get_db_path(&options)?;
if db_path.is_empty() {
return Err(UniffiError::EmptyDocStoragePath);
}
let storage = storage::SqliteDocStorage::new(db_path);
Ok(entry.or_insert(storage))
}
} }
#[uniffi::export] #[uniffi::export]
pub fn new_doc_storage_pool() -> DocStoragePool {
DocStoragePool {
inner: Default::default(),
}
}
#[uniffi::export(async_runtime = "tokio")]
impl DocStoragePool { impl DocStoragePool {
/// Initialize the database and run migrations. /// Initialize the database and run migrations.
pub async fn connect(&self, universal_id: String) -> Result<(), UniffiError> { pub async fn connect(&self, universal_id: String, path: String) -> Result<()> {
let storage = self.ensure_storage(&universal_id)?; Ok(self.inner.connect(universal_id, path).await?)
Ok(storage.connect().await?)
} }
pub async fn close(&self, universal_id: String) -> Result<(), UniffiError> { pub async fn disconnect(&self, universal_id: String) -> Result<()> {
let storage = self.ensure_storage(&universal_id)?; self.inner.disconnect(universal_id).await?;
storage.close().await;
self.inner.remove(&universal_id);
Ok(()) Ok(())
} }
pub fn is_closed(&self, universal_id: String) -> bool { pub async fn set_space_id(&self, universal_id: String, space_id: String) -> Result<()> {
let storage = self.ensure_storage(&universal_id).unwrap(); Ok(
storage.is_closed() self
} .inner
.ensure_storage(universal_id)?
pub async fn checkpoint(&self, universal_id: String) -> Result<(), UniffiError> { .set_space_id(space_id)
let storage = self.ensure_storage(&universal_id)?; .await?,
Ok(storage.checkpoint().await?) )
}
pub async fn validate(&self, universal_id: String) -> Result<bool, UniffiError> {
let storage = self.ensure_storage(&universal_id)?;
Ok(storage.validate().await?)
}
pub async fn set_space_id(
&self,
universal_id: String,
space_id: String,
) -> Result<(), UniffiError> {
let storage = self.ensure_storage(&universal_id)?;
if space_id.is_empty() {
return Err(UniffiError::EmptySpaceId);
}
Ok(storage.set_space_id(space_id).await?)
} }
pub async fn push_update( pub async fn push_update(
@@ -237,10 +222,11 @@ impl DocStoragePool {
universal_id: String, universal_id: String,
doc_id: String, doc_id: String,
update: String, update: String,
) -> Result<SystemTime, UniffiError> { ) -> Result<i64> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.push_update( .push_update(
doc_id, doc_id,
base64_simd::STANDARD base64_simd::STANDARD
@@ -249,7 +235,7 @@ impl DocStoragePool {
) )
.await? .await?
.and_utc() .and_utc()
.into(), .timestamp_millis(),
) )
} }
@@ -257,28 +243,36 @@ impl DocStoragePool {
&self, &self,
universal_id: String, universal_id: String,
doc_id: String, doc_id: String,
) -> Result<Option<DocRecord>, UniffiError> { ) -> Result<Option<DocRecord>> {
let storage = self.ensure_storage(&universal_id)?; Ok(
Ok(storage.get_doc_snapshot(doc_id).await?.map(Into::into)) self
.inner
.ensure_storage(universal_id)?
.get_doc_snapshot(doc_id)
.await?
.map(Into::into),
)
} }
pub async fn set_doc_snapshot( pub async fn set_doc_snapshot(&self, universal_id: String, snapshot: DocRecord) -> Result<bool> {
&self, Ok(
universal_id: String, self
snapshot: DocRecord, .inner
) -> Result<bool, UniffiError> { .ensure_storage(universal_id)?
let storage = self.ensure_storage(&universal_id)?; .set_doc_snapshot(snapshot.try_into()?)
Ok(storage.set_doc_snapshot(snapshot.try_into()?).await?) .await?,
)
} }
pub async fn get_doc_updates( pub async fn get_doc_updates(
&self, &self,
universal_id: String, universal_id: String,
doc_id: String, doc_id: String,
) -> Result<Vec<DocUpdate>, UniffiError> { ) -> Result<Vec<DocUpdate>> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.get_doc_updates(doc_id) .get_doc_updates(doc_id)
.await? .await?
.into_iter() .into_iter()
@@ -291,36 +285,55 @@ impl DocStoragePool {
&self, &self,
universal_id: String, universal_id: String,
doc_id: String, doc_id: String,
updates: Vec<SystemTime>, updates: Vec<i64>,
) -> Result<u32, UniffiError> { ) -> Result<u32> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.mark_updates_merged( .mark_updates_merged(
doc_id, doc_id,
updates updates
.into_iter() .into_iter()
.map(|t| chrono::DateTime::<chrono::Utc>::from(t).naive_utc()) .map(|t| {
.collect(), chrono::DateTime::<chrono::Utc>::from_timestamp_millis(t)
.ok_or(UniffiError::TimestampDecodingError)
.map(|t| t.naive_utc())
})
.collect::<Result<Vec<_>>>()?,
) )
.await?, .await?,
) )
} }
pub async fn delete_doc(&self, universal_id: String, doc_id: String) -> Result<(), UniffiError> { pub async fn delete_doc(&self, universal_id: String, doc_id: String) -> Result<()> {
let storage = self.ensure_storage(&universal_id)?; Ok(
Ok(storage.delete_doc(doc_id).await?) self
.inner
.ensure_storage(universal_id)?
.delete_doc(doc_id)
.await?,
)
} }
pub async fn get_doc_clocks( pub async fn get_doc_clocks(
&self, &self,
universal_id: String, universal_id: String,
after: Option<SystemTime>, after: Option<i64>,
) -> Result<Vec<DocClock>, UniffiError> { ) -> Result<Vec<DocClock>> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.get_doc_clocks(after.map(|t| chrono::DateTime::<chrono::Utc>::from(t).naive_utc())) .inner
.ensure_storage(universal_id)?
.get_doc_clocks(
after
.map(|t| {
chrono::DateTime::<chrono::Utc>::from_timestamp_millis(t)
.ok_or(UniffiError::TimestampDecodingError)
.map(|t| t.naive_utc())
})
.transpose()?,
)
.await? .await?
.into_iter() .into_iter()
.map(Into::into) .map(Into::into)
@@ -332,23 +345,36 @@ impl DocStoragePool {
&self, &self,
universal_id: String, universal_id: String,
doc_id: String, doc_id: String,
) -> Result<Option<DocClock>, UniffiError> { ) -> Result<Option<DocClock>> {
let storage = self.ensure_storage(&universal_id)?; Ok(
Ok(storage.get_doc_clock(doc_id).await?.map(Into::into)) self
.inner
.ensure_storage(universal_id)?
.get_doc_clock(doc_id)
.await?
.map(Into::into),
)
} }
pub async fn get_blob( pub async fn get_blob(&self, universal_id: String, key: String) -> Result<Option<Blob>> {
&self, Ok(
universal_id: String, self
key: String, .inner
) -> Result<Option<Blob>, UniffiError> { .ensure_storage(universal_id)?
let storage = self.ensure_storage(&universal_id)?; .get_blob(key)
Ok(storage.get_blob(key).await?.map(Into::into)) .await?
.map(Into::into),
)
} }
pub async fn set_blob(&self, universal_id: String, blob: SetBlob) -> Result<(), UniffiError> { pub async fn set_blob(&self, universal_id: String, blob: SetBlob) -> Result<()> {
let storage = self.ensure_storage(&universal_id)?; Ok(
Ok(storage.set_blob(blob.try_into()?).await?) self
.inner
.ensure_storage(universal_id)?
.set_blob(blob.try_into()?)
.await?,
)
} }
pub async fn delete_blob( pub async fn delete_blob(
@@ -356,20 +382,31 @@ impl DocStoragePool {
universal_id: String, universal_id: String,
key: String, key: String,
permanently: bool, permanently: bool,
) -> Result<(), UniffiError> { ) -> Result<()> {
let storage = self.ensure_storage(&universal_id)?;
Ok(storage.delete_blob(key, permanently).await?)
}
pub async fn release_blobs(&self, universal_id: String) -> Result<(), UniffiError> {
let storage = self.ensure_storage(&universal_id)?;
Ok(storage.release_blobs().await?)
}
pub async fn list_blobs(&self, universal_id: String) -> Result<Vec<ListedBlob>, UniffiError> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.delete_blob(key, permanently)
.await?,
)
}
pub async fn release_blobs(&self, universal_id: String) -> Result<()> {
Ok(
self
.inner
.ensure_storage(universal_id)?
.release_blobs()
.await?,
)
}
pub async fn list_blobs(&self, universal_id: String) -> Result<Vec<ListedBlob>> {
Ok(
self
.inner
.ensure_storage(universal_id)?
.list_blobs() .list_blobs()
.await? .await?
.into_iter() .into_iter()
@@ -382,10 +419,11 @@ impl DocStoragePool {
&self, &self,
universal_id: String, universal_id: String,
peer: String, peer: String,
) -> Result<Vec<DocClock>, UniffiError> { ) -> Result<Vec<DocClock>> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.get_peer_remote_clocks(peer) .get_peer_remote_clocks(peer)
.await? .await?
.into_iter() .into_iter()
@@ -399,9 +437,15 @@ impl DocStoragePool {
universal_id: String, universal_id: String,
peer: String, peer: String,
doc_id: String, doc_id: String,
) -> Result<DocClock, UniffiError> { ) -> Result<DocClock> {
let storage = self.ensure_storage(&universal_id)?; Ok(
Ok(storage.get_peer_remote_clock(peer, doc_id).await?.into()) self
.inner
.ensure_storage(universal_id)?
.get_peer_remote_clock(peer, doc_id)
.await?
.into(),
)
} }
pub async fn set_peer_remote_clock( pub async fn set_peer_remote_clock(
@@ -409,15 +453,18 @@ impl DocStoragePool {
universal_id: String, universal_id: String,
peer: String, peer: String,
doc_id: String, doc_id: String,
clock: SystemTime, clock: i64,
) -> Result<(), UniffiError> { ) -> Result<()> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.set_peer_remote_clock( .set_peer_remote_clock(
peer, peer,
doc_id, doc_id,
chrono::DateTime::<chrono::Utc>::from(clock).naive_utc(), chrono::DateTime::<chrono::Utc>::from_timestamp_millis(clock)
.ok_or(UniffiError::TimestampDecodingError)?
.naive_utc(),
) )
.await?, .await?,
) )
@@ -427,10 +474,11 @@ impl DocStoragePool {
&self, &self,
universal_id: String, universal_id: String,
peer: String, peer: String,
) -> Result<Vec<DocClock>, UniffiError> { ) -> Result<Vec<DocClock>> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.get_peer_pulled_remote_clocks(peer) .get_peer_pulled_remote_clocks(peer)
.await? .await?
.into_iter() .into_iter()
@@ -444,10 +492,11 @@ impl DocStoragePool {
universal_id: String, universal_id: String,
peer: String, peer: String,
doc_id: String, doc_id: String,
) -> Result<DocClock, UniffiError> { ) -> Result<DocClock> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.get_peer_pulled_remote_clock(peer, doc_id) .get_peer_pulled_remote_clock(peer, doc_id)
.await? .await?
.into(), .into(),
@@ -459,15 +508,18 @@ impl DocStoragePool {
universal_id: String, universal_id: String,
peer: String, peer: String,
doc_id: String, doc_id: String,
clock: SystemTime, clock: i64,
) -> Result<(), UniffiError> { ) -> Result<()> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.set_peer_pulled_remote_clock( .set_peer_pulled_remote_clock(
peer, peer,
doc_id, doc_id,
chrono::DateTime::<chrono::Utc>::from(clock).naive_utc(), chrono::DateTime::<chrono::Utc>::from_timestamp_millis(clock)
.ok_or(UniffiError::TimestampDecodingError)?
.naive_utc(),
) )
.await?, .await?,
) )
@@ -477,10 +529,11 @@ impl DocStoragePool {
&self, &self,
universal_id: String, universal_id: String,
peer: String, peer: String,
) -> Result<Vec<DocClock>, UniffiError> { ) -> Result<Vec<DocClock>> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.get_peer_pushed_clocks(peer) .get_peer_pushed_clocks(peer)
.await? .await?
.into_iter() .into_iter()
@@ -494,203 +547,30 @@ impl DocStoragePool {
universal_id: String, universal_id: String,
peer: String, peer: String,
doc_id: String, doc_id: String,
clock: SystemTime, clock: i64,
) -> Result<(), UniffiError> { ) -> Result<()> {
let storage = self.ensure_storage(&universal_id)?;
Ok( Ok(
storage self
.inner
.ensure_storage(universal_id)?
.set_peer_pushed_clock( .set_peer_pushed_clock(
peer, peer,
doc_id, doc_id,
chrono::DateTime::<chrono::Utc>::from(clock).naive_utc(), chrono::DateTime::<chrono::Utc>::from_timestamp_millis(clock)
.ok_or(UniffiError::TimestampDecodingError)?
.naive_utc(),
) )
.await?, .await?,
) )
} }
pub async fn clear_clocks(&self, universal_id: String) -> Result<(), UniffiError> { pub async fn clear_clocks(&self, universal_id: String) -> Result<()> {
let storage = self.ensure_storage(&universal_id)?; Ok(
Ok(storage.clear_clocks().await?) self
} .inner
} .ensure_storage(universal_id)?
.clear_clocks()
#[uniffi::export] .await?,
pub fn get_db_path(peer: String, space_type: String, id: String) -> Result<String, UniffiError> { )
let options = StorageOptions {
peer,
space_type: SpaceType::from_str(&space_type)?,
id,
};
utils::get_db_path(&options)
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub enum SpaceType {
#[default]
Userspace,
Workspace,
}
impl Display for SpaceType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpaceType::Userspace => write!(f, "userspace"),
SpaceType::Workspace => write!(f, "workspace"),
}
}
}
impl FromStr for SpaceType {
type Err = UniffiError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"userspace" => Self::Userspace,
"workspace" => Self::Workspace,
_ => return Err(UniffiError::InvalidSpaceType(s.to_string())),
})
}
}
pub struct StorageOptions {
pub peer: String,
pub space_type: SpaceType,
pub id: String,
}
pub fn parse_universal_id(id: &str) -> Result<StorageOptions, UniffiError> {
let mut result = StorageOptions {
peer: String::new(),
space_type: SpaceType::default(),
id: String::new(),
};
let mut key = String::new();
let mut value = String::new();
let mut is_in_value = false;
let mut chars = id.chars().peekable();
while let Some(ch) = chars.next() {
if is_in_value {
if ch == ')' && chars.peek() == Some(&';') {
// Store the collected value in the appropriate field
match key.as_str() {
"peer" => result.peer = value.clone(),
"type" => result.space_type = SpaceType::from_str(&value)?,
"id" => result.id = value.clone(),
_ => return Err(UniffiError::InvalidUniversalId(id.to_string())),
}
key.clear();
value.clear();
is_in_value = false;
chars.next(); // Skip the semicolon
continue;
}
value.push(ch);
continue;
}
if ch == '@' {
// Find the position of next '('
let mut temp_chars = chars.clone();
let mut found_paren = false;
let mut key_chars = Vec::new();
while let Some(next_ch) = temp_chars.next() {
if next_ch == '(' {
found_paren = true;
break;
}
key_chars.push(next_ch);
}
// Invalid format if no '(' found or it's immediately after '@'
if !found_paren || key_chars.is_empty() {
return Err(UniffiError::InvalidUniversalId(id.to_string()));
}
key = key_chars.into_iter().collect();
// Advance the original iterator to the position after the key
for _ in 0..key.len() + 1 {
chars.next();
}
is_in_value = true;
} else {
return Err(UniffiError::InvalidUniversalId(id.to_string()));
}
}
// Validate the parsed results
if result.peer.is_empty() || result.id.is_empty() {
return Err(UniffiError::InvalidUniversalId(id.to_string()));
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
// ... existing test functions ...
#[test]
fn test_universal_id() {
let options = StorageOptions {
peer: "123".to_string(),
space_type: SpaceType::Workspace,
id: "456".to_string(),
};
let id = format!(
"@peer({});@type({});@id({});",
options.peer, options.space_type, options.id
);
let result = parse_universal_id(&id).unwrap();
assert_eq!(result.peer, "123");
assert_eq!(result.space_type, SpaceType::Workspace);
assert_eq!(result.id, "456");
}
#[test]
fn test_parse_universal_id_valid_cases() {
let testcases = vec![
"@peer(123);@type(userspace);@id(456);",
"@peer(123);@type(workspace);@id(456);",
"@peer(https://app.affine.pro);@type(userspace);@id(hello:world);",
"@peer(@name);@type(userspace);@id(@id);",
"@peer(@peer(name);@type(userspace);@id(@id);",
];
for id in testcases {
let result = parse_universal_id(id);
assert!(result.is_ok(), "Failed to parse: {}", id);
let parsed = result.unwrap();
assert!(!parsed.peer.is_empty());
assert!(!parsed.id.is_empty());
}
}
#[test]
fn test_parse_universal_id_invalid_cases() {
let testcases = vec![
// invalid space type
"@peer(123);@type(anyspace);@id(456);",
// invalid peer
"@peer(@peer(name););@type(userspace);@id(@id);",
];
for id in testcases {
let result = parse_universal_id(id);
assert!(result.is_err(), "Should have failed to parse: {}", id);
match result {
Err(UniffiError::InvalidUniversalId(_)) => (),
Err(UniffiError::InvalidSpaceType(_)) => (),
_ => panic!("Expected InvalidUniversalId error for: {}", id),
}
}
} }
} }
@@ -1,141 +0,0 @@
use std::fs;
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
use homedir::my_home;
#[cfg(any(target_os = "ios", target_os = "macos"))]
use objc2::rc::autoreleasepool;
#[cfg(any(target_os = "ios", target_os = "macos"))]
use objc2_foundation::{NSFileManager, NSSearchPathDirectory, NSSearchPathDomainMask, NSString};
use crate::{error::UniffiError, SpaceType, StorageOptions};
const DB_FILE_NAME: &str = "storage.db";
#[cfg(any(target_os = "ios", target_os = "macos"))]
pub(crate) fn get_db_path(options: &StorageOptions) -> Result<String, UniffiError> {
let file_manager = unsafe { NSFileManager::defaultManager() };
// equivalent to Swift:
// ```swift
// guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
// return nil
// }
// ```
let urls = unsafe {
file_manager.URLsForDirectory_inDomains(
NSSearchPathDirectory::NSDocumentDirectory,
NSSearchPathDomainMask::NSUserDomainMask,
)
};
let document_directory = urls
.first()
.ok_or(UniffiError::GetUserDocumentDirectoryFailed)?;
let affine_dir = unsafe {
let spaces_dir = match options.space_type {
SpaceType::Userspace => "userspaces",
SpaceType::Workspace => "workspaces",
};
let escaped_peer = escape_filename(&options.peer);
document_directory
.URLByAppendingPathComponent(&NSString::from_str(".affine"))
.and_then(|url| url.URLByAppendingPathComponent(&NSString::from_str(spaces_dir)))
.and_then(|url| url.URLByAppendingPathComponent(&NSString::from_str(&escaped_peer)))
.and_then(|url| url.URLByAppendingPathComponent(&NSString::from_str(&options.id)))
}
.ok_or(UniffiError::ConcatSpaceDirFailed(format!(
"{}:{}:{}",
options.peer, options.space_type, options.id
)))?;
let affine_dir_str = autoreleasepool(|pool| {
Ok::<String, UniffiError>(
unsafe { affine_dir.path() }
.ok_or(UniffiError::GetUserDocumentDirectoryFailed)?
.as_str(pool)
.to_string(),
)
})?;
// Replicate Swift's appending ".affine" subdir, creating it if necessary
fs::create_dir_all(&affine_dir_str)
.map_err(|_| UniffiError::CreateAffineDirFailed(affine_dir_str.clone()))?;
let db_path = autoreleasepool(|pool| {
let db_path =
unsafe { affine_dir.URLByAppendingPathComponent(&NSString::from_str(DB_FILE_NAME)) }.ok_or(
UniffiError::ConcatSpaceDirFailed(format!(
"{}:{}:{}/{DB_FILE_NAME}",
options.peer, options.space_type, options.id
)),
)?;
Ok::<String, UniffiError>(
unsafe { db_path.path() }
.ok_or(UniffiError::GetUserDocumentDirectoryFailed)?
.as_str(pool)
.to_string(),
)
})?;
Ok(db_path)
}
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
pub(crate) fn get_db_path(options: &StorageOptions) -> Result<String, UniffiError> {
let home_dir = my_home()
.map_err(|_| UniffiError::GetUserDocumentDirectoryFailed)?
.ok_or(UniffiError::GetUserDocumentDirectoryFailed)?;
let spaces_dir = match options.space_type {
SpaceType::Userspace => "userspaces",
SpaceType::Workspace => "workspaces",
};
let escaped_peer = escape_filename(&options.peer);
let db_path = home_dir
.join(".affine")
.join(spaces_dir)
.join(&escaped_peer)
.join(&options.id);
fs::create_dir_all(&db_path)
.map_err(|_| UniffiError::CreateAffineDirFailed(db_path.to_string_lossy().to_string()))?;
db_path
.join(DB_FILE_NAME)
.to_str()
.map(|p| p.to_owned())
.ok_or(UniffiError::GetUserDocumentDirectoryFailed)
}
fn escape_filename(name: &str) -> String {
// First replace special chars with '_'
let with_underscores = name.replace(|c: char| "\\/!@#$%^&*()+~`\"':;,?<>|".contains(c), "_");
// Then collapse multiple '_' into single '_'
let mut result = String::with_capacity(with_underscores.len());
let mut last_was_underscore = false;
for c in with_underscores.chars() {
if c == '_' {
if !last_was_underscore {
result.push(c);
}
last_was_underscore = true;
} else {
result.push(c);
last_was_underscore = false;
}
}
// Remove trailing underscore
result.trim_end_matches('_').to_string()
}
#[cfg(all(test, any(target_os = "ios", target_os = "macos")))]
mod tests {
use super::*;
#[test]
fn test_escape_filename() {
assert_eq!(escape_filename("hello@world"), "hello_world");
assert_eq!(escape_filename("test!!file"), "test_file");
assert_eq!(escape_filename("_test_"), "_test"); // Leading underscore preserved
assert_eq!(escape_filename("multi___under"), "multi_under");
assert_eq!(escape_filename("path/to\\file"), "path_to_file");
}
}
+31 -38
View File
@@ -1,41 +1,34 @@
/* auto-generated by NAPI-RS */ /* auto-generated by NAPI-RS */
/* eslint-disable */ /* eslint-disable */
export declare class DocStorage { export declare class DocStoragePool {
constructor(path: string) constructor()
/** Initialize the database and run migrations. */ /** Initialize the database and run migrations. */
connect(): Promise<void> connect(universalId: string, path: string): Promise<void>
close(): Promise<void> disconnect(universalId: string): Promise<void>
get isClosed(): Promise<boolean> setSpaceId(universalId: string, spaceId: string): Promise<void>
/** pushUpdate(universalId: string, docId: string, update: Uint8Array): Promise<Date>
* Flush the WAL file to the database file. getDocSnapshot(universalId: string, docId: string): Promise<DocRecord | null>
* See https://www.sqlite.org/pragma.html#pragma_wal_checkpoint:~:text=PRAGMA%20schema.wal_checkpoint%3B setDocSnapshot(universalId: string, snapshot: DocRecord): Promise<boolean>
*/ getDocUpdates(universalId: string, docId: string): Promise<Array<DocUpdate>>
checkpoint(): Promise<void> markUpdatesMerged(universalId: string, docId: string, updates: Array<Date>): Promise<number>
validate(): Promise<boolean> deleteDoc(universalId: string, docId: string): Promise<void>
setSpaceId(spaceId: string): Promise<void> getDocClocks(universalId: string, after?: Date | undefined | null): Promise<Array<DocClock>>
pushUpdate(docId: string, update: Uint8Array): Promise<Date> getDocClock(universalId: string, docId: string): Promise<DocClock | null>
getDocSnapshot(docId: string): Promise<DocRecord | null> getBlob(universalId: string, key: string): Promise<Blob | null>
setDocSnapshot(snapshot: DocRecord): Promise<boolean> setBlob(universalId: string, blob: SetBlob): Promise<void>
getDocUpdates(docId: string): Promise<Array<DocUpdate>> deleteBlob(universalId: string, key: string, permanently: boolean): Promise<void>
markUpdatesMerged(docId: string, updates: Array<Date>): Promise<number> releaseBlobs(universalId: string): Promise<void>
deleteDoc(docId: string): Promise<void> listBlobs(universalId: string): Promise<Array<ListedBlob>>
getDocClocks(after?: Date | undefined | null): Promise<Array<DocClock>> getPeerRemoteClocks(universalId: string, peer: string): Promise<Array<DocClock>>
getDocClock(docId: string): Promise<DocClock | null> getPeerRemoteClock(universalId: string, peer: string, docId: string): Promise<DocClock>
getBlob(key: string): Promise<Blob | null> setPeerRemoteClock(universalId: string, peer: string, docId: string, clock: Date): Promise<void>
setBlob(blob: SetBlob): Promise<void> getPeerPulledRemoteClocks(universalId: string, peer: string): Promise<Array<DocClock>>
deleteBlob(key: string, permanently: boolean): Promise<void> getPeerPulledRemoteClock(universalId: string, peer: string, docId: string): Promise<DocClock>
releaseBlobs(): Promise<void> setPeerPulledRemoteClock(universalId: string, peer: string, docId: string, clock: Date): Promise<void>
listBlobs(): Promise<Array<ListedBlob>> getPeerPushedClocks(universalId: string, peer: string): Promise<Array<DocClock>>
getPeerRemoteClocks(peer: string): Promise<Array<DocClock>> getPeerPushedClock(universalId: string, peer: string, docId: string): Promise<DocClock>
getPeerRemoteClock(peer: string, docId: string): Promise<DocClock> setPeerPushedClock(universalId: string, peer: string, docId: string, clock: Date): Promise<void>
setPeerRemoteClock(peer: string, docId: string, clock: Date): Promise<void> clearClocks(universalId: string): Promise<void>
getPeerPulledRemoteClocks(peer: string): Promise<Array<DocClock>>
getPeerPulledRemoteClock(peer: string, docId: string): Promise<DocClock>
setPeerPulledRemoteClock(peer: string, docId: string, clock: Date): Promise<void>
getPeerPushedClocks(peer: string): Promise<Array<DocClock>>
getPeerPushedClock(peer: string, docId: string): Promise<DocClock>
setPeerPushedClock(peer: string, docId: string, clock: Date): Promise<void>
clearClocks(): Promise<void>
} }
export declare class SqliteConnection { export declare class SqliteConnection {
@@ -96,14 +89,14 @@ export interface DocClock {
export interface DocRecord { export interface DocRecord {
docId: string docId: string
data: Uint8Array bin: Uint8Array
timestamp: Date timestamp: Date
} }
export interface DocUpdate { export interface DocUpdate {
docId: string docId: string
createdAt: Date timestamp: Date
data: Uint8Array bin: Uint8Array
} }
export interface InsertRow { export interface InsertRow {
+1 -1
View File
@@ -364,7 +364,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`) throw new Error(`Failed to load native binding`)
} }
module.exports.DocStorage = nativeBinding.DocStorage module.exports.DocStoragePool = nativeBinding.DocStoragePool
module.exports.SqliteConnection = nativeBinding.SqliteConnection module.exports.SqliteConnection = nativeBinding.SqliteConnection
module.exports.mintChallengeResponse = nativeBinding.mintChallengeResponse module.exports.mintChallengeResponse = nativeBinding.mintChallengeResponse
module.exports.ValidationResult = nativeBinding.ValidationResult module.exports.ValidationResult = nativeBinding.ValidationResult
@@ -13,8 +13,10 @@ use-as-lib = ["napi-derive/noop", "napi/noop"]
affine_schema = { path = "../schema" } affine_schema = { path = "../schema" }
anyhow = { workspace = true } anyhow = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
dashmap = { workspace = true }
napi = { workspace = true } napi = { workspace = true }
napi-derive = { workspace = true } napi-derive = { workspace = true }
thiserror = { workspace = true }
sqlx = { workspace = true, default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] } sqlx = { workspace = true, default-features = false, features = ["chrono", "macros", "migrate", "runtime-tokio", "sqlite", "tls-rustls"] }
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
+9 -7
View File
@@ -1,18 +1,18 @@
use std::ops::Deref; use std::ops::Deref;
use super::{storage::SqliteDocStorage, Blob, ListedBlob, SetBlob}; use super::{error::Result, storage::SqliteDocStorage, Blob, ListedBlob, SetBlob};
type Result<T> = std::result::Result<T, sqlx::Error>;
impl SqliteDocStorage { impl SqliteDocStorage {
pub async fn get_blob(&self, key: String) -> Result<Option<Blob>> { pub async fn get_blob(&self, key: String) -> Result<Option<Blob>> {
sqlx::query_as!( let result = sqlx::query_as!(
Blob, Blob,
"SELECT key, data, size, mime, created_at FROM blobs WHERE key = ? AND deleted_at IS NULL", "SELECT key, data, size, mime, created_at FROM blobs WHERE key = ? AND deleted_at IS NULL",
key key
) )
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await?;
Ok(result)
} }
pub async fn set_blob(&self, blob: SetBlob) -> Result<()> { pub async fn set_blob(&self, blob: SetBlob) -> Result<()> {
@@ -58,12 +58,14 @@ impl SqliteDocStorage {
} }
pub async fn list_blobs(&self) -> Result<Vec<ListedBlob>> { pub async fn list_blobs(&self) -> Result<Vec<ListedBlob>> {
sqlx::query_as!( let result = sqlx::query_as!(
ListedBlob, ListedBlob,
"SELECT key, size, mime, created_at FROM blobs WHERE deleted_at IS NULL ORDER BY created_at DESC;" "SELECT key, size, mime, created_at FROM blobs WHERE deleted_at IS NULL ORDER BY created_at DESC;"
) )
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await?;
Ok(result)
} }
} }
+29 -22
View File
@@ -3,8 +3,7 @@ use std::ops::Deref;
use chrono::{DateTime, NaiveDateTime}; use chrono::{DateTime, NaiveDateTime};
use sqlx::{QueryBuilder, Row}; use sqlx::{QueryBuilder, Row};
use super::storage::{Result, SqliteDocStorage}; use super::{error::Result, storage::SqliteDocStorage, DocClock, DocRecord, DocUpdate};
use super::{DocClock, DocRecord, DocUpdate};
struct Meta { struct Meta {
space_id: String, space_id: String,
@@ -81,7 +80,7 @@ impl SqliteDocStorage {
Ok(()) => break, Ok(()) => break,
Err(e) => { Err(e) => {
if tried > 10 { if tried > 10 {
return Err(e); return Err(e.into());
} }
// Increment timestamp by 1ms and retry // Increment timestamp by 1ms and retry
@@ -126,13 +125,15 @@ impl SqliteDocStorage {
} }
pub async fn get_doc_snapshot(&self, doc_id: String) -> Result<Option<DocRecord>> { pub async fn get_doc_snapshot(&self, doc_id: String) -> Result<Option<DocRecord>> {
sqlx::query_as!( let result = sqlx::query_as!(
DocRecord, DocRecord,
"SELECT doc_id, data, updated_at as timestamp FROM snapshots WHERE doc_id = ?", "SELECT doc_id, data as bin, updated_at as timestamp FROM snapshots WHERE doc_id = ?",
doc_id doc_id
) )
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await?;
Ok(result)
} }
pub async fn set_doc_snapshot(&self, snapshot: DocRecord) -> Result<bool> { pub async fn set_doc_snapshot(&self, snapshot: DocRecord) -> Result<bool> {
@@ -145,7 +146,7 @@ impl SqliteDocStorage {
WHERE updated_at <= $3;"#, WHERE updated_at <= $3;"#,
) )
.bind(snapshot.doc_id) .bind(snapshot.doc_id)
.bind(snapshot.data.deref()) .bind(snapshot.bin.deref())
.bind(snapshot.timestamp) .bind(snapshot.timestamp)
.execute(&self.pool) .execute(&self.pool)
.await?; .await?;
@@ -154,13 +155,15 @@ impl SqliteDocStorage {
} }
pub async fn get_doc_updates(&self, doc_id: String) -> Result<Vec<DocUpdate>> { pub async fn get_doc_updates(&self, doc_id: String) -> Result<Vec<DocUpdate>> {
sqlx::query_as!( let result = sqlx::query_as!(
DocUpdate, DocUpdate,
"SELECT doc_id, created_at, data FROM updates WHERE doc_id = ?", "SELECT doc_id, created_at as timestamp, data as bin FROM updates WHERE doc_id = ?",
doc_id doc_id
) )
.fetch_all(&self.pool) .fetch_all(&self.pool)
.await .await?;
Ok(result)
} }
pub async fn mark_updates_merged( pub async fn mark_updates_merged(
@@ -204,7 +207,9 @@ impl SqliteDocStorage {
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
tx.commit().await tx.commit().await?;
Ok(())
} }
pub async fn get_doc_clocks(&self, after: Option<NaiveDateTime>) -> Result<Vec<DocClock>> { pub async fn get_doc_clocks(&self, after: Option<NaiveDateTime>) -> Result<Vec<DocClock>> {
@@ -228,13 +233,15 @@ impl SqliteDocStorage {
} }
pub async fn get_doc_clock(&self, doc_id: String) -> Result<Option<DocClock>> { pub async fn get_doc_clock(&self, doc_id: String) -> Result<Option<DocClock>> {
sqlx::query_as!( let result = sqlx::query_as!(
DocClock, DocClock,
"SELECT doc_id, timestamp FROM clocks WHERE doc_id = ?", "SELECT doc_id, timestamp FROM clocks WHERE doc_id = ?",
doc_id doc_id
) )
.fetch_optional(&self.pool) .fetch_optional(&self.pool)
.await .await?;
Ok(result)
} }
} }
@@ -286,7 +293,7 @@ mod tests {
storage storage
.set_doc_snapshot(DocRecord { .set_doc_snapshot(DocRecord {
doc_id: "test".to_string(), doc_id: "test".to_string(),
data: vec![0, 0].into(), bin: vec![0, 0].into(),
timestamp: Utc::now().naive_utc(), timestamp: Utc::now().naive_utc(),
}) })
.await .await
@@ -350,7 +357,7 @@ mod tests {
assert_eq!(result.len(), 4); assert_eq!(result.len(), 4);
assert_eq!( assert_eq!(
result.iter().map(|u| u.data.as_ref()).collect::<Vec<_>>(), result.iter().map(|u| u.bin.as_ref()).collect::<Vec<_>>(),
updates updates
); );
} }
@@ -365,7 +372,7 @@ mod tests {
let snapshot = DocRecord { let snapshot = DocRecord {
doc_id: "test".to_string(), doc_id: "test".to_string(),
data: vec![0, 0].into(), bin: vec![0, 0].into(),
timestamp: Utc::now().naive_utc(), timestamp: Utc::now().naive_utc(),
}; };
@@ -374,7 +381,7 @@ mod tests {
let result = storage.get_doc_snapshot("test".to_string()).await.unwrap(); let result = storage.get_doc_snapshot("test".to_string()).await.unwrap();
assert!(result.is_some()); assert!(result.is_some());
assert_eq!(result.unwrap().data.as_ref(), vec![0, 0]); assert_eq!(result.unwrap().bin.as_ref(), vec![0, 0]);
} }
#[tokio::test] #[tokio::test]
@@ -383,7 +390,7 @@ mod tests {
let snapshot = DocRecord { let snapshot = DocRecord {
doc_id: "test".to_string(), doc_id: "test".to_string(),
data: vec![0, 0].into(), bin: vec![0, 0].into(),
timestamp: Utc::now().naive_utc(), timestamp: Utc::now().naive_utc(),
}; };
@@ -392,11 +399,11 @@ mod tests {
let result = storage.get_doc_snapshot("test".to_string()).await.unwrap(); let result = storage.get_doc_snapshot("test".to_string()).await.unwrap();
assert!(result.is_some()); assert!(result.is_some());
assert_eq!(result.unwrap().data.as_ref(), vec![0, 0]); assert_eq!(result.unwrap().bin.as_ref(), vec![0, 0]);
let snapshot = DocRecord { let snapshot = DocRecord {
doc_id: "test".to_string(), doc_id: "test".to_string(),
data: vec![0, 1].into(), bin: vec![0, 1].into(),
timestamp: DateTime::from_timestamp_millis(Utc::now().timestamp_millis() - 1000) timestamp: DateTime::from_timestamp_millis(Utc::now().timestamp_millis() - 1000)
.unwrap() .unwrap()
.naive_utc(), .naive_utc(),
@@ -408,7 +415,7 @@ mod tests {
let result = storage.get_doc_snapshot("test".to_string()).await.unwrap(); let result = storage.get_doc_snapshot("test".to_string()).await.unwrap();
assert!(result.is_some()); assert!(result.is_some());
assert_eq!(result.unwrap().data.as_ref(), vec![0, 0]); assert_eq!(result.unwrap().bin.as_ref(), vec![0, 0]);
} }
#[tokio::test] #[tokio::test]
@@ -468,7 +475,7 @@ mod tests {
updates updates
.iter() .iter()
.skip(1) .skip(1)
.map(|u| u.created_at) .map(|u| u.timestamp)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
) )
.await .await
@@ -0,0 +1,11 @@
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Sqlite Error: {0}")]
SqlxError(#[from] sqlx::Error),
#[error("Migrate Error: {0}")]
MigrateError(#[from] sqlx::migrate::MigrateError),
#[error("Invalid operation")]
InvalidOperation,
}
+249 -138
View File
@@ -1,11 +1,14 @@
pub mod blob; pub mod blob;
pub mod doc; pub mod doc;
pub mod error;
pub mod pool;
pub mod storage; pub mod storage;
pub mod sync; pub mod sync;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use napi::bindgen_prelude::*; use napi::bindgen_prelude::*;
use napi_derive::napi; use napi_derive::napi;
use pool::SqliteDocStoragePool;
#[cfg(feature = "use-as-lib")] #[cfg(feature = "use-as-lib")]
type Result<T> = anyhow::Result<T>; type Result<T> = anyhow::Result<T>;
@@ -14,13 +17,10 @@ type Result<T> = anyhow::Result<T>;
type Result<T> = napi::Result<T>; type Result<T> = napi::Result<T>;
#[cfg(not(feature = "use-as-lib"))] #[cfg(not(feature = "use-as-lib"))]
fn map_err(err: sqlx::Error) -> Error { impl From<error::Error> for napi::Error {
Error::from(anyhow::Error::from(err)) fn from(err: error::Error) -> Self {
} napi::Error::new(napi::Status::GenericFailure, err.to_string())
}
#[cfg(feature = "use-as-lib")]
fn map_err(err: sqlx::Error) -> anyhow::Error {
anyhow::Error::from(err)
} }
#[cfg(feature = "use-as-lib")] #[cfg(feature = "use-as-lib")]
@@ -32,16 +32,16 @@ pub type Data = Uint8Array;
#[napi(object)] #[napi(object)]
pub struct DocUpdate { pub struct DocUpdate {
pub doc_id: String, pub doc_id: String,
pub created_at: NaiveDateTime, pub timestamp: NaiveDateTime,
#[napi(ts_type = "Uint8Array")] #[napi(ts_type = "Uint8Array")]
pub data: Data, pub bin: Data,
} }
#[napi(object)] #[napi(object)]
pub struct DocRecord { pub struct DocRecord {
pub doc_id: String, pub doc_id: String,
#[napi(ts_type = "Uint8Array")] #[napi(ts_type = "Uint8Array")]
pub data: Data, pub bin: Data,
pub timestamp: NaiveDateTime, pub timestamp: NaiveDateTime,
} }
@@ -79,243 +79,354 @@ pub struct ListedBlob {
} }
#[napi] #[napi]
pub struct DocStorage { pub struct DocStoragePool {
storage: storage::SqliteDocStorage, pool: SqliteDocStoragePool,
} }
#[napi] #[napi]
impl DocStorage { impl DocStoragePool {
#[napi(constructor, async_runtime)] #[napi(constructor, async_runtime)]
pub fn new(path: String) -> Result<Self> { pub fn new() -> Result<Self> {
Ok(Self { Ok(Self {
storage: storage::SqliteDocStorage::new(path), pool: SqliteDocStoragePool::default(),
}) })
} }
#[napi] #[napi]
/// Initialize the database and run migrations. /// Initialize the database and run migrations.
pub async fn connect(&self) -> Result<()> { pub async fn connect(&self, universal_id: String, path: String) -> Result<()> {
self.storage.connect().await.map_err(map_err) self.pool.connect(universal_id, path).await?;
}
#[napi]
pub async fn close(&self) -> Result<()> {
self.storage.close().await;
Ok(()) Ok(())
} }
#[napi(getter)]
pub async fn is_closed(&self) -> Result<bool> {
Ok(self.storage.is_closed())
}
/**
* Flush the WAL file to the database file.
* See https://www.sqlite.org/pragma.html#pragma_wal_checkpoint:~:text=PRAGMA%20schema.wal_checkpoint%3B
*/
#[napi] #[napi]
pub async fn checkpoint(&self) -> Result<()> { pub async fn disconnect(&self, universal_id: String) -> Result<()> {
self.storage.checkpoint().await.map_err(map_err) self.pool.disconnect(universal_id).await?;
Ok(())
} }
#[napi] #[napi]
pub async fn validate(&self) -> Result<bool> { pub async fn set_space_id(&self, universal_id: String, space_id: String) -> Result<()> {
self.storage.validate().await.map_err(map_err)
}
#[napi]
pub async fn set_space_id(&self, space_id: String) -> Result<()> {
self.storage.set_space_id(space_id).await.map_err(map_err)
}
#[napi]
pub async fn push_update(&self, doc_id: String, update: Uint8Array) -> Result<NaiveDateTime> {
self self
.storage .pool
.push_update(doc_id, update) .ensure_storage(universal_id)?
.await .set_space_id(space_id)
.map_err(map_err) .await?;
Ok(())
} }
#[napi] #[napi]
pub async fn get_doc_snapshot(&self, doc_id: String) -> Result<Option<DocRecord>> { pub async fn push_update(
self.storage.get_doc_snapshot(doc_id).await.map_err(map_err) &self,
universal_id: String,
doc_id: String,
update: Uint8Array,
) -> Result<NaiveDateTime> {
Ok(
self
.pool
.ensure_storage(universal_id)?
.push_update(doc_id, update)
.await?,
)
} }
#[napi] #[napi]
pub async fn set_doc_snapshot(&self, snapshot: DocRecord) -> Result<bool> { pub async fn get_doc_snapshot(
self &self,
.storage universal_id: String,
.set_doc_snapshot(snapshot) doc_id: String,
.await ) -> Result<Option<DocRecord>> {
.map_err(map_err) Ok(
self
.pool
.ensure_storage(universal_id)?
.get_doc_snapshot(doc_id)
.await?,
)
} }
#[napi] #[napi]
pub async fn get_doc_updates(&self, doc_id: String) -> Result<Vec<DocUpdate>> { pub async fn set_doc_snapshot(&self, universal_id: String, snapshot: DocRecord) -> Result<bool> {
self.storage.get_doc_updates(doc_id).await.map_err(map_err) Ok(
self
.pool
.ensure_storage(universal_id)?
.set_doc_snapshot(snapshot)
.await?,
)
}
#[napi]
pub async fn get_doc_updates(
&self,
universal_id: String,
doc_id: String,
) -> Result<Vec<DocUpdate>> {
Ok(
self
.pool
.ensure_storage(universal_id)?
.get_doc_updates(doc_id)
.await?,
)
} }
#[napi] #[napi]
pub async fn mark_updates_merged( pub async fn mark_updates_merged(
&self, &self,
universal_id: String,
doc_id: String, doc_id: String,
updates: Vec<NaiveDateTime>, updates: Vec<NaiveDateTime>,
) -> Result<u32> { ) -> Result<u32> {
Ok(
self
.pool
.ensure_storage(universal_id)?
.mark_updates_merged(doc_id, updates)
.await?,
)
}
#[napi]
pub async fn delete_doc(&self, universal_id: String, doc_id: String) -> Result<()> {
self self
.storage .pool
.mark_updates_merged(doc_id, updates) .ensure_storage(universal_id)?
.await .delete_doc(doc_id)
.map_err(map_err) .await?;
Ok(())
} }
#[napi] #[napi]
pub async fn delete_doc(&self, doc_id: String) -> Result<()> { pub async fn get_doc_clocks(
self.storage.delete_doc(doc_id).await.map_err(map_err) &self,
universal_id: String,
after: Option<NaiveDateTime>,
) -> Result<Vec<DocClock>> {
Ok(
self
.pool
.ensure_storage(universal_id)?
.get_doc_clocks(after)
.await?,
)
} }
#[napi] #[napi]
pub async fn get_doc_clocks(&self, after: Option<NaiveDateTime>) -> Result<Vec<DocClock>> { pub async fn get_doc_clock(
self.storage.get_doc_clocks(after).await.map_err(map_err) &self,
universal_id: String,
doc_id: String,
) -> Result<Option<DocClock>> {
Ok(
self
.pool
.ensure_storage(universal_id)?
.get_doc_clock(doc_id)
.await?,
)
} }
#[napi] #[napi]
pub async fn get_doc_clock(&self, doc_id: String) -> Result<Option<DocClock>> { pub async fn get_blob(&self, universal_id: String, key: String) -> Result<Option<Blob>> {
self.storage.get_doc_clock(doc_id).await.map_err(map_err) Ok(
self
.pool
.ensure_storage(universal_id)?
.get_blob(key)
.await?,
)
} }
#[napi] #[napi]
pub async fn get_blob(&self, key: String) -> Result<Option<Blob>> { pub async fn set_blob(&self, universal_id: String, blob: SetBlob) -> Result<()> {
self.storage.get_blob(key).await.map_err(map_err)
}
#[napi]
pub async fn set_blob(&self, blob: SetBlob) -> Result<()> {
self.storage.set_blob(blob).await.map_err(map_err)
}
#[napi]
pub async fn delete_blob(&self, key: String, permanently: bool) -> Result<()> {
self self
.storage .pool
.ensure_storage(universal_id)?
.set_blob(blob)
.await?;
Ok(())
}
#[napi]
pub async fn delete_blob(
&self,
universal_id: String,
key: String,
permanently: bool,
) -> Result<()> {
self
.pool
.ensure_storage(universal_id)?
.delete_blob(key, permanently) .delete_blob(key, permanently)
.await .await?;
.map_err(map_err) Ok(())
} }
#[napi] #[napi]
pub async fn release_blobs(&self) -> Result<()> { pub async fn release_blobs(&self, universal_id: String) -> Result<()> {
self.storage.release_blobs().await.map_err(map_err)
}
#[napi]
pub async fn list_blobs(&self) -> Result<Vec<ListedBlob>> {
self.storage.list_blobs().await.map_err(map_err)
}
#[napi]
pub async fn get_peer_remote_clocks(&self, peer: String) -> Result<Vec<DocClock>> {
self self
.storage .pool
.get_peer_remote_clocks(peer) .ensure_storage(universal_id)?
.await .release_blobs()
.map_err(map_err) .await?;
Ok(())
} }
#[napi] #[napi]
pub async fn get_peer_remote_clock(&self, peer: String, doc_id: String) -> Result<DocClock> { pub async fn list_blobs(&self, universal_id: String) -> Result<Vec<ListedBlob>> {
self Ok(self.pool.ensure_storage(universal_id)?.list_blobs().await?)
.storage }
.get_peer_remote_clock(peer, doc_id)
.await #[napi]
.map_err(map_err) pub async fn get_peer_remote_clocks(
&self,
universal_id: String,
peer: String,
) -> Result<Vec<DocClock>> {
Ok(
self
.pool
.ensure_storage(universal_id)?
.get_peer_remote_clocks(peer)
.await?,
)
}
#[napi]
pub async fn get_peer_remote_clock(
&self,
universal_id: String,
peer: String,
doc_id: String,
) -> Result<DocClock> {
Ok(
self
.pool
.ensure_storage(universal_id)?
.get_peer_remote_clock(peer, doc_id)
.await?,
)
} }
#[napi] #[napi]
pub async fn set_peer_remote_clock( pub async fn set_peer_remote_clock(
&self, &self,
universal_id: String,
peer: String, peer: String,
doc_id: String, doc_id: String,
clock: NaiveDateTime, clock: NaiveDateTime,
) -> Result<()> { ) -> Result<()> {
self self
.storage .pool
.ensure_storage(universal_id)?
.set_peer_remote_clock(peer, doc_id, clock) .set_peer_remote_clock(peer, doc_id, clock)
.await .await?;
.map_err(map_err) Ok(())
} }
#[napi] #[napi]
pub async fn get_peer_pulled_remote_clocks(&self, peer: String) -> Result<Vec<DocClock>> { pub async fn get_peer_pulled_remote_clocks(
self &self,
.storage universal_id: String,
.get_peer_pulled_remote_clocks(peer) peer: String,
.await ) -> Result<Vec<DocClock>> {
.map_err(map_err) Ok(
self
.pool
.ensure_storage(universal_id)?
.get_peer_pulled_remote_clocks(peer)
.await?,
)
} }
#[napi] #[napi]
pub async fn get_peer_pulled_remote_clock( pub async fn get_peer_pulled_remote_clock(
&self, &self,
universal_id: String,
peer: String, peer: String,
doc_id: String, doc_id: String,
) -> Result<DocClock> { ) -> Result<DocClock> {
self Ok(
.storage self
.get_peer_pulled_remote_clock(peer, doc_id) .pool
.await .ensure_storage(universal_id)?
.map_err(map_err) .get_peer_pulled_remote_clock(peer, doc_id)
.await?,
)
} }
#[napi] #[napi]
pub async fn set_peer_pulled_remote_clock( pub async fn set_peer_pulled_remote_clock(
&self, &self,
universal_id: String,
peer: String, peer: String,
doc_id: String, doc_id: String,
clock: NaiveDateTime, clock: NaiveDateTime,
) -> Result<()> { ) -> Result<()> {
self self
.storage .pool
.ensure_storage(universal_id)?
.set_peer_pulled_remote_clock(peer, doc_id, clock) .set_peer_pulled_remote_clock(peer, doc_id, clock)
.await .await?;
.map_err(map_err) Ok(())
} }
#[napi] #[napi]
pub async fn get_peer_pushed_clocks(&self, peer: String) -> Result<Vec<DocClock>> { pub async fn get_peer_pushed_clocks(
self &self,
.storage universal_id: String,
.get_peer_pushed_clocks(peer) peer: String,
.await ) -> Result<Vec<DocClock>> {
.map_err(map_err) Ok(
self
.pool
.ensure_storage(universal_id)?
.get_peer_pushed_clocks(peer)
.await?,
)
} }
#[napi] #[napi]
pub async fn get_peer_pushed_clock(&self, peer: String, doc_id: String) -> Result<DocClock> { pub async fn get_peer_pushed_clock(
self &self,
.storage universal_id: String,
.get_peer_pushed_clock(peer, doc_id) peer: String,
.await doc_id: String,
.map_err(map_err) ) -> Result<DocClock> {
Ok(
self
.pool
.ensure_storage(universal_id)?
.get_peer_pushed_clock(peer, doc_id)
.await?,
)
} }
#[napi] #[napi]
pub async fn set_peer_pushed_clock( pub async fn set_peer_pushed_clock(
&self, &self,
universal_id: String,
peer: String, peer: String,
doc_id: String, doc_id: String,
clock: NaiveDateTime, clock: NaiveDateTime,
) -> Result<()> { ) -> Result<()> {
self self
.storage .pool
.ensure_storage(universal_id)?
.set_peer_pushed_clock(peer, doc_id, clock) .set_peer_pushed_clock(peer, doc_id, clock)
.await .await?;
.map_err(map_err) Ok(())
} }
#[napi] #[napi]
pub async fn clear_clocks(&self) -> Result<()> { pub async fn clear_clocks(&self, universal_id: String) -> Result<()> {
self.storage.clear_clocks().await.map_err(map_err) self
.pool
.ensure_storage(universal_id)?
.clear_clocks()
.await?;
Ok(())
} }
} }
@@ -0,0 +1,54 @@
use dashmap::{mapref::one::RefMut, DashMap, Entry};
use super::{
error::{Error, Result},
storage::SqliteDocStorage,
};
#[derive(Default)]
pub struct SqliteDocStoragePool {
inner: DashMap<String, SqliteDocStorage>,
}
impl SqliteDocStoragePool {
fn get_or_create_storage<'a>(
&'a self,
universal_id: String,
path: &str,
) -> RefMut<'a, String, SqliteDocStorage> {
let entry = self.inner.entry(universal_id);
if let Entry::Occupied(storage) = entry {
return storage.into_ref();
}
let storage = SqliteDocStorage::new(path.to_string());
entry.or_insert(storage)
}
pub fn ensure_storage<'a>(
&'a self,
universal_id: String,
) -> Result<RefMut<'a, String, SqliteDocStorage>> {
let entry = self.inner.entry(universal_id);
if let Entry::Occupied(storage) = entry {
Ok(storage.into_ref())
} else {
Err(Error::InvalidOperation)
}
}
/// Initialize the database and run migrations.
pub async fn connect(&self, universal_id: String, path: String) -> Result<()> {
let storage = self.get_or_create_storage(universal_id.to_owned(), &path);
storage.connect().await?;
Ok(())
}
pub async fn disconnect(&self, universal_id: String) -> Result<()> {
let storage = self.ensure_storage(universal_id.to_owned())?;
storage.close().await;
self.inner.remove(&universal_id);
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More