mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-31 17:19:56 +08:00
feat(editor): migrate typst mermaid to native (#14499)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Native/WASM Mermaid and Typst SVG preview rendering on desktop and mobile, plus cross-platform Preview plugin integrations. * **Improvements** * Centralized, sanitized rendering bridge with automatic Typst font-directory handling and configurable native renderer selection. * More consistent and robust error serialization and worker-backed preview flows for improved stability and performance. * **Tests** * Extensive unit and integration tests for preview rendering, font discovery, sanitization, and error serialization. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -10,6 +10,7 @@ interface TestOps extends OpSchema {
|
||||
add: [{ a: number; b: number }, number];
|
||||
bin: [Uint8Array, Uint8Array];
|
||||
sub: [Uint8Array, number];
|
||||
init: [{ fastText?: boolean } | undefined, { ok: true }];
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
@@ -84,6 +85,55 @@ describe('op client', () => {
|
||||
expect(data.byteLength).toBe(0);
|
||||
});
|
||||
|
||||
it('should send optional payload call with abort signal', async ctx => {
|
||||
const abortController = new AbortController();
|
||||
const result = ctx.producer.call(
|
||||
'init',
|
||||
{ fastText: true },
|
||||
abortController.signal
|
||||
);
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "init:1",
|
||||
"name": "init",
|
||||
"payload": {
|
||||
"fastText": true,
|
||||
},
|
||||
"type": "call",
|
||||
}
|
||||
`);
|
||||
|
||||
ctx.handlers.return({
|
||||
type: 'return',
|
||||
id: 'init:1',
|
||||
data: { ok: true },
|
||||
});
|
||||
|
||||
await expect(result).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('should send undefined payload for optional input call', async ctx => {
|
||||
const result = ctx.producer.call('init', undefined);
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchInlineSnapshot(`
|
||||
{
|
||||
"id": "init:1",
|
||||
"name": "init",
|
||||
"payload": undefined,
|
||||
"type": "call",
|
||||
}
|
||||
`);
|
||||
|
||||
ctx.handlers.return({
|
||||
type: 'return',
|
||||
id: 'init:1',
|
||||
data: { ok: true },
|
||||
});
|
||||
|
||||
await expect(result).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it('should cancel call', async ctx => {
|
||||
const promise = ctx.producer.call('add', { a: 1, b: 2 });
|
||||
|
||||
|
||||
@@ -40,18 +40,14 @@ describe('op consumer', () => {
|
||||
it('should throw if no handler registered', async ctx => {
|
||||
ctx.handlers.call({ type: 'call', id: 'add:1', name: 'add', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
expect(ctx.postMessage.mock.lastCall).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"error": {
|
||||
"message": "Handler for operation [add] is not registered.",
|
||||
"name": "Error",
|
||||
},
|
||||
"id": "add:1",
|
||||
"type": "return",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(ctx.postMessage.mock.lastCall?.[0]).toMatchObject({
|
||||
type: 'return',
|
||||
id: 'add:1',
|
||||
error: {
|
||||
message: 'Handler for operation [add] is not registered.',
|
||||
name: 'Error',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle call message', async ctx => {
|
||||
@@ -73,6 +69,38 @@ describe('op consumer', () => {
|
||||
`);
|
||||
});
|
||||
|
||||
it('should serialize string errors with message', async ctx => {
|
||||
ctx.consumer.register('any', () => {
|
||||
throw 'worker panic';
|
||||
});
|
||||
|
||||
ctx.handlers.call({ type: 'call', id: 'any:1', name: 'any', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
|
||||
expect(ctx.postMessage.mock.calls[0][0]).toMatchObject({
|
||||
type: 'return',
|
||||
id: 'any:1',
|
||||
error: {
|
||||
name: 'Error',
|
||||
message: 'worker panic',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should serialize plain object errors with fallback message', async ctx => {
|
||||
ctx.consumer.register('any', () => {
|
||||
throw { reason: 'panic', code: 'E_PANIC' };
|
||||
});
|
||||
|
||||
ctx.handlers.call({ type: 'call', id: 'any:1', name: 'any', payload: {} });
|
||||
await vi.advanceTimersToNextTimerAsync();
|
||||
|
||||
const message = ctx.postMessage.mock.calls[0][0]?.error?.message;
|
||||
expect(typeof message).toBe('string');
|
||||
expect(message).toContain('"reason":"panic"');
|
||||
expect(message).toContain('"code":"E_PANIC"');
|
||||
});
|
||||
|
||||
it('should handle cancel message', async ctx => {
|
||||
ctx.consumer.register('add', ({ a, b }, { signal }) => {
|
||||
const { reject, resolve, promise } = Promise.withResolvers<number>();
|
||||
|
||||
@@ -16,6 +16,96 @@ import {
|
||||
} from './message';
|
||||
import type { OpInput, OpNames, OpOutput, OpSchema } from './types';
|
||||
|
||||
const SERIALIZABLE_ERROR_FIELDS = [
|
||||
'name',
|
||||
'message',
|
||||
'code',
|
||||
'type',
|
||||
'status',
|
||||
'data',
|
||||
'stacktrace',
|
||||
] as const;
|
||||
|
||||
type SerializableErrorShape = Partial<
|
||||
Record<(typeof SERIALIZABLE_ERROR_FIELDS)[number], unknown>
|
||||
> & {
|
||||
name?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
function getFallbackErrorMessage(error: unknown): string {
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof error === 'number' ||
|
||||
typeof error === 'boolean' ||
|
||||
typeof error === 'bigint' ||
|
||||
typeof error === 'symbol'
|
||||
) {
|
||||
return String(error);
|
||||
}
|
||||
|
||||
if (error === null || error === undefined) {
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
try {
|
||||
const jsonMessage = JSON.stringify(error);
|
||||
if (jsonMessage && jsonMessage !== '{}') {
|
||||
return jsonMessage;
|
||||
}
|
||||
} catch {
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
return 'Unknown error';
|
||||
}
|
||||
|
||||
function serializeError(error: unknown): Error {
|
||||
const valueToPick =
|
||||
error && typeof error === 'object'
|
||||
? error
|
||||
: ({} as Record<string, unknown>);
|
||||
const serialized = pick(
|
||||
valueToPick,
|
||||
SERIALIZABLE_ERROR_FIELDS
|
||||
) as SerializableErrorShape;
|
||||
|
||||
if (!serialized.message || typeof serialized.message !== 'string') {
|
||||
serialized.message = getFallbackErrorMessage(error);
|
||||
}
|
||||
|
||||
if (!serialized.name || typeof serialized.name !== 'string') {
|
||||
if (error instanceof Error && error.name) {
|
||||
serialized.name = error.name;
|
||||
} else if (error && typeof error === 'object') {
|
||||
const constructorName = error.constructor?.name;
|
||||
serialized.name =
|
||||
typeof constructorName === 'string' && constructorName.length > 0
|
||||
? constructorName
|
||||
: 'Error';
|
||||
} else {
|
||||
serialized.name = 'Error';
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!serialized.stacktrace &&
|
||||
error instanceof Error &&
|
||||
typeof error.stack === 'string'
|
||||
) {
|
||||
serialized.stacktrace = error.stack;
|
||||
}
|
||||
|
||||
return serialized as Error;
|
||||
}
|
||||
|
||||
interface OpCallContext {
|
||||
signal: AbortSignal;
|
||||
}
|
||||
@@ -71,15 +161,7 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
|
||||
this.port.postMessage({
|
||||
type: 'return',
|
||||
id: msg.id,
|
||||
error: pick(error, [
|
||||
'name',
|
||||
'message',
|
||||
'code',
|
||||
'type',
|
||||
'status',
|
||||
'data',
|
||||
'stacktrace',
|
||||
]),
|
||||
error: serializeError(error),
|
||||
} satisfies ReturnMessage);
|
||||
},
|
||||
complete: () => {
|
||||
@@ -109,15 +191,7 @@ export class OpConsumer<Ops extends OpSchema> extends AutoMessageHandler {
|
||||
this.port.postMessage({
|
||||
type: 'error',
|
||||
id: msg.id,
|
||||
error: pick(error, [
|
||||
'name',
|
||||
'message',
|
||||
'code',
|
||||
'type',
|
||||
'status',
|
||||
'data',
|
||||
'stacktrace',
|
||||
]),
|
||||
error: serializeError(error),
|
||||
} satisfies SubscriptionErrorMessage);
|
||||
},
|
||||
complete: () => {
|
||||
|
||||
@@ -12,7 +12,16 @@ export interface OpSchema {
|
||||
[key: string]: [any, any?];
|
||||
}
|
||||
|
||||
type RequiredInput<In> = In extends void ? [] : In extends never ? [] : [In];
|
||||
type IsAny<T> = 0 extends 1 & T ? true : false;
|
||||
|
||||
type RequiredInput<In> =
|
||||
IsAny<In> extends true
|
||||
? [In]
|
||||
: [In] extends [never]
|
||||
? []
|
||||
: [In] extends [void]
|
||||
? []
|
||||
: [In];
|
||||
|
||||
export type OpNames<T extends OpSchema> = ValuesOf<KeyToKey<T>>;
|
||||
export type OpInput<
|
||||
|
||||
Reference in New Issue
Block a user