Compare commits

...

5 Commits

Author SHA1 Message Date
EYHN
96f0f04bf7 fix(ios): send affine version on sign in (#11058) 2025-03-21 13:26:38 +08:00
EYHN
b427a89c9a fix(core): fix awareness send message repeatedly (#10643) 2025-03-10 12:26:57 +08:00
forehalo
00398fc63a fix(server): reschedule busy doc merging 2025-03-03 18:38:05 +08:00
Saul-Mirone
cbef681125 fix: table collab (#10489)
[Screen Recording 2025-02-27 at 20.24.15.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.dev/api/v1/graphite/video/thumbnail/EUhyG5TOGVlHZ0Suk1wH/b3ac681f-a14d-483a-820c-53c584f0fb44.mov" />](https://app.graphite.dev/media/video/EUhyG5TOGVlHZ0Suk1wH/b3ac681f-a14d-483a-820c-53c584f0fb44.mov)
2025-03-03 18:09:29 +08:00
forehalo
bf42a4ddb2 fix(server): limit max batch pulled doc updates 2025-03-03 17:02:48 +08:00
10 changed files with 132 additions and 23 deletions

View File

@@ -31,6 +31,7 @@ type CreateProxyOptions = {
transform?: Transform; transform?: Transform;
onDispose: Slot; onDispose: Slot;
shouldByPassSignal: () => boolean; shouldByPassSignal: () => boolean;
shouldByPassYjs: () => boolean;
byPassSignalUpdate: (fn: () => void) => void; byPassSignalUpdate: (fn: () => void) => void;
stashed: Set<string | number>; stashed: Set<string | number>;
initialized: () => boolean; initialized: () => boolean;
@@ -58,6 +59,7 @@ function createProxy(
const { const {
onDispose, onDispose,
shouldByPassSignal, shouldByPassSignal,
shouldByPassYjs,
byPassSignalUpdate, byPassSignalUpdate,
basePath, basePath,
onChange, onChange,
@@ -141,6 +143,9 @@ function createProxy(
if (isPureObject(value)) { if (isPureObject(value)) {
const syncYMap = () => { const syncYMap = () => {
if (shouldByPassYjs()) {
return;
}
yMap.forEach((_, key) => { yMap.forEach((_, key) => {
if (initialized() && keyWithoutPrefix(key).startsWith(fullPath)) { if (initialized() && keyWithoutPrefix(key).startsWith(fullPath)) {
yMap.delete(key); yMap.delete(key);
@@ -185,7 +190,7 @@ function createProxy(
const yValue = native2Y(value); const yValue = native2Y(value);
const next = transform(firstKey, value, yValue); const next = transform(firstKey, value, yValue);
if (!isStashed && initialized()) { if (!isStashed && initialized() && !shouldByPassYjs()) {
yMap.doc?.transact( yMap.doc?.transact(
() => { () => {
yMap.set(keyWithPrefix(fullPath), yValue); yMap.set(keyWithPrefix(fullPath), yValue);
@@ -238,7 +243,7 @@ function createProxy(
}); });
}; };
if (!isStashed && initialized()) { if (!isStashed && initialized() && !shouldByPassYjs()) {
yMap.doc?.transact( yMap.doc?.transact(
() => { () => {
const fullKey = keyWithPrefix(fullPath); const fullKey = keyWithPrefix(fullPath);
@@ -292,12 +297,17 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
if (this._stashed.has(firstKey)) { if (this._stashed.has(firstKey)) {
return; return;
} }
void keys.reduce((acc, key, index, arr) => { this._updateWithYjsSkip(() => {
if (index === arr.length - 1) { void keys.reduce((acc, key, index, arr) => {
acc[key] = y2Native(value); if (!acc[key] && index !== arr.length - 1) {
} acc[key] = {};
return acc[key] as UnRecord; }
}, proxy as UnRecord); if (index === arr.length - 1) {
acc[key] = y2Native(value);
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
});
return; return;
} }
if (type.action === 'delete') { if (type.action === 'delete') {
@@ -307,12 +317,26 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
if (this._stashed.has(firstKey)) { if (this._stashed.has(firstKey)) {
return; return;
} }
void keys.reduce((acc, key, index, arr) => { this._updateWithYjsSkip(() => {
if (index === arr.length - 1) { void keys.reduce((acc, key, index, arr) => {
delete acc[key]; if (index === arr.length - 1) {
} delete acc[key];
return acc[key] as UnRecord; let i = index - 1;
}, proxy as UnRecord); let curr = acc;
while (i > 0) {
const parentPath = keys.slice(0, i);
const parentKey = keys[i];
const parent = parentPath.reduce((acc, key) => {
return acc[key] as UnRecord;
}, proxy as UnRecord);
deleteEmptyObject(curr, parentKey, parent);
curr = parent;
i--;
}
}
return acc[key] as UnRecord;
}, proxy as UnRecord);
});
return; return;
} }
}); });
@@ -393,6 +417,8 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
return root; return root;
}; };
private _byPassYjs = false;
private readonly _getProxy = ( private readonly _getProxy = (
source: UnRecord, source: UnRecord,
root: UnRecord, root: UnRecord,
@@ -402,6 +428,7 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
onDispose: this._onDispose, onDispose: this._onDispose,
shouldByPassSignal: () => this._skipNext, shouldByPassSignal: () => this._skipNext,
byPassSignalUpdate: this._updateWithSkip, byPassSignalUpdate: this._updateWithSkip,
shouldByPassYjs: () => this._byPassYjs,
basePath: path, basePath: path,
onChange: this._onChange, onChange: this._onChange,
transform: this._transform, transform: this._transform,
@@ -410,6 +437,12 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
}); });
}; };
private readonly _updateWithYjsSkip = (fn: () => void) => {
this._byPassYjs = true;
fn();
this._byPassYjs = false;
};
constructor( constructor(
protected readonly _ySource: YMap<unknown>, protected readonly _ySource: YMap<unknown>,
private readonly _onDispose: Slot, private readonly _onDispose: Slot,
@@ -453,3 +486,9 @@ export class ReactiveFlatYMap extends BaseReactiveYData<
this._stashed.add(prop); this._stashed.add(prop);
}; };
} }
function deleteEmptyObject(obj: UnRecord, key: string, parent: UnRecord): void {
if (Object.keys(obj).length === 0) {
delete parent[key];
}
}

View File

@@ -1,6 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { PrismaClient } from '@prisma/client';
import { OnJob } from '../../base'; import { JobQueue, OnJob } from '../../base';
import { PgWorkspaceDocStorageAdapter } from '../doc'; import { PgWorkspaceDocStorageAdapter } from '../doc';
declare global { declare global {
@@ -14,7 +16,11 @@ declare global {
@Injectable() @Injectable()
export class DocServiceCronJob { export class DocServiceCronJob {
constructor(private readonly workspace: PgWorkspaceDocStorageAdapter) {} constructor(
private readonly workspace: PgWorkspaceDocStorageAdapter,
private readonly prisma: PrismaClient,
private readonly job: JobQueue
) {}
@OnJob('doc.mergePendingDocUpdates') @OnJob('doc.mergePendingDocUpdates')
async mergePendingDocUpdates({ async mergePendingDocUpdates({
@@ -23,4 +29,29 @@ export class DocServiceCronJob {
}: Jobs['doc.mergePendingDocUpdates']) { }: Jobs['doc.mergePendingDocUpdates']) {
await this.workspace.getDoc(workspaceId, docId); await this.workspace.getDoc(workspaceId, docId);
} }
@Cron(CronExpression.EVERY_10_SECONDS)
async schedule() {
const group = await this.prisma.update.groupBy({
by: ['workspaceId', 'id'],
_count: true,
});
for (const update of group) {
if (update._count > 100) {
await this.job.add(
'doc.mergePendingDocUpdates',
{
workspaceId: update.workspaceId,
docId: update.id,
},
{
jobId: `doc:merge-pending-updates:${update.workspaceId}:${update.id}`,
priority: update._count,
delay: 0,
}
);
}
}
}
} }

View File

@@ -92,6 +92,7 @@ export class DocModel extends BaseModel {
orderBy: { orderBy: {
createdAt: 'asc', createdAt: 'asc',
}, },
take: 100,
}); });
return rows.map(r => this.updateToDocRecord(r)); return rows.map(r => this.updateToDocRecord(r));
} }

View File

@@ -47,7 +47,7 @@ export class AwarenessFrontend {
return; return;
} }
applyAwarenessUpdate(awareness, update.bin, origin); applyAwarenessUpdate(awareness, update.bin, uniqueId);
}; };
const handleSyncCollect = () => { const handleSyncCollect = () => {
return Promise.resolve({ return Promise.resolve({

View File

@@ -25,6 +25,7 @@
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 */; };
9DAE9BD92D8D1AB0000C1D5A /* AppConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DAE9BD82D8D1AA9000C1D5A /* AppConfigManager.swift */; };
9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC59422D323EE00027CEBD /* Mutex.swift */; }; 9DEC59432D323EE40027CEBD /* Mutex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DEC59422D323EE00027CEBD /* Mutex.swift */; };
9DFCD1462D27D1D70028C92B /* libaffine_mobile_native.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */; }; 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 */; };
@@ -55,6 +56,7 @@
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>"; };
9DAE9BD82D8D1AA9000C1D5A /* AppConfigManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConfigManager.swift; sourceTree = "<group>"; };
9DEC59422D323EE00027CEBD /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = "<group>"; }; 9DEC59422D323EE00027CEBD /* Mutex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mutex.swift; sourceTree = "<group>"; };
9DFCD1452D27D1D70028C92B /* libaffine_mobile_native.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libaffine_mobile_native.a; 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; };
@@ -169,6 +171,7 @@
9D90BE242CCB9876006677DB /* App */ = { 9D90BE242CCB9876006677DB /* App */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9DAE9BD82D8D1AA9000C1D5A /* AppConfigManager.swift */,
9DEC59422D323EE00027CEBD /* Mutex.swift */, 9DEC59422D323EE00027CEBD /* Mutex.swift */,
9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */, 9D52FC422D26CDB600105D0A /* JSValueContainerExt.swift */,
9D90BE1A2CCB9876006677DB /* Plugins */, 9D90BE1A2CCB9876006677DB /* Plugins */,
@@ -346,6 +349,7 @@
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 */,
9DAE9BD92D8D1AB0000C1D5A /* AppConfigManager.swift in Sources */,
C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */, C4C97C7D2D030BE000BC2AD1 /* affine_mobile_nativeFFI.h in Sources */,
9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */, 9D5622962D64A6A5009F1BE4 /* AuthPlugin.swift in Sources */,
C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */, C4C97C7E2D030BE000BC2AD1 /* affine_mobile_nativeFFI.modulemap in Sources */,

View File

@@ -0,0 +1,20 @@
import Foundation
final class AppConfigManager {
struct AppConfig: Decodable {
let affineVersion: String
}
static var affineVersion: String? = nil
static func getAffineVersion() -> String {
if affineVersion == nil {
let file = Bundle(for: AppConfigManager.self).url(forResource: "capacitor.config", withExtension: "json")!
let data = try! Data(contentsOf: file)
let config = try! JSONDecoder().decode(AppConfig.self, from: data)
affineVersion = config.affineVersion
}
return affineVersion!
}
}

View File

@@ -80,7 +80,7 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/auth/sign-in", headers: [ let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/auth/sign-in", headers: [
"x-captcha-token": verifyToken, "x-captcha-token": verifyToken,
"x-captcha-challenge": challenge, "x-captcha-challenge": challenge
], body: ["email": email, "password": password]) ], body: ["email": email, "password": password])
if response.statusCode >= 400 { if response.statusCode >= 400 {
@@ -155,6 +155,7 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(body!) request.httpBody = try JSONEncoder().encode(body!)
} }
request.setValue(AppConfigManager.getAffineVersion(), forHTTPHeaderField: "x-affine-version")
request.timeoutInterval = 10 // time out 10s request.timeoutInterval = 10 // time out 10s
let (data, response) = try await URLSession.shared.data(for: request); let (data, response) = try await URLSession.shared.data(for: request);

View File

@@ -1,11 +1,11 @@
PODS: PODS:
- Capacitor (7.0.1): - Capacitor (7.1.0):
- CapacitorCordova - CapacitorCordova
- CapacitorApp (7.0.0): - CapacitorApp (7.0.0):
- Capacitor - Capacitor
- CapacitorBrowser (7.0.0): - CapacitorBrowser (7.0.0):
- Capacitor - Capacitor
- CapacitorCordova (7.0.1) - CapacitorCordova (7.1.0)
- CapacitorHaptics (7.0.0): - CapacitorHaptics (7.0.0):
- Capacitor - Capacitor
- CapacitorKeyboard (7.0.0): - CapacitorKeyboard (7.0.0):
@@ -40,10 +40,10 @@ EXTERNAL SOURCES:
:path: "../../../../../node_modules/@capacitor/keyboard" :path: "../../../../../node_modules/@capacitor/keyboard"
SPEC CHECKSUMS: SPEC CHECKSUMS:
Capacitor: 23fff43571a4d1e3ee7d67b5a3588c6e757c2913 Capacitor: 68ff8eabbcce387e69767c13b5fbcc1c5399eabc
CapacitorApp: 45cb7cbef4aa380b9236fd6980033eb5cde6fcd2 CapacitorApp: 45cb7cbef4aa380b9236fd6980033eb5cde6fcd2
CapacitorBrowser: 352a66541b15ceadae1d703802b11979023705e3 CapacitorBrowser: 352a66541b15ceadae1d703802b11979023705e3
CapacitorCordova: 63d476958d5022d76f197031e8b7ea3519988c64 CapacitorCordova: 866217f32c1d25b326c568a10ea3ed0c36b13e29
CapacitorHaptics: 1fba3e460e7614349c6d5f868b1fccdc5c87b66d CapacitorHaptics: 1fba3e460e7614349c6d5f868b1fccdc5c87b66d
CapacitorKeyboard: 2c26c6fccde35023c579fc37d4cae6326d5e6343 CapacitorKeyboard: 2c26c6fccde35023c579fc37d4cae6326d5e6343
CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483

View File

@@ -1,10 +1,22 @@
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import type { CapacitorConfig } from '@capacitor/cli'; import type { CapacitorConfig } from '@capacitor/cli';
import { KeyboardResize } from '@capacitor/keyboard'; import { KeyboardResize } from '@capacitor/keyboard';
const config: CapacitorConfig = { const packageJson = JSON.parse(
readFileSync(resolve(__dirname, './package.json'), 'utf-8')
);
interface AppConfig {
affineVersion: string;
}
const config: CapacitorConfig & AppConfig = {
appId: 'app.affine.pro', appId: 'app.affine.pro',
appName: 'AFFiNE', appName: 'AFFiNE',
webDir: 'dist', webDir: 'dist',
affineVersion: packageJson.version,
ios: { ios: {
path: '.', path: '.',
webContentsDebuggingEnabled: true, webContentsDebuggingEnabled: true,

View File

@@ -7,6 +7,7 @@
"scripts": { "scripts": {
"build": "affine bundle", "build": "affine bundle",
"dev": "affine bundle --dev", "dev": "affine bundle --dev",
"xcode": "cap open ios",
"sync": "cap sync", "sync": "cap sync",
"sync:dev": "CAP_SERVER_URL=http://localhost:8080 cap sync" "sync:dev": "CAP_SERVER_URL=http://localhost:8080 cap sync"
}, },