Synchronizer较为完整的实现
This commit is contained in:
parent
a806e0638a
commit
b12f04c523
|
|
@ -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>;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
Loading…
Reference in New Issue