feat(core): improve mobile perf (#15317)

#### PR Dependency Tree


* **PR #15317** 👈

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**
* Virtualized mobile navigation with shell navigation and interactive
swipe menus; coordinated mobile back handling with interactive
phases/state restoration.
* Added shared auth request proxy and message-port based token handling
across mobile and worker flows.
* **Bug Fixes**
  * Hydrated remote worker error stacks for calls and observable errors.
* Improved SQLite FTS/indexer and nbstore optional text handling;
refined docs-search ref parsing and notification loading/retry.
* **Refactor / UX**
* Modal focus-preservation and pointer behavior updates; improved mobile
menu controls and back gesture plugins.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-07-23 00:23:21 +08:00
committed by GitHub
parent 02e75862cc
commit 1d36e2e4b2
160 changed files with 6660 additions and 1890 deletions
@@ -20,6 +20,8 @@ import { createNode } from './node-builder';
import { queryRaw } from './query';
import { getText, tryParseArrayField } from './utils';
const SQLITE_INDEXER_VERSION_OFFSET = 1;
export class SqliteIndexerStorage extends IndexerStorageBase {
static readonly identifier = 'SqliteIndexerStorage';
override readonly recommendRefreshInterval = 30 * 1000; // 5 seconds
@@ -92,7 +94,7 @@ export class SqliteIndexerStorage extends IndexerStorageBase {
`${table}:${field as string}`,
id
);
if (text) {
if (typeof text === 'string' && text.length > 0) {
let values: string[] = [text];
const parsed = tryParseArrayField(text);
if (parsed) {
@@ -129,6 +131,13 @@ export class SqliteIndexerStorage extends IndexerStorageBase {
}
}
}
} else if (text != null && typeof text !== 'string') {
console.warn('[nbstore] invalid indexed aggregate type', {
table,
field: field as string,
id,
type: typeof text,
});
}
}
@@ -244,6 +253,9 @@ export class SqliteIndexerStorage extends IndexerStorageBase {
}
async indexVersion(): Promise<number> {
return this.connection.apis.ftsIndexVersion();
return (
(await this.connection.apis.ftsIndexVersion()) +
SQLITE_INDEXER_VERSION_OFFSET
);
}
}
@@ -0,0 +1,70 @@
import { describe, expect, it, vi } from 'vitest';
import type { NativeDBConnection } from '../db';
import { SqliteIndexerStorage } from '.';
import { createNode } from './node-builder';
import { getText } from './utils';
const query = { type: 'match', field: 'title', match: 'query' } as const;
const connectionWith = (value: unknown) =>
({
apis: {
ftsGetDocument: vi.fn().mockResolvedValue(value),
ftsGetMatches: vi.fn().mockResolvedValue([]),
},
}) as unknown as NativeDBConnection;
describe('sqlite indexer node fields', () => {
it.each([
['string', 'summary', 'summary'],
['singleton array', ['one'], 'one'],
['array', ['one', 'two'], ['one', 'two']],
['serialized array', '["one","two"]', ['one', 'two']],
['malformed array', '[not-json]', '[not-json]'],
['null', null, ''],
['missing', undefined, ''],
['wrong type', 42, ''],
])('isolates %s values', async (_, value, expected) => {
const node = await createNode(
connectionWith(Array.isArray(value) ? getText(value) : value),
'doc',
'doc-id',
1,
{ fields: ['title'] },
query
);
expect(node.fields.title).toEqual(expected);
});
it('does not highlight non-string values', async () => {
const connection = connectionWith({ invalid: true });
const node = await createNode(
connection,
'doc',
'doc-id',
1,
{ highlights: [{ field: 'title', before: '<b>', end: '</b>' }] },
query
);
expect(node.highlights.title).toEqual([]);
expect(connection.apis.ftsGetMatches).not.toHaveBeenCalled();
});
it('isolates wrong aggregate field types', async () => {
const connection = connectionWith(42);
connection.apis.ftsSearch = vi
.fn()
.mockResolvedValue([{ id: 'doc-id', score: 1, terms: [] }]);
const storage = Object.create(
SqliteIndexerStorage.prototype
) as SqliteIndexerStorage;
Object.defineProperty(storage, 'connection', { value: connection });
await expect(
storage.aggregate('doc', query, 'title')
).resolves.toMatchObject({ buckets: [] });
});
});
@@ -20,14 +20,22 @@ export async function createNode(
`${table}:${field as string}`,
id
);
if (text !== null) {
if (typeof text === 'string') {
const parsed = tryParseArrayField(text);
if (parsed) {
fields[field as string] = parsed;
} else {
fields[field as string] = text;
}
} else if (text == null) {
fields[field as string] = '';
} else {
console.warn('[nbstore] invalid indexed field type', {
table,
field: field as string,
id,
type: typeof text,
});
fields[field as string] = '';
}
}
@@ -43,7 +51,7 @@ export async function createNode(
`${table}:${h.field as string}`,
id
);
if (text) {
if (typeof text === 'string' && text.length > 0) {
const queryString = Array.from(queryStrings).join(' ');
const matches = await connection.apis.ftsGetMatches(
`${table}:${h.field as string}`,
@@ -67,6 +75,14 @@ export async function createNode(
highlights[h.field as string] = [];
}
} else {
if (text != null && typeof text !== 'string') {
console.warn('[nbstore] invalid indexed highlight type', {
table,
field: h.field as string,
id,
type: typeof text,
});
}
highlights[h.field as string] = [];
}
}
@@ -2,6 +2,9 @@ export function getText(
val: string | string[] | undefined
): string | undefined {
if (Array.isArray(val)) {
if (val.length === 1) {
return val[0];
}
return JSON.stringify(val);
}
return val;
@@ -568,9 +568,7 @@ export class IndexerSyncImpl implements IndexerSync {
const title = node.fields.title;
return [
node.id,
{
title: typeof title === 'string' ? title : title.at(0),
},
{ title: typeof title === 'string' ? title : undefined },
];
})
);