Synchronizer较为完整的实现

This commit is contained in:
Xu Chang 2024-03-01 16:22:04 +08:00
parent a806e0638a
commit b12f04c523
7 changed files with 183 additions and 263 deletions

2
lib/AppLoader.d.ts vendored
View File

@ -12,7 +12,7 @@ export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt exten
private aspectDict; private aspectDict;
private externalDependencies; private externalDependencies;
protected dataSubscriber?: DataSubscriber<ED, Cxt>; protected dataSubscriber?: DataSubscriber<ED, Cxt>;
protected synchronizer?: Synchronizer<ED, Cxt>; protected synchronizers?: Synchronizer<ED, Cxt>[];
protected contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>; protected contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>;
private requireSth; private requireSth;
protected makeContext(cxtStr?: string, headers?: IncomingHttpHeaders): Promise<Cxt>; protected makeContext(cxtStr?: string, headers?: IncomingHttpHeaders): Promise<Cxt>;

View File

@ -21,7 +21,7 @@ class AppLoader extends types_1.AppLoader {
aspectDict; aspectDict;
externalDependencies; externalDependencies;
dataSubscriber; dataSubscriber;
synchronizer; synchronizers;
contextBuilder; contextBuilder;
requireSth(filePath) { requireSth(filePath) {
const depFilePath = (0, path_1.join)(this.path, filePath); const depFilePath = (0, path_1.join)(this.path, filePath);
@ -101,10 +101,10 @@ class AppLoader extends types_1.AppLoader {
const dbConfigFile = (0, path_1.join)(this.path, 'configuration', 'mysql.json'); const dbConfigFile = (0, path_1.join)(this.path, 'configuration', 'mysql.json');
const dbConfig = require(dbConfigFile); const dbConfig = require(dbConfigFile);
const syncConfigFile = (0, path_1.join)(this.path, 'lib', 'configuration', 'sync.js'); const syncConfigFile = (0, path_1.join)(this.path, 'lib', 'configuration', 'sync.js');
const syncConfig = (0, fs_1.existsSync)(syncConfigFile) && require(syncConfigFile).default; const syncConfigs = (0, fs_1.existsSync)(syncConfigFile) && require(syncConfigFile).default;
return { return {
dbConfig: dbConfig, dbConfig: dbConfig,
syncConfig: syncConfig, syncConfigs: syncConfigs,
}; };
} }
constructor(path, contextBuilder, ns, nsServer) { constructor(path, contextBuilder, ns, nsServer) {
@ -149,70 +149,20 @@ class AppLoader extends types_1.AppLoader {
adTriggers.forEach((trigger) => this.registerTrigger(trigger)); adTriggers.forEach((trigger) => this.registerTrigger(trigger));
checkers.forEach((checker) => this.dbStore.registerChecker(checker)); checkers.forEach((checker) => this.dbStore.registerChecker(checker));
adCheckers.forEach((checker) => this.dbStore.registerChecker(checker)); adCheckers.forEach((checker) => this.dbStore.registerChecker(checker));
if (this.synchronizer) { if (this.synchronizers) {
// 同步数据到远端结点通过commit trigger来完成 // 同步数据到远端结点通过commit trigger来完成
const syncTriggers = this.synchronizer.getSyncTriggers(); for (const synchronizer of this.synchronizers) {
syncTriggers.forEach((trigger) => this.registerTrigger(trigger)); const syncTriggers = synchronizer.getSyncTriggers();
syncTriggers.forEach((trigger) => this.registerTrigger(trigger));
}
} }
} }
async mount(initialize) { async mount(initialize) {
const { path } = this; const { path } = this;
if (!initialize) { if (!initialize) {
const { dbConfig, syncConfig } = this.getConfiguration(); const { syncConfigs } = this.getConfiguration();
if (syncConfig) { if (syncConfigs) {
const { self, remotes } = syncConfig; this.synchronizers = syncConfigs.map(config => new Synchronizer_1.default(config, this.dbStore.getSchema()));
const { getSelfEncryptInfo, ...restSelf } = self;
this.synchronizer = new Synchronizer_1.default({
self: {
// entity: self.entity,
getSelfEncryptInfo: async () => {
const context = await this.contextBuilder()(this.dbStore);
await context.begin();
try {
const result = await self.getSelfEncryptInfo(context);
await context.commit();
return result;
}
catch (err) {
await context.rollback();
throw err;
}
},
...restSelf
},
remotes: remotes.map((r) => {
const { getPushInfo, getPullInfo, ...rest } = r;
return {
getRemotePushInfo: async (id) => {
const context = await this.contextBuilder()(this.dbStore);
await context.begin();
try {
const result = await getPushInfo(id, context);
await context.commit();
return result;
}
catch (err) {
await context.rollback();
throw err;
}
},
getRemotePullInfo: async (userId) => {
const context = await this.contextBuilder()(this.dbStore);
await context.begin();
try {
const result = await getPullInfo(userId, context);
await context.commit();
return result;
}
catch (err) {
await context.rollback();
throw err;
}
},
...rest,
};
})
}, this.dbStore.getSchema());
} }
this.initTriggers(); this.initTriggers();
} }
@ -325,9 +275,11 @@ class AppLoader extends types_1.AppLoader {
transformEndpointItem(router, item); transformEndpointItem(router, item);
} }
} }
if (this.synchronizer) { if (this.synchronizers) {
const syncEp = this.synchronizer.getSelfEndpoint(); this.synchronizers.forEach((synchronizer) => {
transformEndpointItem(syncEp.name, syncEp); const syncEp = synchronizer.getSelfEndpoint();
transformEndpointItem(syncEp.name, syncEp);
});
} }
return endPointRouters; return endPointRouters;
} }

View File

@ -1,8 +1,7 @@
import { EntityDict, StorageSchema, EndpointItem } from 'oak-domain/lib/types'; import { EntityDict, StorageSchema, EndpointItem, SyncConfig } from 'oak-domain/lib/types';
import { VolatileTrigger } from 'oak-domain/lib/types/Trigger'; import { VolatileTrigger } from 'oak-domain/lib/types/Trigger';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext'; import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
import { SyncConfigWrapper } from './types/Sync';
export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> { export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> {
private config; private config;
private schema; private schema;
@ -27,12 +26,11 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
private pushOper; private pushOper;
private getSelfEncryptInfo; private getSelfEncryptInfo;
private makeCreateOperTrigger; private makeCreateOperTrigger;
constructor(config: SyncConfigWrapper<ED, Cxt>, schema: StorageSchema<ED>); constructor(config: SyncConfig<ED, Cxt>, schema: StorageSchema<ED>);
/** /**
* sync的定义 commit triggers * sync的定义 commit triggers
* @returns * @returns
*/ */
getSyncTriggers(): VolatileTrigger<ED, keyof ED, Cxt>[]; getSyncTriggers(): VolatileTrigger<ED, keyof ED, Cxt>[];
private checkOperationConsistent;
getSelfEndpoint(): EndpointItem<ED, Cxt>; getSelfEndpoint(): EndpointItem<ED, Cxt>;
} }

View File

@ -6,7 +6,8 @@ const assert_1 = tslib_1.__importDefault(require("assert"));
const path_1 = require("path"); const path_1 = require("path");
const lodash_1 = require("oak-domain/lib/utils/lodash"); const lodash_1 = require("oak-domain/lib/utils/lodash");
const filter_1 = require("oak-domain/lib/store/filter"); const filter_1 = require("oak-domain/lib/store/filter");
const OAK_SYNC_HEADER_ITEM = 'oak-sync-remote-id'; const OAK_SYNC_HEADER_ENTITY = 'oak-sync-entity';
const OAK_SYNC_HEADER_ENTITYID = 'oak-sync-entity-id';
class Synchronizer { class Synchronizer {
config; config;
schema; schema;
@ -19,7 +20,7 @@ class Synchronizer {
* @param channel * @param channel
* @param retry * @param retry
*/ */
async pushOnChannel(channel, retry) { async pushOnChannel(remoteEntity, remoteEntityId, context, channel, retry) {
const { queue, api, nextPushTimestamp } = channel; const { queue, api, nextPushTimestamp } = channel;
(0, assert_1.default)(nextPushTimestamp); (0, assert_1.default)(nextPushTimestamp);
// 失败重试的间隔失败次数多了应当适当延长最多延长到1024秒 // 失败重试的间隔失败次数多了应当适当延长最多延长到1024秒
@ -31,18 +32,20 @@ class Synchronizer {
let json; let json;
try { try {
// todo 加密 // todo 加密
const selfEncryptInfo = await this.getSelfEncryptInfo(); const selfEncryptInfo = await this.getSelfEncryptInfo(context);
console.log('向远端结点sync数据', api, JSON.stringify(opers)); console.log('向远端结点sync数据', api, JSON.stringify(opers));
const res = await fetch(api, { const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
const res = await fetch(finalApi, {
method: 'post', method: 'post',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
[OAK_SYNC_HEADER_ITEM]: selfEncryptInfo.id, [OAK_SYNC_HEADER_ENTITY]: remoteEntity,
[OAK_SYNC_HEADER_ENTITYID]: remoteEntityId,
}, },
body: JSON.stringify(opers), body: JSON.stringify(opers),
}); });
if (res.status !== 200) { if (res.status !== 200) {
throw new Error(`sync数据时访问api「${api}」的结果不是200。「${res.status}`); throw new Error(`sync数据时访问api「${finalApi}」的结果不是200。「${res.status}`);
} }
json = await res.json(); json = await res.json();
} }
@ -74,7 +77,7 @@ class Synchronizer {
const interval = Math.max(0, channel.nextPushTimestamp - Date.now()); const interval = Math.max(0, channel.nextPushTimestamp - Date.now());
const retry2 = needRetry ? (typeof retry === 'number' ? retry + 1 : 1) : undefined; const retry2 = needRetry ? (typeof retry === 'number' ? retry + 1 : 1) : undefined;
console.log('need retry', retry2); console.log('need retry', retry2);
setTimeout(() => this.pushOnChannel(channel, retry2), interval); setTimeout(() => this.pushOnChannel(remoteEntity, remoteEntityId, context, channel, retry2), interval);
} }
else { else {
channel.handler = undefined; channel.handler = undefined;
@ -90,7 +93,7 @@ class Synchronizer {
* *
* 其实这里还无法严格保证先产生的oper一定先到达被推送因为volatile trigger是在事务提交后再发生的但这种情况在目前应该跑不出来在实际执行oper的时候assert掉先by Xc 20240226 * 其实这里还无法严格保证先产生的oper一定先到达被推送因为volatile trigger是在事务提交后再发生的但这种情况在目前应该跑不出来在实际执行oper的时候assert掉先by Xc 20240226
*/ */
async pushOper(oper, userId, url, endpoint, nextPushTimestamp) { async pushOper(context, oper, userId, url, endpoint, remoteEntity, remoteEntityId, nextPushTimestamp) {
if (!this.remotePushChannel[userId]) { if (!this.remotePushChannel[userId]) {
this.remotePushChannel[userId] = { this.remotePushChannel[userId] = {
api: (0, path_1.join)(url, 'endpoint', endpoint), api: (0, path_1.join)(url, 'endpoint', endpoint),
@ -125,7 +128,7 @@ class Synchronizer {
if (!channel.handler) { if (!channel.handler) {
channel.nextPushTimestamp = nextPushTimestamp2; channel.nextPushTimestamp = nextPushTimestamp2;
channel.handler = setTimeout(async () => { channel.handler = setTimeout(async () => {
await this.pushOnChannel(channel); await this.pushOnChannel(remoteEntity, remoteEntityId, context, channel);
}, nextPushTimestamp2 - now); }, nextPushTimestamp2 - now);
} }
else if (channel.nextPushTimestamp && channel.nextPushTimestamp > nextPushTimestamp2) { else if (channel.nextPushTimestamp && channel.nextPushTimestamp > nextPushTimestamp2) {
@ -139,11 +142,11 @@ class Synchronizer {
console.warn('在sync数据时遇到了重复推送的oper', JSON.stringify(oper), userId, url); console.warn('在sync数据时遇到了重复推送的oper', JSON.stringify(oper), userId, url);
} }
} }
async getSelfEncryptInfo() { async getSelfEncryptInfo(context) {
if (this.selfEncryptInfo) { if (this.selfEncryptInfo) {
return this.selfEncryptInfo; return this.selfEncryptInfo;
} }
this.selfEncryptInfo = await this.config.self.getSelfEncryptInfo(); this.selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
return this.selfEncryptInfo; return this.selfEncryptInfo;
} }
makeCreateOperTrigger() { makeCreateOperTrigger() {
@ -152,10 +155,10 @@ class Synchronizer {
// 根据remotes定义建立从entity到需要同步的远端结点信息的Map // 根据remotes定义建立从entity到需要同步的远端结点信息的Map
const pushAccessMap = {}; const pushAccessMap = {};
remotes.forEach((remote) => { remotes.forEach((remote) => {
const { getRemotePushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote, entitySelf } = remote; const { getPushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote } = remote;
if (pushEntityDefs) { if (pushEntityDefs) {
const pushEntities = []; const pushEntities = [];
const endpoint2 = (0, path_1.join)(endpoint || 'sync', entitySelf || self.entitySelf); const endpoint2 = (0, path_1.join)(endpoint || 'sync', self.entity);
for (const def of pushEntityDefs) { for (const def of pushEntityDefs) {
const { path, relationName, recursive, entity, actions, onSynchronized } = def; const { path, relationName, recursive, entity, actions, onSynchronized } = def;
pushEntities.push(entity); pushEntities.push(entity);
@ -168,15 +171,21 @@ class Synchronizer {
}, recursive) : (0, relationPath_1.destructDirectPath)(this.schema, entity, path2, recursive); }, recursive) : (0, relationPath_1.destructDirectPath)(this.schema, entity, path2, recursive);
const groupByUsers = (rows) => { const groupByUsers = (rows) => {
const userRowDict = {}; const userRowDict = {};
rows.filter((row) => { rows.forEach((row) => {
const userIds = getData(row)?.map(ele => ele.userId); const goals = getData(row);
if (userIds) { if (goals) {
userIds.forEach((userId) => { goals.forEach(({ entity, entityId, userId }) => {
if (userRowDict[userId]) { if (userRowDict[userId]) {
userRowDict[userId].push(row.id); // 逻辑上来说同一个userId其关联的entity和entityId必然相同这个entity/entityId代表了对方
(0, assert_1.default)(userRowDict[userId].entity === entity && userRowDict[userId].entityId === entityId);
userRowDict[userId].rowIds.push(row.id);
} }
else { else {
userRowDict[userId] = [row.id]; userRowDict[userId] = {
entity,
entityId,
rowIds: [row.id],
};
} }
}); });
} }
@ -187,7 +196,7 @@ class Synchronizer {
pushAccessMap[entity] = [{ pushAccessMap[entity] = [{
projection, projection,
groupByUsers, groupByUsers,
getRemotePushInfo, getRemotePushInfo: getPushInfo,
endpoint: endpoint2, endpoint: endpoint2,
entity, entity,
actions, actions,
@ -198,7 +207,7 @@ class Synchronizer {
pushAccessMap[entity].push({ pushAccessMap[entity].push({
projection, projection,
groupByUsers, groupByUsers,
getRemotePushInfo, getRemotePushInfo: getPushInfo,
endpoint: endpoint2, endpoint: endpoint2,
entity, entity,
actions, actions,
@ -250,7 +259,7 @@ class Synchronizer {
if (pushEntityNodes && pushEntityNodes.length > 0) { if (pushEntityNodes && pushEntityNodes.length > 0) {
// 每个pushEntityNode代表配置的一个remoteEntity // 每个pushEntityNode代表配置的一个remoteEntity
await Promise.all(pushEntityNodes.map(async (node) => { await Promise.all(pushEntityNodes.map(async (node) => {
const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, entity, actions, onSynchronized } = node; const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, actions, onSynchronized } = node;
if (!actions || actions.includes(action)) { if (!actions || actions.includes(action)) {
const pushed = []; const pushed = [];
const rows = await context.select(targetEntity, { const rows = await context.select(targetEntity, {
@ -267,7 +276,7 @@ class Synchronizer {
// userId就是需要发送给远端的user但是要将本次操作的user过滤掉操作的原本产生者 // userId就是需要发送给远端的user但是要将本次操作的user过滤掉操作的原本产生者
const userSendDict = groupByUsers(rows); const userSendDict = groupByUsers(rows);
const pushToUserIdFn = async (userId) => { const pushToUserIdFn = async (userId) => {
const rowIds = userSendDict[userId]; const { entity, entityId, rowIds } = userSendDict[userId];
// 推送到远端结点的oper // 推送到远端结点的oper
const oper2 = { const oper2 = {
id: oper.id, id: oper.id,
@ -281,8 +290,11 @@ class Synchronizer {
bornAt: oper.bornAt, bornAt: oper.bornAt,
targetEntity, targetEntity,
}; };
const { url } = await getRemoteAccessInfo(userId); const { url } = await getRemoteAccessInfo(context, {
await this.pushOper(oper2 /** 这里不明白为什么TS过不去 */, userId, url, endpoint); userId,
remoteEntityId: entityId,
});
await this.pushOper(context, oper2 /** 这里不明白为什么TS过不去 */, userId, url, endpoint, entity, entityId);
}; };
for (const userId in userSendDict) { for (const userId in userSendDict) {
if (userId !== operatorId) { if (userId !== operatorId) {
@ -319,17 +331,15 @@ class Synchronizer {
getSyncTriggers() { getSyncTriggers() {
return [this.makeCreateOperTrigger()]; return [this.makeCreateOperTrigger()];
} }
async checkOperationConsistent(entity, ids, bornAt) {
}
getSelfEndpoint() { getSelfEndpoint() {
return { return {
name: this.config.self.endpoint || 'sync', name: this.config.self.endpoint || 'sync',
method: 'post', method: 'post',
params: ['entity'], params: ['entity', 'entityId'],
fn: async (context, params, headers, req, body) => { fn: async (context, params, headers, req, body) => {
// body中是传过来的oper数组信息 // body中是传过来的oper数组信息
const { entity } = params; const { entity, entityId } = params;
const { [OAK_SYNC_HEADER_ITEM]: id } = headers; const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITYID]: meEntityId } = headers;
console.log('接收到来自远端的sync数据', entity, JSON.stringify(body)); console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
const successIds = []; const successIds = [];
let failed; let failed;
@ -337,22 +347,31 @@ class Synchronizer {
if (!this.remotePullInfoMap[entity]) { if (!this.remotePullInfoMap[entity]) {
this.remotePullInfoMap[entity] = {}; this.remotePullInfoMap[entity] = {};
} }
if (!this.remotePullInfoMap[entity][id]) { if (!this.remotePullInfoMap[entity][entityId]) {
const { getRemotePullInfo, pullEntities } = this.config.remotes.find(ele => ele.entity === entity); const { getPullInfo, pullEntities } = this.config.remotes.find(ele => ele.entity === entity);
const pullEntityDict = {}; const pullEntityDict = {};
if (pullEntities) { if (pullEntities) {
pullEntities.forEach((def) => pullEntityDict[def.entity] = def); pullEntities.forEach((def) => pullEntityDict[def.entity] = def);
} }
this.remotePullInfoMap[entity][id] = { this.remotePullInfoMap[entity][entityId] = {
pullInfo: await getRemotePullInfo(id), pullInfo: await getPullInfo(context, {
selfId: meEntityId,
remoteEntityId: entityId,
}),
pullEntityDict, pullEntityDict,
}; };
} }
const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][id]; const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][entityId];
const { userId, algorithm, publicKey } = pullInfo; const { userId, algorithm, publicKey, cxtInfo } = pullInfo;
// todo 解密
(0, assert_1.default)(userId); (0, assert_1.default)(userId);
if (!this.pullMaxBornAtMap.hasOwnProperty(id)) { context.setCurrentUserId(userId);
if (cxtInfo) {
await context.initialize(cxtInfo);
}
const selfEncryptInfo = await this.getSelfEncryptInfo(context);
(0, assert_1.default)(selfEncryptInfo.id === meEntityId && meEntity === this.config.self.entity);
// todo 解密
if (!this.pullMaxBornAtMap.hasOwnProperty(entityId)) {
const [maxHisOper] = await context.select('oper', { const [maxHisOper] = await context.select('oper', {
data: { data: {
id: 1, id: 1,
@ -372,10 +391,9 @@ class Synchronizer {
indexFrom: 0, indexFrom: 0,
count: 1, count: 1,
}, { dontCollect: true }); }, { dontCollect: true });
this.pullMaxBornAtMap[id] = maxHisOper?.bornAt || 0; this.pullMaxBornAtMap[entityId] = maxHisOper?.bornAt || 0;
} }
let maxBornAt = this.pullMaxBornAtMap[id]; let maxBornAt = this.pullMaxBornAtMap[entityId];
context.setCurrentUserId(userId);
const opers = body; const opers = body;
const outdatedOpers = opers.filter(ele => ele.bornAt <= maxBornAt); const outdatedOpers = opers.filter(ele => ele.bornAt <= maxBornAt);
const freshOpers = opers.filter(ele => ele.bornAt > maxBornAt); const freshOpers = opers.filter(ele => ele.bornAt > maxBornAt);
@ -441,7 +459,7 @@ class Synchronizer {
} }
})() })()
]); ]);
this.pullMaxBornAtMap[id] = maxBornAt; this.pullMaxBornAtMap[entityId] = maxBornAt;
return { return {
successIds, successIds,
failed, failed,

View File

@ -6,7 +6,7 @@ import { makeIntrinsicCTWs } from "oak-domain/lib/store/actionDef";
import { intersection, omit } from 'oak-domain/lib/utils/lodash'; import { intersection, omit } from 'oak-domain/lib/utils/lodash';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid'; import { generateNewIdAsync } from 'oak-domain/lib/utils/uuid';
import { AppLoader as GeneralAppLoader, Trigger, Checker, Aspect, CreateOpResult, Context, EntityDict, Watcher, BBWatcher, WBWatcher, OpRecord, Routine, FreeRoutine, Timer, FreeTimer, StorageSchema, OperationResult } from "oak-domain/lib/types"; import { AppLoader as GeneralAppLoader, Trigger, Checker, Aspect, CreateOpResult, SyncConfig, EntityDict, Watcher, BBWatcher, WBWatcher, OpRecord, Routine, FreeRoutine, Timer, FreeTimer, StorageSchema, OperationResult } from "oak-domain/lib/types";
import { DbStore } from "./DbStore"; import { DbStore } from "./DbStore";
import generalAspectDict, { clearPorts, registerPorts } from 'oak-common-aspect/lib/index'; import generalAspectDict, { clearPorts, registerPorts } from 'oak-common-aspect/lib/index';
import { MySQLConfiguration } from 'oak-db/lib/MySQL/types/Configuration'; import { MySQLConfiguration } from 'oak-db/lib/MySQL/types/Configuration';
@ -19,7 +19,6 @@ import { Server as SocketIoServer, Namespace } from 'socket.io';
import DataSubscriber from './cluster/DataSubscriber'; import DataSubscriber from './cluster/DataSubscriber';
import { getClusterInfo } from './cluster/env'; import { getClusterInfo } from './cluster/env';
import Synchronizer from './Synchronizer'; import Synchronizer from './Synchronizer';
import { SyncConfig } from './types/Sync';
export class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends GeneralAppLoader<ED, Cxt> { export class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends GeneralAppLoader<ED, Cxt> {
@ -27,7 +26,7 @@ export class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends Backe
private aspectDict: Record<string, Aspect<ED, Cxt>>; private aspectDict: Record<string, Aspect<ED, Cxt>>;
private externalDependencies: string[]; private externalDependencies: string[];
protected dataSubscriber?: DataSubscriber<ED, Cxt>; protected dataSubscriber?: DataSubscriber<ED, Cxt>;
protected synchronizer?: Synchronizer<ED, Cxt>; protected synchronizers?: Synchronizer<ED, Cxt>[];
protected contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>; protected contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>;
private requireSth(filePath: string): any { private requireSth(filePath: string): any {
@ -127,11 +126,11 @@ export class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends Backe
const dbConfigFile = join(this.path, 'configuration', 'mysql.json'); const dbConfigFile = join(this.path, 'configuration', 'mysql.json');
const dbConfig = require(dbConfigFile); const dbConfig = require(dbConfigFile);
const syncConfigFile = join(this.path, 'lib', 'configuration', 'sync.js'); const syncConfigFile = join(this.path, 'lib', 'configuration', 'sync.js');
const syncConfig = existsSync(syncConfigFile) && require(syncConfigFile).default; const syncConfigs = existsSync(syncConfigFile) && require(syncConfigFile).default;
return { return {
dbConfig: dbConfig as MySQLConfiguration, dbConfig: dbConfig as MySQLConfiguration,
syncConfig: syncConfig as SyncConfig<ED, Cxt> | undefined, syncConfigs: syncConfigs as SyncConfig<ED, Cxt>[] | undefined,
}; };
} }
@ -202,80 +201,26 @@ export class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends Backe
(checker) => this.dbStore.registerChecker(checker) (checker) => this.dbStore.registerChecker(checker)
); );
if (this.synchronizer) { if (this.synchronizers) {
// 同步数据到远端结点通过commit trigger来完成 // 同步数据到远端结点通过commit trigger来完成
const syncTriggers = this.synchronizer.getSyncTriggers(); for (const synchronizer of this.synchronizers) {
syncTriggers.forEach( const syncTriggers = synchronizer.getSyncTriggers();
(trigger) => this.registerTrigger(trigger) syncTriggers.forEach(
); (trigger) => this.registerTrigger(trigger)
);
}
} }
} }
async mount(initialize?: true) { async mount(initialize?: true) {
const { path } = this; const { path } = this;
if (!initialize) { if (!initialize) {
const { dbConfig, syncConfig } = this.getConfiguration(); const { syncConfigs } = this.getConfiguration();
if (syncConfig) { if (syncConfigs) {
const { this.synchronizers = syncConfigs.map(
self, remotes config => new Synchronizer(config, this.dbStore.getSchema())
} = syncConfig; );
const { getSelfEncryptInfo, ...restSelf } = self;
this.synchronizer = new Synchronizer({
self: {
// entity: self.entity,
getSelfEncryptInfo: async () => {
const context = await this.contextBuilder()(this.dbStore);
await context.begin();
try {
const result = await self.getSelfEncryptInfo(context);
await context.commit();
return result;
}
catch (err) {
await context.rollback();
throw err;
}
},
...restSelf
},
remotes: remotes.map(
(r) => {
const { getPushInfo, getPullInfo, ...rest } = r;
return {
getRemotePushInfo: async (id) => {
const context = await this.contextBuilder()(this.dbStore);
await context.begin();
try {
const result = await getPushInfo(id, context);
await context.commit();
return result;
}
catch (err) {
await context.rollback();
throw err;
}
},
getRemotePullInfo: async (userId) => {
const context = await this.contextBuilder()(this.dbStore);
await context.begin();
try {
const result = await getPullInfo(userId, context);
await context.commit();
return result;
}
catch (err) {
await context.rollback();
throw err;
}
},
...rest,
};
}
)
}, this.dbStore.getSchema());
} }
this.initTriggers(); this.initTriggers();
@ -407,9 +352,13 @@ export class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends Backe
} }
} }
if (this.synchronizer) { if (this.synchronizers) {
const syncEp = this.synchronizer.getSelfEndpoint(); this.synchronizers.forEach(
transformEndpointItem(syncEp.name, syncEp); (synchronizer) => {
const syncEp = synchronizer.getSelfEndpoint();
transformEndpointItem(syncEp.name, syncEp);
}
);
} }
return endPointRouters; return endPointRouters;
} }

View File

@ -1,4 +1,5 @@
import { EntityDict, StorageSchema, EndpointItem, RemotePullInfo, SelfEncryptInfo, RemotePushInfo, PushEntityDef, PullEntityDef } from 'oak-domain/lib/types'; import { EntityDict, StorageSchema, EndpointItem, RemotePullInfo, SelfEncryptInfo,
RemotePushInfo, PushEntityDef, PullEntityDef, SyncConfig } from 'oak-domain/lib/types';
import { VolatileTrigger } from 'oak-domain/lib/types/Trigger'; import { VolatileTrigger } from 'oak-domain/lib/types/Trigger';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain'; import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { destructRelationPath, destructDirectPath } from 'oak-domain/lib/utils/relationPath'; import { destructRelationPath, destructDirectPath } from 'oak-domain/lib/utils/relationPath';
@ -6,10 +7,10 @@ import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRunt
import assert from 'assert'; import assert from 'assert';
import { join } from 'path'; import { join } from 'path';
import { difference } from 'oak-domain/lib/utils/lodash'; import { difference } from 'oak-domain/lib/utils/lodash';
import { SyncConfigWrapper } from './types/Sync';
import { getRelevantIds } from 'oak-domain/lib/store/filter'; import { getRelevantIds } from 'oak-domain/lib/store/filter';
const OAK_SYNC_HEADER_ITEM = 'oak-sync-remote-id'; const OAK_SYNC_HEADER_ENTITY = 'oak-sync-entity';
const OAK_SYNC_HEADER_ENTITYID = 'oak-sync-entity-id';
type Channel<ED extends EntityDict & BaseEntityDict> = { type Channel<ED extends EntityDict & BaseEntityDict> = {
queue: Array<{ queue: Array<{
@ -23,7 +24,7 @@ type Channel<ED extends EntityDict & BaseEntityDict> = {
}; };
export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> { export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> {
private config: SyncConfigWrapper<ED, Cxt>; private config: SyncConfig<ED, Cxt>;
private schema: StorageSchema<ED>; private schema: StorageSchema<ED>;
private selfEncryptInfo?: SelfEncryptInfo; private selfEncryptInfo?: SelfEncryptInfo;
private remotePullInfoMap: Record<string, Record<string, { private remotePullInfoMap: Record<string, Record<string, {
@ -39,7 +40,7 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
* @param channel * @param channel
* @param retry * @param retry
*/ */
private async pushOnChannel(channel: Channel<ED>, retry?: number) { private async pushOnChannel(remoteEntity: keyof ED, remoteEntityId: string, context: Cxt, channel: Channel<ED>, retry?: number) {
const { queue, api, nextPushTimestamp } = channel; const { queue, api, nextPushTimestamp } = channel;
assert(nextPushTimestamp); assert(nextPushTimestamp);
@ -59,19 +60,21 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
}; };
try { try {
// todo 加密 // todo 加密
const selfEncryptInfo = await this.getSelfEncryptInfo(); const selfEncryptInfo = await this.getSelfEncryptInfo(context);
console.log('向远端结点sync数据', api, JSON.stringify(opers)); console.log('向远端结点sync数据', api, JSON.stringify(opers));
const res = await fetch(api, { const finalApi = join(api, selfEncryptInfo.id);
const res = await fetch(finalApi, {
method: 'post', method: 'post',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
[OAK_SYNC_HEADER_ITEM]: selfEncryptInfo!.id, [OAK_SYNC_HEADER_ENTITY]: remoteEntity as string,
[OAK_SYNC_HEADER_ENTITYID]: remoteEntityId,
}, },
body: JSON.stringify(opers), body: JSON.stringify(opers),
}); });
if (res.status !== 200) { if (res.status !== 200) {
throw new Error(`sync数据时访问api「${api}」的结果不是200。「${res.status}`); throw new Error(`sync数据时访问api「${finalApi}」的结果不是200。「${res.status}`);
} }
json = await res.json(); json = await res.json();
} }
@ -107,7 +110,7 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
const interval = Math.max(0, channel.nextPushTimestamp - Date.now()); const interval = Math.max(0, channel.nextPushTimestamp - Date.now());
const retry2 = needRetry ? (typeof retry === 'number' ? retry + 1 : 1) : undefined; const retry2 = needRetry ? (typeof retry === 'number' ? retry + 1 : 1) : undefined;
console.log('need retry', retry2); console.log('need retry', retry2);
setTimeout(() => this.pushOnChannel(channel, retry2), interval); setTimeout(() => this.pushOnChannel(remoteEntity, remoteEntityId, context, channel, retry2), interval);
} }
else { else {
channel.handler = undefined; channel.handler = undefined;
@ -125,10 +128,13 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
* oper一定先到达被推送volatile trigger是在事务提交后再发生的oper的时候assert掉先by Xc 20240226 * oper一定先到达被推送volatile trigger是在事务提交后再发生的oper的时候assert掉先by Xc 20240226
*/ */
private async pushOper( private async pushOper(
context: Cxt,
oper: Partial<ED['oper']['Schema']>, oper: Partial<ED['oper']['Schema']>,
userId: string, userId: string,
url: string, url: string,
endpoint: string, endpoint: string,
remoteEntity: keyof ED,
remoteEntityId: string,
nextPushTimestamp?: number nextPushTimestamp?: number
) { ) {
if (!this.remotePushChannel[userId]) { if (!this.remotePushChannel[userId]) {
@ -168,7 +174,7 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
if (!channel.handler) { if (!channel.handler) {
channel.nextPushTimestamp = nextPushTimestamp2; channel.nextPushTimestamp = nextPushTimestamp2;
channel.handler = setTimeout(async () => { channel.handler = setTimeout(async () => {
await this.pushOnChannel(channel); await this.pushOnChannel(remoteEntity, remoteEntityId, context, channel);
}, nextPushTimestamp2 - now); }, nextPushTimestamp2 - now);
} }
else if (channel.nextPushTimestamp && channel.nextPushTimestamp > nextPushTimestamp2) { else if (channel.nextPushTimestamp && channel.nextPushTimestamp > nextPushTimestamp2) {
@ -184,11 +190,11 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
} }
} }
private async getSelfEncryptInfo() { private async getSelfEncryptInfo(context: Cxt) {
if (this.selfEncryptInfo) { if (this.selfEncryptInfo) {
return this.selfEncryptInfo; return this.selfEncryptInfo;
} }
this.selfEncryptInfo = await this.config.self.getSelfEncryptInfo(); this.selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
return this.selfEncryptInfo!; return this.selfEncryptInfo!;
} }
@ -198,20 +204,24 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
// 根据remotes定义建立从entity到需要同步的远端结点信息的Map // 根据remotes定义建立从entity到需要同步的远端结点信息的Map
const pushAccessMap: Record<string, Array<{ const pushAccessMap: Record<string, Array<{
projection: ED[keyof ED]['Selection']['data']; // 从entity上取到相关user需要的projection projection: ED[keyof ED]['Selection']['data']; // 从entity上取到相关user需要的projection
groupByUsers: (row: Partial<ED[keyof ED]['Schema']>[]) => Record<string, string[]>; // 根据相关数据行关联的userId对行ID进行分组 groupByUsers: (row: Partial<ED[keyof ED]['Schema']>[]) => Record<string, {
getRemotePushInfo: (userId: string) => Promise<RemotePushInfo>; // 根据userId获得相应push远端的信息 entity: keyof ED; // 对方目标对象
endpoint: string; // 远端接收endpoint的url entityId: string; // 对象目标对象Id
rowIds: string[]; // 要推送的rowId
}>; // 根据相关数据行关联的userId对行ID进行重分组键值为userId
getRemotePushInfo: SyncConfig<ED, Cxt>['remotes'][number]['getPushInfo']; // 根据userId获得相应push远端的信息
endpoint: string; // 远端接收endpoint的url
actions?: string[]; actions?: string[];
onSynchronized: PushEntityDef<ED, keyof ED, Cxt>['onSynchronized']; onSynchronized: PushEntityDef<ED, keyof ED, Cxt>['onSynchronized'];
entity: keyof ED; entity: keyof ED;
}>> = {}; }>> = {};
remotes.forEach( remotes.forEach(
(remote) => { (remote) => {
const { getRemotePushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote, entitySelf } = remote; const { getPushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote } = remote;
if (pushEntityDefs) { if (pushEntityDefs) {
const pushEntities = [] as Array<keyof ED>; const pushEntities = [] as Array<keyof ED>;
const endpoint2 = join(endpoint || 'sync', entitySelf as string || self.entitySelf as string); const endpoint2 = join(endpoint || 'sync', self.entity as string);
for (const def of pushEntityDefs) { for (const def of pushEntityDefs) {
const { path, relationName, recursive, entity, actions, onSynchronized } = def; const { path, relationName, recursive, entity, actions, onSynchronized } = def;
pushEntities.push(entity); pushEntities.push(entity);
@ -228,19 +238,30 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
}, recursive) : destructDirectPath(this.schema, entity, path2, recursive); }, recursive) : destructDirectPath(this.schema, entity, path2, recursive);
const groupByUsers = (rows: Partial<ED[keyof ED]['Schema']>[]) => { const groupByUsers = (rows: Partial<ED[keyof ED]['Schema']>[]) => {
const userRowDict: Record<string, string[]> = {}; const userRowDict: Record<string, {
rows.filter( rowIds: string[];
entityId: string;
entity: keyof ED;
}> = {};
rows.forEach(
(row) => { (row) => {
const userIds = getData(row)?.map(ele => ele.userId); const goals = getData(row);
if (userIds) { if (goals) {
userIds.forEach( goals.forEach(
(userId) => { ({ entity, entityId, userId }) => {
if (userRowDict[userId]) { if (userRowDict[userId]) {
userRowDict[userId].push(row.id!); // 逻辑上来说同一个userId其关联的entity和entityId必然相同这个entity/entityId代表了对方
assert(userRowDict[userId].entity === entity && userRowDict[userId].entityId === entityId);
userRowDict[userId].rowIds.push(row.id!);
} }
else { else {
userRowDict[userId] = [row.id!]; userRowDict[userId] = {
entity,
entityId,
rowIds: [row.id!],
};
} }
} }
) )
} }
@ -253,7 +274,7 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
pushAccessMap[entity as string] = [{ pushAccessMap[entity as string] = [{
projection, projection,
groupByUsers, groupByUsers,
getRemotePushInfo, getRemotePushInfo: getPushInfo,
endpoint: endpoint2, endpoint: endpoint2,
entity, entity,
actions, actions,
@ -264,7 +285,7 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
pushAccessMap[entity as string].push({ pushAccessMap[entity as string].push({
projection, projection,
groupByUsers, groupByUsers,
getRemotePushInfo, getRemotePushInfo: getPushInfo,
endpoint: endpoint2, endpoint: endpoint2,
entity, entity,
actions, actions,
@ -324,7 +345,7 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
await Promise.all( await Promise.all(
pushEntityNodes.map( pushEntityNodes.map(
async (node) => { async (node) => {
const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, entity, actions, onSynchronized } = node; const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, actions, onSynchronized } = node;
if (!actions || actions.includes(action!)) { if (!actions || actions.includes(action!)) {
const pushed = [] as Promise<void>[]; const pushed = [] as Promise<void>[];
const rows = await context.select(targetEntity!, { const rows = await context.select(targetEntity!, {
@ -342,7 +363,7 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
// userId就是需要发送给远端的user但是要将本次操作的user过滤掉操作的原本产生者 // userId就是需要发送给远端的user但是要将本次操作的user过滤掉操作的原本产生者
const userSendDict = groupByUsers(rows); const userSendDict = groupByUsers(rows);
const pushToUserIdFn = async (userId: string) => { const pushToUserIdFn = async (userId: string) => {
const rowIds = userSendDict[userId]; const { entity, entityId, rowIds } = userSendDict[userId];
// 推送到远端结点的oper // 推送到远端结点的oper
const oper2 = { const oper2 = {
id: oper.id!, id: oper.id!,
@ -356,8 +377,11 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
bornAt: oper.bornAt!, bornAt: oper.bornAt!,
targetEntity, targetEntity,
}; };
const { url } = await getRemoteAccessInfo(userId); const { url } = await getRemoteAccessInfo(context, {
await this.pushOper(oper2 as any /** 这里不明白为什么TS过不去 */, userId, url, endpoint); userId,
remoteEntityId: entityId,
});
await this.pushOper(context, oper2 as any /** 这里不明白为什么TS过不去 */, userId, url, endpoint, entity, entityId);
}; };
for (const userId in userSendDict) { for (const userId in userSendDict) {
if (userId !== operatorId) { if (userId !== operatorId) {
@ -389,7 +413,7 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
return createOperTrigger; return createOperTrigger;
} }
constructor(config: SyncConfigWrapper<ED, Cxt>, schema: StorageSchema<ED>) { constructor(config: SyncConfig<ED, Cxt>, schema: StorageSchema<ED>) {
this.config = config; this.config = config;
this.schema = schema; this.schema = schema;
} }
@ -402,15 +426,11 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
return [this.makeCreateOperTrigger()] as Array<VolatileTrigger<ED, keyof ED, Cxt>>; return [this.makeCreateOperTrigger()] as Array<VolatileTrigger<ED, keyof ED, Cxt>>;
} }
private async checkOperationConsistent(entity: keyof ED, ids: string[], bornAt: number) {
}
getSelfEndpoint(): EndpointItem<ED, Cxt> { getSelfEndpoint(): EndpointItem<ED, Cxt> {
return { return {
name: this.config.self.endpoint || 'sync', name: this.config.self.endpoint || 'sync',
method: 'post', method: 'post',
params: ['entity'], params: ['entity', 'entityId'],
fn: async (context, params, headers, req, body): Promise<{ fn: async (context, params, headers, req, body): Promise<{
successIds: string[], successIds: string[],
failed?: { failed?: {
@ -419,8 +439,8 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
}; };
}> => { }> => {
// body中是传过来的oper数组信息 // body中是传过来的oper数组信息
const { entity } = params; const { entity, entityId } = params;
const { [OAK_SYNC_HEADER_ITEM]: id } = headers; const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITYID]: meEntityId } = headers;
console.log('接收到来自远端的sync数据', entity, JSON.stringify(body)); console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
const successIds = [] as string[]; const successIds = [] as string[];
@ -432,26 +452,35 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
if (!this.remotePullInfoMap[entity]) { if (!this.remotePullInfoMap[entity]) {
this.remotePullInfoMap[entity] = {}; this.remotePullInfoMap[entity] = {};
} }
if (!this.remotePullInfoMap[entity]![id as string]) { if (!this.remotePullInfoMap[entity]![entityId]) {
const { getRemotePullInfo, pullEntities } = this.config.remotes.find(ele => ele.entity === entity)!; const { getPullInfo, pullEntities } = this.config.remotes.find(ele => ele.entity === entity)!;
const pullEntityDict = {} as Record<string, PullEntityDef<ED, keyof ED, Cxt>>; const pullEntityDict = {} as Record<string, PullEntityDef<ED, keyof ED, Cxt>>;
if (pullEntities) { if (pullEntities) {
pullEntities.forEach( pullEntities.forEach(
(def) => pullEntityDict[def.entity as string] = def (def) => pullEntityDict[def.entity as string] = def
); );
} }
this.remotePullInfoMap[entity]![id as string] = { this.remotePullInfoMap[entity]![entityId] = {
pullInfo: await getRemotePullInfo(id as string), pullInfo: await getPullInfo(context, {
selfId: meEntityId as string,
remoteEntityId: entityId,
}),
pullEntityDict, pullEntityDict,
}; };
} }
const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][id as string]!; const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][entityId]!;
const { userId, algorithm, publicKey } = pullInfo; const { userId, algorithm, publicKey, cxtInfo } = pullInfo;
assert(userId);
context.setCurrentUserId(userId);
if (cxtInfo) {
await context.initialize(cxtInfo);
}
const selfEncryptInfo = await this.getSelfEncryptInfo(context);
assert(selfEncryptInfo.id === meEntityId && meEntity === this.config.self.entity);
// todo 解密 // todo 解密
assert(userId); if (!this.pullMaxBornAtMap.hasOwnProperty(entityId)) {
if (!this.pullMaxBornAtMap.hasOwnProperty(id as string)) {
const [maxHisOper] = await context.select('oper', { const [maxHisOper] = await context.select('oper', {
data: { data: {
id: 1, id: 1,
@ -471,11 +500,10 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
indexFrom: 0, indexFrom: 0,
count: 1, count: 1,
}, { dontCollect: true }); }, { dontCollect: true });
this.pullMaxBornAtMap[id as string] = maxHisOper?.bornAt as number || 0; this.pullMaxBornAtMap[entityId] = maxHisOper?.bornAt as number || 0;
} }
let maxBornAt = this.pullMaxBornAtMap[id as string]!; let maxBornAt = this.pullMaxBornAtMap[entityId]!;
context.setCurrentUserId(userId);
const opers = body as ED['oper']['Schema'][]; const opers = body as ED['oper']['Schema'][];
const outdatedOpers = opers.filter( const outdatedOpers = opers.filter(
@ -553,7 +581,7 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
] ]
); );
this.pullMaxBornAtMap[id as string] = maxBornAt; this.pullMaxBornAtMap[entityId] = maxBornAt;
return { return {
successIds, successIds,
failed, failed,

View File

@ -1,25 +0,0 @@
import { EntityDict } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
import { RemotePushInfo, RemotePullInfo, SelfEncryptInfo, SyncRemoteConfigBase, SyncSelfConfigBase, SyncConfig } from 'oak-domain/lib/types/Sync';
interface SyncRemoteConfigWrapper<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends SyncRemoteConfigBase<ED, Cxt> {
getRemotePushInfo: (userId: string) => Promise<RemotePushInfo>;
getRemotePullInfo: (id: string) => Promise<RemotePullInfo>;
};
interface SyncSelfConfigWrapper<ED extends EntityDict & BaseEntityDict> extends SyncSelfConfigBase<ED> {
getSelfEncryptInfo: () => Promise<SelfEncryptInfo>;
};
export interface SyncConfigWrapper<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> {
self: SyncSelfConfigWrapper<ED>;
remotes: Array<SyncRemoteConfigWrapper<ED, Cxt>>;
};
export {
RemotePushInfo,
RemotePullInfo,
SelfEncryptInfo,
SyncConfig,
};