feat: improve mobile native impl (#14481)

fix #13529 

#### PR Dependency Tree


* **PR #14481** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Mobile blob caching with file-backed storage for faster loads and
reduced network usage
* Blob decoding with lazy refresh on token-read failures for improved
reliability
  * Full-text search/indexing exposed to mobile apps
* Document sync APIs and peer clock management for robust cross-device
sync

* **Tests**
* Added unit tests covering payload decoding, cache safety, and
concurrency

* **Dependencies**
* Added an LRU cache dependency and a new mobile-shared package for
shared mobile logic
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-21 04:13:24 +08:00
committed by GitHub
parent d8cc0acdd0
commit c9bffc13b5
37 changed files with 2325 additions and 866 deletions
@@ -2,7 +2,7 @@ import type { CrawlResult, DocIndexedClock } from '@affine/nbstore';
export interface Blob {
key: string;
// base64 encoded data
// base64 encoded data, or "__AFFINE_BLOB_FILE__:<absolutePath>" for large blobs
data: string;
mime: string;
size: number;
@@ -41,6 +41,7 @@ export interface NbStorePlugin {
pushUpdate: (options: {
id: string;
docId: string;
// base64 encoded data
data: string;
}) => Promise<{ timestamp: number }>;
getDocSnapshot: (options: { id: string; docId: string }) => Promise<
@@ -55,6 +56,7 @@ export interface NbStorePlugin {
setDocSnapshot: (options: {
id: string;
docId: string;
// base64 encoded data
bin: string;
timestamp: number;
}) => Promise<{ success: boolean }>;
@@ -2,6 +2,10 @@ import {
base64ToUint8Array,
uint8ArrayToBase64,
} from '@affine/core/modules/workspace-engine';
import {
decodePayload,
MOBILE_BLOB_FILE_PREFIX,
} from '@affine/mobile-shared/nbstore/payload';
import {
type BlobRecord,
type CrawlResult,
@@ -132,14 +136,23 @@ export const NbStoreNativeDBApis: NativeDBApis = {
id,
key,
});
return record
? {
data: base64ToUint8Array(record.data),
key: record.key,
mime: record.mime,
createdAt: new Date(record.createdAt),
}
: null;
if (!record) {
return null;
}
let refreshedBlobPromise: ReturnType<typeof NbStore.getBlob> | undefined;
return {
data: await decodePayload(record.data, MOBILE_BLOB_FILE_PREFIX, {
onTokenReadFailure: async () => {
refreshedBlobPromise ??= NbStore.getBlob({ id, key });
return (await refreshedBlobPromise)?.data;
},
}),
key: record.key,
mime: record.mime,
createdAt: new Date(record.createdAt),
};
},
setBlob: async function (id: string, blob: BlobRecord): Promise<void> {
await NbStore.setBlob({