mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-02 09:59:55 +08:00
feat(core): migrate more pull to realtime (#14936)
#### PR Dependency Tree * **PR #14936** 👈 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 * **Refactor** * Consolidated realtime subscription patterns for consistent, more reliable live updates across comments, notifications, transcription tasks, and embedding progress. * Standardized realtime room naming and subscription keys for deterministic delivery. * **New Features** * Introduced a reusable live-query mechanism powering realtime snapshot + event workflows used by comments, notifications, transcript tasks, and embedding progress. * **Tests** * Added tests covering live-query behavior and deterministic subscription key generation. [](https://app.coderabbit.ai/change-stack/toeverything/AFFiNE/pull/14936) <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import type { RealtimeEvent } from '@affine/realtime';
|
||||
import { getRealtimeInputKey, type RealtimeEvent } from '@affine/realtime';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { RealtimeManager, stableStringify } from '../manager';
|
||||
import { RealtimeManager } from '../manager';
|
||||
|
||||
type Handler = (payload?: unknown) => void;
|
||||
|
||||
@@ -70,16 +70,16 @@ beforeEach(() => {
|
||||
socket.disconnected = false;
|
||||
});
|
||||
|
||||
test('stableStringify is deterministic for realtime subscription inputs', () => {
|
||||
expect(stableStringify({ workspaceId: 'space', docId: 'doc' })).toBe(
|
||||
stableStringify({ docId: 'doc', workspaceId: 'space' })
|
||||
test('getRealtimeInputKey is deterministic for realtime subscription inputs', () => {
|
||||
expect(getRealtimeInputKey({ workspaceId: 'space', docId: 'doc' })).toBe(
|
||||
getRealtimeInputKey({ docId: 'doc', workspaceId: 'space' })
|
||||
);
|
||||
});
|
||||
|
||||
test('stableStringify follows JSON semantics for edge values', () => {
|
||||
expect(stableStringify({ a: undefined })).toBe(stableStringify({}));
|
||||
expect(stableStringify([undefined])).toBe('[null]');
|
||||
expect(stableStringify(new Date('2026-01-02T03:04:05.000Z'))).toBe(
|
||||
test('getRealtimeInputKey follows JSON semantics for edge values', () => {
|
||||
expect(getRealtimeInputKey({ a: undefined })).toBe(getRealtimeInputKey({}));
|
||||
expect(getRealtimeInputKey([undefined])).toBe('[null]');
|
||||
expect(getRealtimeInputKey(new Date('2026-01-02T03:04:05.000Z'))).toBe(
|
||||
'"2026-01-02T03:04:05.000Z"'
|
||||
);
|
||||
});
|
||||
@@ -159,13 +159,13 @@ test('subscribe routes events by topic and stable input key', async () => {
|
||||
|
||||
socket.emit('realtime:event', {
|
||||
topic: 'comment.changed',
|
||||
inputKey: stableStringify({ workspaceId: 'space', docId: 'other' }),
|
||||
inputKey: getRealtimeInputKey({ workspaceId: 'space', docId: 'other' }),
|
||||
sentAt: 1,
|
||||
event: { changed: true },
|
||||
} satisfies RealtimeEvent);
|
||||
socket.emit('realtime:event', {
|
||||
topic: 'comment.changed',
|
||||
inputKey: stableStringify({ workspaceId: 'space', docId: 'doc' }),
|
||||
inputKey: getRealtimeInputKey({ workspaceId: 'space', docId: 'doc' }),
|
||||
sentAt: 2,
|
||||
event: { changed: true },
|
||||
} satisfies RealtimeEvent);
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { RealtimeManager, stableStringify } from './manager';
|
||||
export { RealtimeManager } from './manager';
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
RealtimeTopicInputOf,
|
||||
RealtimeTopicName,
|
||||
} from '@affine/realtime';
|
||||
import { getRealtimeInputKey } from '@affine/realtime';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
import { SocketConnection } from '../impls/cloud/socket';
|
||||
@@ -151,7 +152,7 @@ export class RealtimeManager {
|
||||
this.subscriptions.set(subscriptionId, {
|
||||
topic,
|
||||
input,
|
||||
inputKey: stableStringify(input),
|
||||
inputKey: getRealtimeInputKey(input),
|
||||
subject$,
|
||||
});
|
||||
subscriber.next({
|
||||
@@ -302,35 +303,3 @@ export class RealtimeManager {
|
||||
this.subscriptions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export function stableStringify(value: unknown): string {
|
||||
if (
|
||||
value === undefined ||
|
||||
typeof value === 'function' ||
|
||||
typeof value === 'symbol'
|
||||
) {
|
||||
return 'null';
|
||||
}
|
||||
if (value === null || typeof value !== 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return `[${value.map(stableStringify).join(',')}]`;
|
||||
}
|
||||
if (value instanceof Date) {
|
||||
return JSON.stringify(value.toJSON());
|
||||
}
|
||||
const record = value as Record<string, unknown>;
|
||||
return `{${Object.keys(record)
|
||||
.filter(key => {
|
||||
const property = record[key];
|
||||
return (
|
||||
property !== undefined &&
|
||||
typeof property !== 'function' &&
|
||||
typeof property !== 'symbol'
|
||||
);
|
||||
})
|
||||
.sort()
|
||||
.map(key => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
||||
.join(',')}}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user