oak-backend-base 3.4.0 → 3.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/AppLoader.d.ts +1 -1
- package/lib/AppLoader.js +16 -23
- package/lib/Synchronizer.d.ts +12 -20
- package/lib/Synchronizer.js +252 -201
- package/package.json +3 -3
package/lib/AppLoader.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt exten
|
|
|
12
12
|
private aspectDict;
|
|
13
13
|
private externalDependencies;
|
|
14
14
|
protected dataSubscriber?: DataSubscriber<ED, Cxt>;
|
|
15
|
-
protected
|
|
15
|
+
protected synchronizer?: Synchronizer<ED, Cxt>;
|
|
16
16
|
protected contextBuilder: (scene?: string) => (store: DbStore<ED, Cxt>) => Promise<Cxt>;
|
|
17
17
|
private requireSth;
|
|
18
18
|
protected makeContext(cxtStr?: string, headers?: IncomingHttpHeaders): Promise<Cxt>;
|
package/lib/AppLoader.js
CHANGED
|
@@ -21,7 +21,7 @@ class AppLoader extends types_1.AppLoader {
|
|
|
21
21
|
aspectDict;
|
|
22
22
|
externalDependencies;
|
|
23
23
|
dataSubscriber;
|
|
24
|
-
|
|
24
|
+
synchronizer;
|
|
25
25
|
contextBuilder;
|
|
26
26
|
requireSth(filePath) {
|
|
27
27
|
const depFilePath = (0, path_1.join)(this.path, filePath);
|
|
@@ -104,7 +104,7 @@ class AppLoader extends types_1.AppLoader {
|
|
|
104
104
|
const syncConfigs = (0, fs_1.existsSync)(syncConfigFile) && require(syncConfigFile).default;
|
|
105
105
|
return {
|
|
106
106
|
dbConfig: dbConfig,
|
|
107
|
-
|
|
107
|
+
syncConfig: syncConfigs,
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
constructor(path, contextBuilder, ns, nsServer) {
|
|
@@ -149,20 +149,18 @@ class AppLoader extends types_1.AppLoader {
|
|
|
149
149
|
adTriggers.forEach((trigger) => this.registerTrigger(trigger));
|
|
150
150
|
checkers.forEach((checker) => this.dbStore.registerChecker(checker));
|
|
151
151
|
adCheckers.forEach((checker) => this.dbStore.registerChecker(checker));
|
|
152
|
-
if (this.
|
|
153
|
-
// 同步数据到远端结点通过commit trigger来完成
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
syncTriggers.forEach((trigger) => this.registerTrigger(trigger));
|
|
157
|
-
}
|
|
152
|
+
if (this.synchronizer) {
|
|
153
|
+
// 同步数据到远端结点通过commit trigger来完成
|
|
154
|
+
const syncTriggers = this.synchronizer.getSyncTriggers();
|
|
155
|
+
syncTriggers.forEach((trigger) => this.registerTrigger(trigger));
|
|
158
156
|
}
|
|
159
157
|
}
|
|
160
158
|
async mount(initialize) {
|
|
161
159
|
const { path } = this;
|
|
162
160
|
if (!initialize) {
|
|
163
|
-
const {
|
|
164
|
-
if (
|
|
165
|
-
this.
|
|
161
|
+
const { syncConfig: syncConfig } = this.getConfiguration();
|
|
162
|
+
if (syncConfig) {
|
|
163
|
+
this.synchronizer = new Synchronizer_1.default(syncConfig, this.dbStore.getSchema(), () => this.contextBuilder()(this.dbStore));
|
|
166
164
|
}
|
|
167
165
|
this.initTriggers();
|
|
168
166
|
}
|
|
@@ -275,11 +273,9 @@ class AppLoader extends types_1.AppLoader {
|
|
|
275
273
|
transformEndpointItem(router, item);
|
|
276
274
|
}
|
|
277
275
|
}
|
|
278
|
-
if (this.
|
|
279
|
-
this.
|
|
280
|
-
|
|
281
|
-
transformEndpointItem(syncEp.name, syncEp);
|
|
282
|
-
});
|
|
276
|
+
if (this.synchronizer) {
|
|
277
|
+
const syncEp = this.synchronizer.getSelfEndpoint();
|
|
278
|
+
transformEndpointItem(syncEp.name, syncEp);
|
|
283
279
|
}
|
|
284
280
|
return endPointRouters;
|
|
285
281
|
}
|
|
@@ -399,6 +395,10 @@ class AppLoader extends types_1.AppLoader {
|
|
|
399
395
|
}
|
|
400
396
|
async execStartRoutines() {
|
|
401
397
|
const routines = this.requireSth('lib/routines/start');
|
|
398
|
+
if (this.synchronizer) {
|
|
399
|
+
const routine = this.synchronizer.getSyncRoutine();
|
|
400
|
+
routines.push(routine);
|
|
401
|
+
}
|
|
402
402
|
for (const routine of routines) {
|
|
403
403
|
if (routine.hasOwnProperty('entity')) {
|
|
404
404
|
const start = Date.now();
|
|
@@ -428,13 +428,6 @@ class AppLoader extends types_1.AppLoader {
|
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
|
-
if (this.synchronizers) {
|
|
432
|
-
this.synchronizers.forEach((synchronizer) => {
|
|
433
|
-
// 这个routine在内部处理异步
|
|
434
|
-
const routine = synchronizer.getSyncRoutine();
|
|
435
|
-
this.execWatcher(routine);
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
431
|
}
|
|
439
432
|
async execRoutine(routine) {
|
|
440
433
|
const context = await this.makeContext();
|
package/lib/Synchronizer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityDict, StorageSchema, EndpointItem, SyncConfig,
|
|
1
|
+
import { EntityDict, StorageSchema, EndpointItem, SyncConfig, FreeRoutine } from 'oak-domain/lib/types';
|
|
2
2
|
import { VolatileTrigger } from 'oak-domain/lib/types/Trigger';
|
|
3
3
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
4
4
|
import { BackendRuntimeContext } from 'oak-frontend-base/lib/context/BackendRuntimeContext';
|
|
@@ -7,7 +7,8 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
|
|
|
7
7
|
private schema;
|
|
8
8
|
private remotePullInfoMap;
|
|
9
9
|
private pullMaxBornAtMap;
|
|
10
|
-
private
|
|
10
|
+
private channelDict;
|
|
11
|
+
private contextBuilder;
|
|
11
12
|
private pushAccessMap;
|
|
12
13
|
/**
|
|
13
14
|
* 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
|
|
@@ -15,32 +16,23 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
|
|
|
15
16
|
* @param retry
|
|
16
17
|
*/
|
|
17
18
|
private startChannel;
|
|
18
|
-
private
|
|
19
|
+
private startAllChannel;
|
|
20
|
+
private pushOperToChannel;
|
|
21
|
+
private refineOperData;
|
|
22
|
+
private dispatchOperToChannels;
|
|
19
23
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* 1)oper如果推送失败了,必须留存在queue中,以保证在后面产生的oper之前推送
|
|
23
|
-
* 2)当对queue中增加oper时,要检查是否有重(有重说明之前失败过),如果无重则将之放置在队列尾
|
|
24
|
-
*
|
|
25
|
-
* 其实这里还无法严格保证先产生的oper一定先到达被推送,因为volatile trigger是在事务提交后再发生的,但这种情况在目前应该跑不出来,在实际执行oper的时候assert掉先。by Xc 20240226
|
|
26
|
-
*/
|
|
27
|
-
private pushOper;
|
|
28
|
-
/**
|
|
29
|
-
* 因为应用可能是多租户,得提前确定context下的selfEncryptInfo
|
|
30
|
-
* 由于checkpoint时无法区别不同上下文之间的未完成oper数据,所以接口只能这样设计
|
|
31
|
-
* @param id
|
|
24
|
+
* 为了保证推送的oper序,采用从database中顺序读取所有需要推送的oper来进行推送
|
|
25
|
+
* 每个进程都保证把当前所有的oper顺序处理掉,就不会有乱序的问题,大家通过database上的锁来完成同步
|
|
32
26
|
* @param context
|
|
33
|
-
* @param selfEncryptInfo
|
|
34
|
-
* @returns
|
|
35
27
|
*/
|
|
36
|
-
private
|
|
28
|
+
private trySynchronizeOpers;
|
|
37
29
|
private makeCreateOperTrigger;
|
|
38
|
-
constructor(config: SyncConfig<ED, Cxt>, schema: StorageSchema<ED>);
|
|
30
|
+
constructor(config: SyncConfig<ED, Cxt>, schema: StorageSchema<ED>, contextBuilder: () => Promise<Cxt>);
|
|
39
31
|
/**
|
|
40
32
|
* 根据sync的定义,生成对应的 commit triggers
|
|
41
33
|
* @returns
|
|
42
34
|
*/
|
|
43
35
|
getSyncTriggers(): VolatileTrigger<ED, keyof ED, Cxt>[];
|
|
44
|
-
getSyncRoutine():
|
|
36
|
+
getSyncRoutine(): FreeRoutine<ED, Cxt>;
|
|
45
37
|
getSelfEndpoint(): EndpointItem<ED, Cxt>;
|
|
46
38
|
}
|
package/lib/Synchronizer.js
CHANGED
|
@@ -8,6 +8,7 @@ const path_1 = require("path");
|
|
|
8
8
|
const lodash_1 = require("oak-domain/lib/utils/lodash");
|
|
9
9
|
const filter_1 = require("oak-domain/lib/store/filter");
|
|
10
10
|
const uuid_1 = require("oak-domain/lib/utils/uuid");
|
|
11
|
+
const lodash_2 = require("lodash");
|
|
11
12
|
const OAK_SYNC_HEADER_ENTITY = 'oak-sync-entity';
|
|
12
13
|
const OAK_SYNC_HEADER_ENTITYID = 'oak-sync-entity-id';
|
|
13
14
|
class Synchronizer {
|
|
@@ -15,24 +16,21 @@ class Synchronizer {
|
|
|
15
16
|
schema;
|
|
16
17
|
remotePullInfoMap = {};
|
|
17
18
|
pullMaxBornAtMap = {};
|
|
18
|
-
|
|
19
|
+
channelDict = {};
|
|
20
|
+
contextBuilder;
|
|
19
21
|
pushAccessMap = {};
|
|
20
22
|
/**
|
|
21
23
|
* 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
|
|
22
24
|
* @param channel
|
|
23
25
|
* @param retry
|
|
24
26
|
*/
|
|
25
|
-
async startChannel(channel, retry) {
|
|
27
|
+
async startChannel(context, channel, retry) {
|
|
26
28
|
const { queue, api, selfEncryptInfo, entity, entityId } = channel;
|
|
27
|
-
channel.queue = [];
|
|
28
|
-
channel.running = true;
|
|
29
|
-
channel.nextPushTimestamp = Number.MAX_SAFE_INTEGER;
|
|
30
|
-
const opers = queue.map(ele => ele.oper);
|
|
31
|
-
let failedOpers = [];
|
|
32
|
-
let needRetry = false;
|
|
33
29
|
let json;
|
|
34
30
|
try {
|
|
35
31
|
// todo 加密
|
|
32
|
+
const queue = channel.queue;
|
|
33
|
+
const opers = queue.map(ele => ele.oper);
|
|
36
34
|
console.log('向远端结点sync数据', api, JSON.stringify(opers));
|
|
37
35
|
const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
|
|
38
36
|
const res = await fetch(finalApi, {
|
|
@@ -50,156 +48,143 @@ class Synchronizer {
|
|
|
50
48
|
json = await res.json();
|
|
51
49
|
}
|
|
52
50
|
catch (err) {
|
|
51
|
+
// 最大延迟redo时间512秒
|
|
52
|
+
const retryDelay = Math.pow(2, Math.min(9, retry)) * 1000;
|
|
53
53
|
console.error('sync push时出现error', err);
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
console.error(`将于${retryDelay}毫秒后重试`);
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
setTimeout(async () => {
|
|
57
|
+
await this.startChannel(context, channel, retry + 1);
|
|
58
|
+
resolve(undefined);
|
|
59
|
+
}, retryDelay);
|
|
60
|
+
});
|
|
56
61
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const { id, error } = failed;
|
|
65
|
-
console.error('同步过程中发生异常', id, error);
|
|
66
|
-
}
|
|
67
|
-
for (const req of queue) {
|
|
68
|
-
if (successIds.includes(req.oper.id)) {
|
|
69
|
-
req.resolve(undefined);
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
failedOpers.push(req);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
62
|
+
/**
|
|
63
|
+
* 返回结构见this.getSelfEndpoint
|
|
64
|
+
*/
|
|
65
|
+
const { successIds, failed, redundantIds } = json;
|
|
66
|
+
if (failed) {
|
|
67
|
+
const { id, error } = failed;
|
|
68
|
+
console.error('同步过程中发生异常', id, error, retry);
|
|
75
69
|
}
|
|
76
|
-
|
|
77
|
-
channel
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
let idx = 0;
|
|
85
|
-
const now = Date.now();
|
|
86
|
-
opers.forEach((oper) => {
|
|
87
|
-
for (; idx < channel.queue.length; idx++) {
|
|
88
|
-
if (channel.queue[idx].oper.id === oper.oper.id) {
|
|
89
|
-
(0, assert_1.default)(false, '不应当出现重复的oper');
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
else if (channel.queue[idx].oper.bornAt > oper.oper.bornAt) {
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
channel.queue.splice(idx, 0, oper);
|
|
97
|
-
});
|
|
98
|
-
const retryWeight = Math.pow(2, Math.min(retry, 10));
|
|
99
|
-
const nextPushTimestamp = retryWeight * 1000 + now;
|
|
100
|
-
if (channel.queue.length > 0) {
|
|
101
|
-
if (channel.running) {
|
|
102
|
-
if (channel.nextPushTimestamp > nextPushTimestamp) {
|
|
103
|
-
channel.nextPushTimestamp = nextPushTimestamp;
|
|
104
|
-
}
|
|
70
|
+
const unsuccessfulOpers = queue.filter(ele => !successIds.includes(ele.oper.id) && !redundantIds.includes(ele.oper.id));
|
|
71
|
+
// 重新开始前,可以将已经完成的oper的triggerData位清零。要注意,在多个remote配置下,有可能一个oper要推给多个channel
|
|
72
|
+
// 这里可能设计过度了,代码也没经过测试
|
|
73
|
+
channel.queue = unsuccessfulOpers;
|
|
74
|
+
const aliveOperIds = [];
|
|
75
|
+
for (const k in this.channelDict) {
|
|
76
|
+
if (this.channelDict[k].queue.length > 0) {
|
|
77
|
+
aliveOperIds.push(...this.channelDict[k].queue.map(ele => ele.oper.id));
|
|
105
78
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
79
|
+
}
|
|
80
|
+
const overIds = (0, lodash_1.difference)(successIds.concat(redundantIds), aliveOperIds);
|
|
81
|
+
if (overIds.length > 0) {
|
|
82
|
+
await context.operate('oper', {
|
|
83
|
+
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
84
|
+
action: 'update',
|
|
85
|
+
data: {
|
|
86
|
+
[types_1.TriggerDataAttribute]: null,
|
|
87
|
+
[types_1.TriggerUuidAttribute]: null,
|
|
88
|
+
},
|
|
89
|
+
filter: {
|
|
90
|
+
id: {
|
|
91
|
+
$in: overIds,
|
|
111
92
|
}
|
|
112
|
-
channel.handler = setTimeout(async () => {
|
|
113
|
-
await this.startChannel(channel, retry);
|
|
114
|
-
}, nextPushTimestamp - now);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
// 当前队列的开始时间要早于自身要求,不用管
|
|
118
|
-
(0, assert_1.default)(channel.handler);
|
|
119
93
|
}
|
|
94
|
+
}, {});
|
|
95
|
+
}
|
|
96
|
+
if (successIds.length > 0) {
|
|
97
|
+
try {
|
|
98
|
+
await Promise.all(successIds.map((id) => {
|
|
99
|
+
const { onSynchronized, oper } = queue.find(ele => ele.oper.id === id);
|
|
100
|
+
return onSynchronized && onSynchronized({
|
|
101
|
+
action: oper.action,
|
|
102
|
+
data: oper.data,
|
|
103
|
+
rowIds: (0, filter_1.getRelevantIds)(oper.filter),
|
|
104
|
+
}, context);
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// 这时候无法处理?
|
|
109
|
+
console.error('onSynchronzied时出错', err);
|
|
110
|
+
(0, assert_1.default)(false);
|
|
120
111
|
}
|
|
121
112
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
113
|
+
if (channel.queue.length > 0) {
|
|
114
|
+
// 最大延迟redo时间512秒
|
|
115
|
+
const retryDelay = Math.pow(2, Math.min(9, retry)) * 1000;
|
|
116
|
+
console.error(`有${channel.queue.length}个oper同步失败,将于${retryDelay}毫秒后重试`);
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
setTimeout(async () => {
|
|
119
|
+
await this.startChannel(context, channel, retry + 1);
|
|
120
|
+
resolve(undefined);
|
|
121
|
+
}, retryDelay);
|
|
122
|
+
});
|
|
125
123
|
}
|
|
126
124
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!this.
|
|
125
|
+
async startAllChannel(context) {
|
|
126
|
+
await Promise.all(Object.keys(this.channelDict).map(async (k) => {
|
|
127
|
+
const channel = this.channelDict[k];
|
|
128
|
+
if (channel.queue.length > 0) {
|
|
129
|
+
channel.queue.sort((o1, o2) => o1.oper.$$seq$$ - o2.oper.$$seq$$);
|
|
130
|
+
return this.startChannel(context, channel, 0);
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
pushOperToChannel(oper, userId, url, endpoint, remoteEntity, remoteEntityId, selfEncryptInfo, onSynchronized) {
|
|
135
|
+
if (!this.channelDict[userId]) {
|
|
138
136
|
// channel上缓存这些信息,暂不支持动态更新
|
|
139
|
-
this.
|
|
137
|
+
this.channelDict[userId] = {
|
|
140
138
|
api: (0, path_1.join)(url, 'endpoint', endpoint),
|
|
141
139
|
queue: [],
|
|
142
140
|
entity: remoteEntity,
|
|
143
141
|
entityId: remoteEntityId,
|
|
144
|
-
nextPushTimestamp: Number.MAX_SAFE_INTEGER,
|
|
145
|
-
running: false,
|
|
146
142
|
selfEncryptInfo,
|
|
147
143
|
};
|
|
148
144
|
}
|
|
149
|
-
|
|
145
|
+
else {
|
|
146
|
+
// 趁机更新一下加密信息
|
|
147
|
+
this.channelDict[userId].selfEncryptInfo = selfEncryptInfo;
|
|
148
|
+
}
|
|
149
|
+
const channel = this.channelDict[userId];
|
|
150
150
|
(0, assert_1.default)(channel.api === (0, path_1.join)(url, 'endpoint', endpoint));
|
|
151
151
|
(0, assert_1.default)(channel.entity === remoteEntity);
|
|
152
152
|
(0, assert_1.default)(channel.entityId === remoteEntityId);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
resolve,
|
|
157
|
-
}], 0);
|
|
153
|
+
channel.queue.push({
|
|
154
|
+
oper,
|
|
155
|
+
onSynchronized,
|
|
158
156
|
});
|
|
159
|
-
await promise;
|
|
160
157
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
bornAt: 1,
|
|
186
|
-
$$createAt$$: 1,
|
|
187
|
-
filter: 1,
|
|
188
|
-
},
|
|
189
|
-
filter: {
|
|
190
|
-
id,
|
|
191
|
-
}
|
|
192
|
-
}, { dontCollect: true, forUpdate: true });
|
|
193
|
-
const { operatorId, targetEntity, operEntity$oper: operEntities, action, data } = oper;
|
|
194
|
-
const entityIds = operEntities.map(ele => ele.entityId);
|
|
158
|
+
refineOperData(oper, rowIds) {
|
|
159
|
+
const { action, id, targetEntity, data, $$seq$$, filter } = oper;
|
|
160
|
+
const data2 = (action === 'create' && data instanceof Array) ? data.filter(ele => rowIds.includes(ele.id)) : data;
|
|
161
|
+
// 过滤掉数据中的跨事务trigger信息
|
|
162
|
+
if (data2 instanceof Array) {
|
|
163
|
+
data2.forEach((d) => {
|
|
164
|
+
(0, lodash_2.unset)(d, types_1.TriggerDataAttribute);
|
|
165
|
+
(0, lodash_2.unset)(d, types_1.TriggerUuidAttribute);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
(0, lodash_2.unset)(data2, types_1.TriggerDataAttribute);
|
|
170
|
+
(0, lodash_2.unset)(data2, types_1.TriggerUuidAttribute);
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
id, action, targetEntity, data: data2, $$seq$$, filter,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async dispatchOperToChannels(oper, context) {
|
|
177
|
+
const { operatorId, targetEntity, filter, action, data } = oper;
|
|
178
|
+
const entityIds = (0, filter_1.getRelevantIds)(filter);
|
|
179
|
+
(0, assert_1.default)(entityIds.length > 0);
|
|
195
180
|
const pushEntityNodes = this.pushAccessMap[targetEntity];
|
|
181
|
+
let pushed = false;
|
|
196
182
|
if (pushEntityNodes && pushEntityNodes.length > 0) {
|
|
197
183
|
// 每个pushEntityNode代表配置的一个remoteEntity
|
|
198
184
|
await Promise.all(pushEntityNodes.map(async (node) => {
|
|
199
|
-
const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, actions, onSynchronized } = node;
|
|
185
|
+
const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, groupBySelfEntity, endpoint, actions, onSynchronized } = node;
|
|
200
186
|
// 定义中应该不可能没有actions
|
|
201
187
|
if (!actions || actions.includes(action)) {
|
|
202
|
-
const pushed = [];
|
|
203
188
|
const rows = await context.select(targetEntity, {
|
|
204
189
|
data: {
|
|
205
190
|
id: 1,
|
|
@@ -213,60 +198,117 @@ class Synchronizer {
|
|
|
213
198
|
}, { dontCollect: true, includedDeleted: true });
|
|
214
199
|
// userId就是需要发送给远端的user,但是要将本次操作的user过滤掉(操作的原本产生者)
|
|
215
200
|
const userSendDict = groupByUsers(rows);
|
|
201
|
+
const selfEntityIdDict = groupBySelfEntity(rows);
|
|
202
|
+
const encryptInfoDict = {};
|
|
216
203
|
const pushToUserIdFn = async (userId) => {
|
|
217
204
|
const { entity, entityId, rowIds } = userSendDict[userId];
|
|
205
|
+
const selfEntityIds = rowIds.map((rowId) => selfEntityIdDict[rowId]);
|
|
206
|
+
const uniqSelfEntityIds = (0, lodash_2.uniq)(selfEntityIds);
|
|
207
|
+
(0, assert_1.default)(uniqSelfEntityIds.length === 1, '推向同一个userId的oper不可能关联在多个不同的selfEntity行上');
|
|
208
|
+
const selfEntityId = uniqSelfEntityIds[0];
|
|
209
|
+
if (!encryptInfoDict[selfEntityId]) {
|
|
210
|
+
encryptInfoDict[selfEntityId] = await this.config.self.getSelfEncryptInfo(context, selfEntityId);
|
|
211
|
+
}
|
|
212
|
+
const selfEncryptInfo = encryptInfoDict[selfEntityId];
|
|
218
213
|
// 推送到远端结点的oper
|
|
219
|
-
const oper2 =
|
|
220
|
-
id: oper.id,
|
|
221
|
-
action: action,
|
|
222
|
-
data: (action === 'create' && data instanceof Array) ? data.filter(ele => rowIds.includes(ele.id)) : data,
|
|
223
|
-
filter: {
|
|
224
|
-
id: rowIds.length === 1 ? rowIds[0] : {
|
|
225
|
-
$in: rowIds,
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
bornAt: oper.bornAt,
|
|
229
|
-
targetEntity,
|
|
230
|
-
};
|
|
214
|
+
const oper2 = this.refineOperData(oper, rowIds);
|
|
231
215
|
const { url } = await getRemoteAccessInfo(context, {
|
|
232
216
|
userId,
|
|
233
217
|
remoteEntityId: entityId,
|
|
234
218
|
});
|
|
235
|
-
|
|
219
|
+
this.pushOperToChannel(oper2, userId, url, endpoint, entity, entityId, selfEncryptInfo, onSynchronized);
|
|
236
220
|
};
|
|
237
221
|
for (const userId in userSendDict) {
|
|
238
222
|
if (userId !== operatorId) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
if (pushed.length > 0) {
|
|
243
|
-
// 对单个oper,这里必须要等所有的push返回,不然会一直等在上面
|
|
244
|
-
await Promise.all(pushed);
|
|
245
|
-
if (onSynchronized) {
|
|
246
|
-
await onSynchronized({
|
|
247
|
-
action: action,
|
|
248
|
-
data: data,
|
|
249
|
-
rowIds: entityIds,
|
|
250
|
-
}, context);
|
|
223
|
+
await pushToUserIdFn(userId);
|
|
224
|
+
pushed = true;
|
|
251
225
|
}
|
|
252
226
|
}
|
|
253
227
|
}
|
|
254
228
|
}));
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
229
|
+
}
|
|
230
|
+
// 如果oper一个也不用推送,说明其定义的推送path和对象行的path不匹配(动态指针)
|
|
231
|
+
return pushed;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 为了保证推送的oper序,采用从database中顺序读取所有需要推送的oper来进行推送
|
|
235
|
+
* 每个进程都保证把当前所有的oper顺序处理掉,就不会有乱序的问题,大家通过database上的锁来完成同步
|
|
236
|
+
* @param context
|
|
237
|
+
*/
|
|
238
|
+
async trySynchronizeOpers() {
|
|
239
|
+
const context = await this.contextBuilder();
|
|
240
|
+
await context.begin();
|
|
241
|
+
try {
|
|
242
|
+
let dirtyOpers = await context.select('oper', {
|
|
260
243
|
data: {
|
|
261
|
-
|
|
262
|
-
[types_1.TriggerUuidAttribute]: null,
|
|
244
|
+
id: 1,
|
|
263
245
|
},
|
|
264
246
|
filter: {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
247
|
+
[types_1.TriggerDataAttribute]: {
|
|
248
|
+
$exists: true,
|
|
249
|
+
},
|
|
250
|
+
}
|
|
251
|
+
}, { dontCollect: true });
|
|
252
|
+
if (dirtyOpers.length > 0) {
|
|
253
|
+
// 这一步是加锁,保证只有一个进程完成推送,推送者提交前会将$$triggerData$$清零
|
|
254
|
+
const ids = dirtyOpers.map(ele => ele.id);
|
|
255
|
+
dirtyOpers = await context.select('oper', {
|
|
256
|
+
data: {
|
|
257
|
+
id: 1,
|
|
258
|
+
action: 1,
|
|
259
|
+
data: 1,
|
|
260
|
+
targetEntity: 1,
|
|
261
|
+
operatorId: 1,
|
|
262
|
+
[types_1.TriggerDataAttribute]: 1,
|
|
263
|
+
bornAt: 1,
|
|
264
|
+
$$createAt$$: 1,
|
|
265
|
+
$$seq$$: 1,
|
|
266
|
+
filter: 1,
|
|
267
|
+
},
|
|
268
|
+
filter: {
|
|
269
|
+
id: { $in: ids },
|
|
270
|
+
},
|
|
271
|
+
}, { dontCollect: true, forUpdate: true });
|
|
272
|
+
dirtyOpers = dirtyOpers.filter(ele => !!ele[types_1.TriggerDataAttribute]);
|
|
273
|
+
if (dirtyOpers.length > 0) {
|
|
274
|
+
const pushedIds = [];
|
|
275
|
+
const unpushedIds = [];
|
|
276
|
+
await Promise.all(dirtyOpers.map(async (oper) => {
|
|
277
|
+
const result = await this.dispatchOperToChannels(oper, context);
|
|
278
|
+
if (result) {
|
|
279
|
+
pushedIds.push(oper.id);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
unpushedIds.push(oper.id);
|
|
283
|
+
}
|
|
284
|
+
}));
|
|
285
|
+
if (unpushedIds.length > 0) {
|
|
286
|
+
await context.operate('oper', {
|
|
287
|
+
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
288
|
+
action: 'update',
|
|
289
|
+
data: {
|
|
290
|
+
[types_1.TriggerDataAttribute]: null,
|
|
291
|
+
[types_1.TriggerUuidAttribute]: null,
|
|
292
|
+
},
|
|
293
|
+
filter: {
|
|
294
|
+
id: {
|
|
295
|
+
$in: unpushedIds,
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}, {});
|
|
299
|
+
}
|
|
300
|
+
if (pushedIds.length > 0) {
|
|
301
|
+
await this.startAllChannel(context);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
await context.commit();
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
await context.rollback();
|
|
309
|
+
console.error(err);
|
|
310
|
+
throw err;
|
|
268
311
|
}
|
|
269
|
-
return 0;
|
|
270
312
|
}
|
|
271
313
|
makeCreateOperTrigger() {
|
|
272
314
|
const { config } = this;
|
|
@@ -278,21 +320,24 @@ class Synchronizer {
|
|
|
278
320
|
const pushEntities = [];
|
|
279
321
|
const endpoint2 = (0, path_1.join)(endpoint || 'sync', self.entity);
|
|
280
322
|
for (const def of pushEntityDefs) {
|
|
281
|
-
const {
|
|
323
|
+
const { pathToRemoteEntity, pathToSelfEntity, relationName, recursive, entity, actions, onSynchronized } = def;
|
|
282
324
|
pushEntities.push(entity);
|
|
283
325
|
const relationName2 = relationName || rnRemote;
|
|
284
|
-
const path2 = pathToUser ? `${
|
|
326
|
+
const path2 = pathToUser ? `${pathToRemoteEntity}.${pathToUser}` : pathToRemoteEntity;
|
|
327
|
+
(0, assert_1.default)(!recursive);
|
|
285
328
|
const { projection, getData } = relationName2 ? (0, relationPath_1.destructRelationPath)(this.schema, entity, path2, {
|
|
286
329
|
relation: {
|
|
287
330
|
name: relationName,
|
|
288
331
|
}
|
|
289
|
-
}, recursive) : (0, relationPath_1.
|
|
332
|
+
}, recursive) : (0, relationPath_1.destructDirectUserPath)(this.schema, entity, path2);
|
|
333
|
+
const toSelfEntity = (0, relationPath_1.destructDirectPath)(this.schema, entity, pathToSelfEntity);
|
|
290
334
|
const groupByUsers = (rows) => {
|
|
291
335
|
const userRowDict = {};
|
|
292
336
|
rows.forEach((row) => {
|
|
293
337
|
const goals = getData(row);
|
|
294
338
|
if (goals) {
|
|
295
339
|
goals.forEach(({ entity, entityId, userId }) => {
|
|
340
|
+
(0, assert_1.default)(userId);
|
|
296
341
|
if (userRowDict[userId]) {
|
|
297
342
|
// 逻辑上来说同一个userId,其关联的entity和entityId必然相同,这个entity/entityId代表了对方
|
|
298
343
|
(0, assert_1.default)(userRowDict[userId].entity === entity && userRowDict[userId].entityId === entityId);
|
|
@@ -310,10 +355,28 @@ class Synchronizer {
|
|
|
310
355
|
});
|
|
311
356
|
return userRowDict;
|
|
312
357
|
};
|
|
358
|
+
const projectionMerged = (0, lodash_2.merge)(projection, toSelfEntity.projection);
|
|
359
|
+
const groupBySelfEntity = (rows) => {
|
|
360
|
+
const selfEntityIdDict = {};
|
|
361
|
+
for (const row of rows) {
|
|
362
|
+
const selfEntityInfo = toSelfEntity.getData(row, pathToSelfEntity);
|
|
363
|
+
if (selfEntityInfo) {
|
|
364
|
+
const selfEntityIds = selfEntityInfo.map((info) => {
|
|
365
|
+
(0, assert_1.default)(info.entity === this.config.self.entity);
|
|
366
|
+
return info.data.id;
|
|
367
|
+
});
|
|
368
|
+
const uniqSelfEntityIds = (0, lodash_2.uniq)(selfEntityIds);
|
|
369
|
+
(0, assert_1.default)(uniqSelfEntityIds.length === 1, '同一行数据不可能关联在两行selfEntity上');
|
|
370
|
+
selfEntityIdDict[row.id] = uniqSelfEntityIds[0];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return selfEntityIdDict;
|
|
374
|
+
};
|
|
313
375
|
if (!this.pushAccessMap[entity]) {
|
|
314
376
|
this.pushAccessMap[entity] = [{
|
|
315
|
-
projection,
|
|
377
|
+
projection: projectionMerged,
|
|
316
378
|
groupByUsers,
|
|
379
|
+
groupBySelfEntity,
|
|
317
380
|
getRemotePushInfo: getPushInfo,
|
|
318
381
|
endpoint: endpoint2,
|
|
319
382
|
entity,
|
|
@@ -325,6 +388,7 @@ class Synchronizer {
|
|
|
325
388
|
this.pushAccessMap[entity].push({
|
|
326
389
|
projection,
|
|
327
390
|
groupByUsers,
|
|
391
|
+
groupBySelfEntity,
|
|
328
392
|
getRemotePushInfo: getPushInfo,
|
|
329
393
|
endpoint: endpoint2,
|
|
330
394
|
entity,
|
|
@@ -349,18 +413,19 @@ class Synchronizer {
|
|
|
349
413
|
return pushEntities.includes(data.targetEntity)
|
|
350
414
|
&& !!this.pushAccessMap[targetEntity].find(({ actions }) => !actions || actions.includes(action));
|
|
351
415
|
},
|
|
352
|
-
fn: async ({ ids }
|
|
416
|
+
fn: async ({ ids }) => {
|
|
353
417
|
(0, assert_1.default)(ids.length === 1);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
throw new types_1.
|
|
418
|
+
this.trySynchronizeOpers();
|
|
419
|
+
// 内部自主处理triggerData,因此不需要让triggerExecutor处理
|
|
420
|
+
throw new types_1.OakMakeSureByMySelfException();
|
|
357
421
|
}
|
|
358
422
|
};
|
|
359
423
|
return createOperTrigger;
|
|
360
424
|
}
|
|
361
|
-
constructor(config, schema) {
|
|
425
|
+
constructor(config, schema, contextBuilder) {
|
|
362
426
|
this.config = config;
|
|
363
427
|
this.schema = schema;
|
|
428
|
+
this.contextBuilder = contextBuilder;
|
|
364
429
|
}
|
|
365
430
|
/**
|
|
366
431
|
* 根据sync的定义,生成对应的 commit triggers
|
|
@@ -372,26 +437,10 @@ class Synchronizer {
|
|
|
372
437
|
getSyncRoutine() {
|
|
373
438
|
return {
|
|
374
439
|
name: 'checkpoint routine for sync',
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
[types_1.TriggerDataAttribute]: {
|
|
378
|
-
$exists: true,
|
|
379
|
-
}
|
|
380
|
-
},
|
|
381
|
-
projection: {
|
|
382
|
-
id: 1,
|
|
383
|
-
[types_1.TriggerDataAttribute]: 1,
|
|
384
|
-
},
|
|
385
|
-
fn: async (context, data) => {
|
|
386
|
-
for (const ele of data) {
|
|
387
|
-
const { id, [types_1.TriggerDataAttribute]: triggerData } = ele;
|
|
388
|
-
const { cxtStr = '{}' } = triggerData;
|
|
389
|
-
await context.initialize(JSON.parse(cxtStr), true);
|
|
390
|
-
const selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
|
|
391
|
-
this.synchronizeOpersToRemote(id, context, selfEncryptInfo);
|
|
392
|
-
}
|
|
440
|
+
routine: async () => {
|
|
441
|
+
this.trySynchronizeOpers();
|
|
393
442
|
return {};
|
|
394
|
-
}
|
|
443
|
+
},
|
|
395
444
|
};
|
|
396
445
|
}
|
|
397
446
|
getSelfEndpoint() {
|
|
@@ -405,6 +454,7 @@ class Synchronizer {
|
|
|
405
454
|
const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITYID]: meEntityId } = headers;
|
|
406
455
|
console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
|
|
407
456
|
const successIds = [];
|
|
457
|
+
const redundantIds = [];
|
|
408
458
|
let failed;
|
|
409
459
|
// todo 这里先缓存,不考虑本身同步相关信息的更新
|
|
410
460
|
if (!this.remotePullInfoMap[entity]) {
|
|
@@ -456,12 +506,12 @@ class Synchronizer {
|
|
|
456
506
|
}
|
|
457
507
|
let maxBornAt = this.pullMaxBornAtMap[entityId];
|
|
458
508
|
const opers = body;
|
|
459
|
-
const
|
|
460
|
-
const freshOpers = opers.filter(ele => ele
|
|
509
|
+
const staleOpers = opers.filter(ele => ele.$$seq$$ <= maxBornAt);
|
|
510
|
+
const freshOpers = opers.filter(ele => ele.$$seq$$ > maxBornAt);
|
|
461
511
|
await Promise.all([
|
|
462
512
|
// 无法严格保证推送按bornAt,所以一旦还有outdatedOpers,检查其已经被apply
|
|
463
513
|
(async () => {
|
|
464
|
-
const ids =
|
|
514
|
+
const ids = staleOpers.map(ele => ele.id);
|
|
465
515
|
if (ids.length > 0) {
|
|
466
516
|
const opersExisted = await context.select('oper', {
|
|
467
517
|
data: {
|
|
@@ -478,13 +528,13 @@ class Synchronizer {
|
|
|
478
528
|
// todo 这里如果远端业务逻辑严格,发生乱序应是无关的oper,直接执行就好 by Xc
|
|
479
529
|
throw new Error(`在sync过程中发现有丢失的oper数据「${missed}」`);
|
|
480
530
|
}
|
|
481
|
-
|
|
531
|
+
redundantIds.push(...ids);
|
|
482
532
|
}
|
|
483
533
|
})(),
|
|
484
534
|
(async () => {
|
|
485
535
|
for (const freshOper of freshOpers) {
|
|
486
|
-
// freshOpers
|
|
487
|
-
const { id, targetEntity, action, data,
|
|
536
|
+
// freshOpers是按$$seq$$序产生的
|
|
537
|
+
const { id, targetEntity, action, data, $$seq$$, filter } = freshOper;
|
|
488
538
|
const ids = (0, filter_1.getRelevantIds)(filter);
|
|
489
539
|
(0, assert_1.default)(ids.length > 0);
|
|
490
540
|
try {
|
|
@@ -503,11 +553,11 @@ class Synchronizer {
|
|
|
503
553
|
$in: ids,
|
|
504
554
|
},
|
|
505
555
|
},
|
|
506
|
-
bornAt:
|
|
556
|
+
bornAt: $$seq$$,
|
|
507
557
|
};
|
|
508
558
|
await context.operate(targetEntity, operation, {});
|
|
509
559
|
successIds.push(id);
|
|
510
|
-
maxBornAt =
|
|
560
|
+
maxBornAt = $$seq$$;
|
|
511
561
|
}
|
|
512
562
|
catch (err) {
|
|
513
563
|
console.error(err);
|
|
@@ -525,6 +575,7 @@ class Synchronizer {
|
|
|
525
575
|
return {
|
|
526
576
|
successIds,
|
|
527
577
|
failed,
|
|
578
|
+
redundantIds,
|
|
528
579
|
};
|
|
529
580
|
}
|
|
530
581
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oak-backend-base",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.2",
|
|
4
4
|
"description": "oak-backend-base",
|
|
5
5
|
"main": "lib/index",
|
|
6
6
|
"author": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"node-schedule": "^2.1.0",
|
|
23
23
|
"oak-common-aspect": "^2.3.0",
|
|
24
24
|
"oak-db": "^3.2.0",
|
|
25
|
-
"oak-domain": "^4.
|
|
26
|
-
"oak-frontend-base": "^4.
|
|
25
|
+
"oak-domain": "^4.4.0",
|
|
26
|
+
"oak-frontend-base": "^4.4.0",
|
|
27
27
|
"socket.io": "^4.7.2",
|
|
28
28
|
"socket.io-client": "^4.7.2",
|
|
29
29
|
"uuid": "^8.3.2"
|