oak-backend-base 3.3.2 → 3.3.4
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 +23 -64
- package/lib/Synchronizer.d.ts +15 -7
- package/lib/Synchronizer.js +257 -180
- 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 synchronizers?: 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
|
+
synchronizers;
|
|
25
25
|
contextBuilder;
|
|
26
26
|
requireSth(filePath) {
|
|
27
27
|
const depFilePath = (0, path_1.join)(this.path, filePath);
|
|
@@ -101,10 +101,10 @@ class AppLoader extends types_1.AppLoader {
|
|
|
101
101
|
const dbConfigFile = (0, path_1.join)(this.path, 'configuration', 'mysql.json');
|
|
102
102
|
const dbConfig = require(dbConfigFile);
|
|
103
103
|
const syncConfigFile = (0, path_1.join)(this.path, 'lib', 'configuration', 'sync.js');
|
|
104
|
-
const
|
|
104
|
+
const syncConfigs = (0, fs_1.existsSync)(syncConfigFile) && require(syncConfigFile).default;
|
|
105
105
|
return {
|
|
106
106
|
dbConfig: dbConfig,
|
|
107
|
-
|
|
107
|
+
syncConfigs: syncConfigs,
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
constructor(path, contextBuilder, ns, nsServer) {
|
|
@@ -149,70 +149,20 @@ 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.
|
|
152
|
+
if (this.synchronizers) {
|
|
153
153
|
// 同步数据到远端结点通过commit trigger来完成
|
|
154
|
-
const
|
|
155
|
-
|
|
154
|
+
for (const synchronizer of this.synchronizers) {
|
|
155
|
+
const syncTriggers = synchronizer.getSyncTriggers();
|
|
156
|
+
syncTriggers.forEach((trigger) => this.registerTrigger(trigger));
|
|
157
|
+
}
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
160
|
async mount(initialize) {
|
|
159
161
|
const { path } = this;
|
|
160
162
|
if (!initialize) {
|
|
161
|
-
const {
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
const { getSelfEncryptInfo, ...restSelf } = self;
|
|
165
|
-
this.synchronizer = new Synchronizer_1.default({
|
|
166
|
-
self: {
|
|
167
|
-
// entity: self.entity,
|
|
168
|
-
getSelfEncryptInfo: async () => {
|
|
169
|
-
const context = await this.contextBuilder()(this.dbStore);
|
|
170
|
-
await context.begin();
|
|
171
|
-
try {
|
|
172
|
-
const result = await self.getSelfEncryptInfo(context);
|
|
173
|
-
await context.commit();
|
|
174
|
-
return result;
|
|
175
|
-
}
|
|
176
|
-
catch (err) {
|
|
177
|
-
await context.rollback();
|
|
178
|
-
throw err;
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
...restSelf
|
|
182
|
-
},
|
|
183
|
-
remotes: remotes.map((r) => {
|
|
184
|
-
const { getPushInfo, getPullInfo, ...rest } = r;
|
|
185
|
-
return {
|
|
186
|
-
getRemotePushInfo: async (id) => {
|
|
187
|
-
const context = await this.contextBuilder()(this.dbStore);
|
|
188
|
-
await context.begin();
|
|
189
|
-
try {
|
|
190
|
-
const result = await getPushInfo(id, context);
|
|
191
|
-
await context.commit();
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
catch (err) {
|
|
195
|
-
await context.rollback();
|
|
196
|
-
throw err;
|
|
197
|
-
}
|
|
198
|
-
},
|
|
199
|
-
getRemotePullInfo: async (userId) => {
|
|
200
|
-
const context = await this.contextBuilder()(this.dbStore);
|
|
201
|
-
await context.begin();
|
|
202
|
-
try {
|
|
203
|
-
const result = await getPullInfo(userId, context);
|
|
204
|
-
await context.commit();
|
|
205
|
-
return result;
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
await context.rollback();
|
|
209
|
-
throw err;
|
|
210
|
-
}
|
|
211
|
-
},
|
|
212
|
-
...rest,
|
|
213
|
-
};
|
|
214
|
-
})
|
|
215
|
-
}, this.dbStore.getSchema());
|
|
163
|
+
const { syncConfigs } = this.getConfiguration();
|
|
164
|
+
if (syncConfigs) {
|
|
165
|
+
this.synchronizers = syncConfigs.map(config => new Synchronizer_1.default(config, this.dbStore.getSchema()));
|
|
216
166
|
}
|
|
217
167
|
this.initTriggers();
|
|
218
168
|
}
|
|
@@ -325,9 +275,11 @@ class AppLoader extends types_1.AppLoader {
|
|
|
325
275
|
transformEndpointItem(router, item);
|
|
326
276
|
}
|
|
327
277
|
}
|
|
328
|
-
if (this.
|
|
329
|
-
|
|
330
|
-
|
|
278
|
+
if (this.synchronizers) {
|
|
279
|
+
this.synchronizers.forEach((synchronizer) => {
|
|
280
|
+
const syncEp = synchronizer.getSelfEndpoint();
|
|
281
|
+
transformEndpointItem(syncEp.name, syncEp);
|
|
282
|
+
});
|
|
331
283
|
}
|
|
332
284
|
return endPointRouters;
|
|
333
285
|
}
|
|
@@ -476,6 +428,13 @@ class AppLoader extends types_1.AppLoader {
|
|
|
476
428
|
}
|
|
477
429
|
}
|
|
478
430
|
}
|
|
431
|
+
if (this.synchronizers) {
|
|
432
|
+
this.synchronizers.forEach((synchronizer) => {
|
|
433
|
+
// 这个routine在内部处理异步
|
|
434
|
+
const routine = synchronizer.getSyncRoutine();
|
|
435
|
+
this.execWatcher(routine);
|
|
436
|
+
});
|
|
437
|
+
}
|
|
479
438
|
}
|
|
480
439
|
async execRoutine(routine) {
|
|
481
440
|
const context = await this.makeContext();
|
package/lib/Synchronizer.d.ts
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { EntityDict, StorageSchema, EndpointItem } from 'oak-domain/lib/types';
|
|
1
|
+
import { EntityDict, StorageSchema, EndpointItem, SyncConfig, Watcher } 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';
|
|
5
|
-
import { SyncConfigWrapper } from './types/Sync';
|
|
6
5
|
export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> {
|
|
7
6
|
private config;
|
|
8
7
|
private schema;
|
|
9
|
-
private selfEncryptInfo?;
|
|
10
8
|
private remotePullInfoMap;
|
|
11
9
|
private pullMaxBornAtMap;
|
|
12
10
|
private remotePushChannel;
|
|
11
|
+
private pushAccessMap;
|
|
13
12
|
/**
|
|
14
13
|
* 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
|
|
15
14
|
* @param channel
|
|
16
15
|
* @param retry
|
|
17
16
|
*/
|
|
18
|
-
private
|
|
17
|
+
private startChannel;
|
|
18
|
+
private joinChannel;
|
|
19
19
|
/**
|
|
20
20
|
* 推向远端Node的oper,需要严格保证按产生的时间序推送。根据幂等原理,这里必须要推送成功
|
|
21
21
|
* 因此在这里要实现两点:
|
|
@@ -25,14 +25,22 @@ export default class Synchronizer<ED extends EntityDict & BaseEntityDict, Cxt ex
|
|
|
25
25
|
* 其实这里还无法严格保证先产生的oper一定先到达被推送,因为volatile trigger是在事务提交后再发生的,但这种情况在目前应该跑不出来,在实际执行oper的时候assert掉先。by Xc 20240226
|
|
26
26
|
*/
|
|
27
27
|
private pushOper;
|
|
28
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 因为应用可能是多租户,得提前确定context下的selfEncryptInfo
|
|
30
|
+
* 由于checkpoint时无法区别不同上下文之间的未完成oper数据,所以接口只能这样设计
|
|
31
|
+
* @param id
|
|
32
|
+
* @param context
|
|
33
|
+
* @param selfEncryptInfo
|
|
34
|
+
* @returns
|
|
35
|
+
*/
|
|
36
|
+
private synchronizeOpersToRemote;
|
|
29
37
|
private makeCreateOperTrigger;
|
|
30
|
-
constructor(config:
|
|
38
|
+
constructor(config: SyncConfig<ED, Cxt>, schema: StorageSchema<ED>);
|
|
31
39
|
/**
|
|
32
40
|
* 根据sync的定义,生成对应的 commit triggers
|
|
33
41
|
* @returns
|
|
34
42
|
*/
|
|
35
43
|
getSyncTriggers(): VolatileTrigger<ED, keyof ED, Cxt>[];
|
|
36
|
-
|
|
44
|
+
getSyncRoutine(): Watcher<ED, keyof ED, Cxt>;
|
|
37
45
|
getSelfEndpoint(): EndpointItem<ED, Cxt>;
|
|
38
46
|
}
|
package/lib/Synchronizer.js
CHANGED
|
@@ -1,55 +1,58 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const tslib_1 = require("tslib");
|
|
4
|
+
const types_1 = require("oak-domain/lib/types");
|
|
4
5
|
const relationPath_1 = require("oak-domain/lib/utils/relationPath");
|
|
5
6
|
const assert_1 = tslib_1.__importDefault(require("assert"));
|
|
6
7
|
const path_1 = require("path");
|
|
7
8
|
const lodash_1 = require("oak-domain/lib/utils/lodash");
|
|
8
9
|
const filter_1 = require("oak-domain/lib/store/filter");
|
|
9
|
-
const
|
|
10
|
+
const uuid_1 = require("oak-domain/lib/utils/uuid");
|
|
11
|
+
const OAK_SYNC_HEADER_ENTITY = 'oak-sync-entity';
|
|
12
|
+
const OAK_SYNC_HEADER_ENTITYID = 'oak-sync-entity-id';
|
|
10
13
|
class Synchronizer {
|
|
11
14
|
config;
|
|
12
15
|
schema;
|
|
13
|
-
selfEncryptInfo;
|
|
14
16
|
remotePullInfoMap = {};
|
|
15
17
|
pullMaxBornAtMap = {};
|
|
16
18
|
remotePushChannel = {};
|
|
19
|
+
pushAccessMap = {};
|
|
17
20
|
/**
|
|
18
21
|
* 向某一个远端对象push opers。根据幂等性,这里如果失败了必须反复推送
|
|
19
22
|
* @param channel
|
|
20
23
|
* @param retry
|
|
21
24
|
*/
|
|
22
|
-
async
|
|
23
|
-
const { queue, api,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
channel.nextPushTimestamp = nextPushTimestamp2 * 1000 + Date.now();
|
|
25
|
+
async startChannel(channel, retry) {
|
|
26
|
+
const { queue, api, selfEncryptInfo, entity, entityId } = channel;
|
|
27
|
+
channel.queue = [];
|
|
28
|
+
channel.running = true;
|
|
29
|
+
channel.nextPushTimestamp = Number.MAX_SAFE_INTEGER;
|
|
28
30
|
const opers = queue.map(ele => ele.oper);
|
|
29
|
-
let
|
|
31
|
+
let failedOpers = [];
|
|
30
32
|
let needRetry = false;
|
|
31
33
|
let json;
|
|
32
34
|
try {
|
|
33
35
|
// todo 加密
|
|
34
|
-
const selfEncryptInfo = await this.getSelfEncryptInfo();
|
|
35
36
|
console.log('向远端结点sync数据', api, JSON.stringify(opers));
|
|
36
|
-
const
|
|
37
|
+
const finalApi = (0, path_1.join)(api, selfEncryptInfo.id);
|
|
38
|
+
const res = await fetch(finalApi, {
|
|
37
39
|
method: 'post',
|
|
38
40
|
headers: {
|
|
39
41
|
'Content-Type': 'application/json',
|
|
40
|
-
[
|
|
42
|
+
[OAK_SYNC_HEADER_ENTITY]: entity,
|
|
43
|
+
[OAK_SYNC_HEADER_ENTITYID]: entityId,
|
|
41
44
|
},
|
|
42
45
|
body: JSON.stringify(opers),
|
|
43
46
|
});
|
|
44
47
|
if (res.status !== 200) {
|
|
45
|
-
throw new Error(`sync数据时,访问api「${
|
|
48
|
+
throw new Error(`sync数据时,访问api「${finalApi}」的结果不是200。「${res.status}」`);
|
|
46
49
|
}
|
|
47
50
|
json = await res.json();
|
|
48
51
|
}
|
|
49
52
|
catch (err) {
|
|
50
53
|
console.error('sync push时出现error', err);
|
|
51
54
|
needRetry = true;
|
|
52
|
-
|
|
55
|
+
failedOpers = queue;
|
|
53
56
|
}
|
|
54
57
|
if (!needRetry) {
|
|
55
58
|
/**
|
|
@@ -63,22 +66,62 @@ class Synchronizer {
|
|
|
63
66
|
}
|
|
64
67
|
for (const req of queue) {
|
|
65
68
|
if (successIds.includes(req.oper.id)) {
|
|
66
|
-
req.resolve();
|
|
69
|
+
req.resolve(undefined);
|
|
67
70
|
}
|
|
68
71
|
else {
|
|
69
|
-
|
|
72
|
+
failedOpers.push(req);
|
|
70
73
|
}
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
channel.running = false;
|
|
77
|
+
channel.handler = undefined;
|
|
78
|
+
const retry2 = retry + 1;
|
|
79
|
+
console.log('need retry', retry2);
|
|
80
|
+
this.joinChannel(channel, failedOpers, retry2);
|
|
81
|
+
}
|
|
82
|
+
joinChannel(channel, opers, retry) {
|
|
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
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (channel.nextPushTimestamp > nextPushTimestamp) {
|
|
108
|
+
channel.nextPushTimestamp = nextPushTimestamp;
|
|
109
|
+
if (channel.handler) {
|
|
110
|
+
clearTimeout(channel.handler);
|
|
111
|
+
}
|
|
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
|
+
}
|
|
120
|
+
}
|
|
78
121
|
}
|
|
79
122
|
else {
|
|
80
123
|
channel.handler = undefined;
|
|
81
|
-
channel.nextPushTimestamp =
|
|
124
|
+
channel.nextPushTimestamp = Number.MAX_SAFE_INTEGER;
|
|
82
125
|
}
|
|
83
126
|
}
|
|
84
127
|
// 将产生的oper推送到远端Node。注意要尽量在本地阻止重复推送
|
|
@@ -90,72 +133,149 @@ class Synchronizer {
|
|
|
90
133
|
*
|
|
91
134
|
* 其实这里还无法严格保证先产生的oper一定先到达被推送,因为volatile trigger是在事务提交后再发生的,但这种情况在目前应该跑不出来,在实际执行oper的时候assert掉先。by Xc 20240226
|
|
92
135
|
*/
|
|
93
|
-
async pushOper(oper, userId, url, endpoint,
|
|
136
|
+
async pushOper(oper, userId, url, endpoint, remoteEntity, remoteEntityId, selfEncryptInfo) {
|
|
94
137
|
if (!this.remotePushChannel[userId]) {
|
|
138
|
+
// channel上缓存这些信息,暂不支持动态更新
|
|
95
139
|
this.remotePushChannel[userId] = {
|
|
96
140
|
api: (0, path_1.join)(url, 'endpoint', endpoint),
|
|
97
141
|
queue: [],
|
|
142
|
+
entity: remoteEntity,
|
|
143
|
+
entityId: remoteEntityId,
|
|
144
|
+
nextPushTimestamp: Number.MAX_SAFE_INTEGER,
|
|
145
|
+
running: false,
|
|
146
|
+
selfEncryptInfo,
|
|
98
147
|
};
|
|
99
148
|
}
|
|
100
149
|
const channel = this.remotePushChannel[userId];
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
150
|
+
(0, assert_1.default)(channel.api === (0, path_1.join)(url, 'endpoint', endpoint));
|
|
151
|
+
(0, assert_1.default)(channel.entity === remoteEntity);
|
|
152
|
+
(0, assert_1.default)(channel.entityId === remoteEntityId);
|
|
153
|
+
const promise = new Promise((resolve) => {
|
|
154
|
+
this.joinChannel(channel, [{
|
|
155
|
+
oper,
|
|
156
|
+
resolve,
|
|
157
|
+
}], 0);
|
|
158
|
+
});
|
|
159
|
+
await promise;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 因为应用可能是多租户,得提前确定context下的selfEncryptInfo
|
|
163
|
+
* 由于checkpoint时无法区别不同上下文之间的未完成oper数据,所以接口只能这样设计
|
|
164
|
+
* @param id
|
|
165
|
+
* @param context
|
|
166
|
+
* @param selfEncryptInfo
|
|
167
|
+
* @returns
|
|
168
|
+
*/
|
|
169
|
+
async synchronizeOpersToRemote(id, context, selfEncryptInfo) {
|
|
170
|
+
const [oper] = await context.select('oper', {
|
|
171
|
+
data: {
|
|
172
|
+
id: 1,
|
|
173
|
+
action: 1,
|
|
174
|
+
data: 1,
|
|
175
|
+
targetEntity: 1,
|
|
176
|
+
operatorId: 1,
|
|
177
|
+
operEntity$oper: {
|
|
178
|
+
$entity: 'operEntity',
|
|
179
|
+
data: {
|
|
180
|
+
id: 1,
|
|
181
|
+
entity: 1,
|
|
182
|
+
entityId: 1,
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
bornAt: 1,
|
|
186
|
+
$$createAt$$: 1,
|
|
187
|
+
filter: 1,
|
|
188
|
+
},
|
|
189
|
+
filter: {
|
|
190
|
+
id,
|
|
111
191
|
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
192
|
+
}, { dontCollect: true, forUpdate: true });
|
|
193
|
+
const { operatorId, targetEntity, operEntity$oper: operEntities, action, data } = oper;
|
|
194
|
+
const entityIds = operEntities.map(ele => ele.entityId);
|
|
195
|
+
const pushEntityNodes = this.pushAccessMap[targetEntity];
|
|
196
|
+
if (pushEntityNodes && pushEntityNodes.length > 0) {
|
|
197
|
+
// 每个pushEntityNode代表配置的一个remoteEntity
|
|
198
|
+
await Promise.all(pushEntityNodes.map(async (node) => {
|
|
199
|
+
const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, actions, onSynchronized } = node;
|
|
200
|
+
if (!actions || actions.includes(action)) {
|
|
201
|
+
const pushed = [];
|
|
202
|
+
const rows = await context.select(targetEntity, {
|
|
203
|
+
data: {
|
|
204
|
+
id: 1,
|
|
205
|
+
...projection,
|
|
206
|
+
},
|
|
207
|
+
filter: {
|
|
208
|
+
id: {
|
|
209
|
+
$in: entityIds,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
}, { dontCollect: true, includedDeleted: true });
|
|
213
|
+
// userId就是需要发送给远端的user,但是要将本次操作的user过滤掉(操作的原本产生者)
|
|
214
|
+
const userSendDict = groupByUsers(rows);
|
|
215
|
+
const pushToUserIdFn = async (userId) => {
|
|
216
|
+
const { entity, entityId, rowIds } = userSendDict[userId];
|
|
217
|
+
// 推送到远端结点的oper
|
|
218
|
+
const oper2 = {
|
|
219
|
+
id: oper.id,
|
|
220
|
+
action: action,
|
|
221
|
+
data: (action === 'create' && data instanceof Array) ? data.filter(ele => rowIds.includes(ele.id)) : data,
|
|
222
|
+
filter: {
|
|
223
|
+
id: rowIds.length === 1 ? rowIds[0] : {
|
|
224
|
+
$in: rowIds,
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
bornAt: oper.bornAt,
|
|
228
|
+
targetEntity,
|
|
229
|
+
};
|
|
230
|
+
const { url } = await getRemoteAccessInfo(context, {
|
|
231
|
+
userId,
|
|
232
|
+
remoteEntityId: entityId,
|
|
233
|
+
});
|
|
234
|
+
await this.pushOper(oper, userId, url, endpoint, entity, entityId, selfEncryptInfo);
|
|
235
|
+
};
|
|
236
|
+
for (const userId in userSendDict) {
|
|
237
|
+
if (userId !== operatorId) {
|
|
238
|
+
pushed.push(pushToUserIdFn(userId));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (pushed.length > 0) {
|
|
242
|
+
// 对单个oper,这里必须要等所有的push返回,不然会一直等在上面
|
|
243
|
+
await Promise.all(pushed);
|
|
244
|
+
if (onSynchronized) {
|
|
245
|
+
await onSynchronized({
|
|
246
|
+
action: action,
|
|
247
|
+
data: data,
|
|
248
|
+
rowIds: entityIds,
|
|
249
|
+
}, context);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
123
252
|
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// 感觉应该跑不出来
|
|
139
|
-
console.warn('在sync数据时,遇到了重复推送的oper', JSON.stringify(oper), userId, url);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
async getSelfEncryptInfo() {
|
|
143
|
-
if (this.selfEncryptInfo) {
|
|
144
|
-
return this.selfEncryptInfo;
|
|
253
|
+
}));
|
|
254
|
+
// 到这里说明此oper成功,否则会在内部不停循环重试
|
|
255
|
+
// 主动去把oper上的跨事务标志清除,不依赖底层的triggerExecutor
|
|
256
|
+
await context.operate('oper', {
|
|
257
|
+
id: await (0, uuid_1.generateNewIdAsync)(),
|
|
258
|
+
action: 'update',
|
|
259
|
+
data: {
|
|
260
|
+
[types_1.TriggerDataAttribute]: null,
|
|
261
|
+
[types_1.TriggerUuidAttribute]: null,
|
|
262
|
+
},
|
|
263
|
+
filter: {
|
|
264
|
+
id
|
|
265
|
+
},
|
|
266
|
+
}, {});
|
|
145
267
|
}
|
|
146
|
-
|
|
147
|
-
return this.selfEncryptInfo;
|
|
268
|
+
return 0;
|
|
148
269
|
}
|
|
149
270
|
makeCreateOperTrigger() {
|
|
150
271
|
const { config } = this;
|
|
151
272
|
const { remotes, self } = config;
|
|
152
273
|
// 根据remotes定义,建立从entity到需要同步的远端结点信息的Map
|
|
153
|
-
const pushAccessMap = {};
|
|
154
274
|
remotes.forEach((remote) => {
|
|
155
|
-
const {
|
|
275
|
+
const { getPushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote } = remote;
|
|
156
276
|
if (pushEntityDefs) {
|
|
157
277
|
const pushEntities = [];
|
|
158
|
-
const endpoint2 = (0, path_1.join)(endpoint || 'sync',
|
|
278
|
+
const endpoint2 = (0, path_1.join)(endpoint || 'sync', self.entity);
|
|
159
279
|
for (const def of pushEntityDefs) {
|
|
160
280
|
const { path, relationName, recursive, entity, actions, onSynchronized } = def;
|
|
161
281
|
pushEntities.push(entity);
|
|
@@ -168,26 +288,32 @@ class Synchronizer {
|
|
|
168
288
|
}, recursive) : (0, relationPath_1.destructDirectPath)(this.schema, entity, path2, recursive);
|
|
169
289
|
const groupByUsers = (rows) => {
|
|
170
290
|
const userRowDict = {};
|
|
171
|
-
rows.
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
174
|
-
|
|
291
|
+
rows.forEach((row) => {
|
|
292
|
+
const goals = getData(row);
|
|
293
|
+
if (goals) {
|
|
294
|
+
goals.forEach(({ entity, entityId, userId }) => {
|
|
175
295
|
if (userRowDict[userId]) {
|
|
176
|
-
|
|
296
|
+
// 逻辑上来说同一个userId,其关联的entity和entityId必然相同,这个entity/entityId代表了对方
|
|
297
|
+
(0, assert_1.default)(userRowDict[userId].entity === entity && userRowDict[userId].entityId === entityId);
|
|
298
|
+
userRowDict[userId].rowIds.push(row.id);
|
|
177
299
|
}
|
|
178
300
|
else {
|
|
179
|
-
userRowDict[userId] =
|
|
301
|
+
userRowDict[userId] = {
|
|
302
|
+
entity,
|
|
303
|
+
entityId,
|
|
304
|
+
rowIds: [row.id],
|
|
305
|
+
};
|
|
180
306
|
}
|
|
181
307
|
});
|
|
182
308
|
}
|
|
183
309
|
});
|
|
184
310
|
return userRowDict;
|
|
185
311
|
};
|
|
186
|
-
if (!pushAccessMap[entity]) {
|
|
187
|
-
pushAccessMap[entity] = [{
|
|
312
|
+
if (!this.pushAccessMap[entity]) {
|
|
313
|
+
this.pushAccessMap[entity] = [{
|
|
188
314
|
projection,
|
|
189
315
|
groupByUsers,
|
|
190
|
-
getRemotePushInfo,
|
|
316
|
+
getRemotePushInfo: getPushInfo,
|
|
191
317
|
endpoint: endpoint2,
|
|
192
318
|
entity,
|
|
193
319
|
actions,
|
|
@@ -195,10 +321,10 @@ class Synchronizer {
|
|
|
195
321
|
}];
|
|
196
322
|
}
|
|
197
323
|
else {
|
|
198
|
-
pushAccessMap[entity].push({
|
|
324
|
+
this.pushAccessMap[entity].push({
|
|
199
325
|
projection,
|
|
200
326
|
groupByUsers,
|
|
201
|
-
getRemotePushInfo,
|
|
327
|
+
getRemotePushInfo: getPushInfo,
|
|
202
328
|
endpoint: endpoint2,
|
|
203
329
|
entity,
|
|
204
330
|
actions,
|
|
@@ -208,7 +334,7 @@ class Synchronizer {
|
|
|
208
334
|
}
|
|
209
335
|
}
|
|
210
336
|
});
|
|
211
|
-
const pushEntities = Object.keys(pushAccessMap);
|
|
337
|
+
const pushEntities = Object.keys(this.pushAccessMap);
|
|
212
338
|
// push相关联的entity,在发生操作时,需要将operation推送到远端
|
|
213
339
|
const createOperTrigger = {
|
|
214
340
|
name: 'push oper to remote node',
|
|
@@ -222,88 +348,9 @@ class Synchronizer {
|
|
|
222
348
|
},
|
|
223
349
|
fn: async ({ ids }, context) => {
|
|
224
350
|
(0, assert_1.default)(ids.length === 1);
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
action: 1,
|
|
229
|
-
data: 1,
|
|
230
|
-
targetEntity: 1,
|
|
231
|
-
operatorId: 1,
|
|
232
|
-
operEntity$oper: {
|
|
233
|
-
$entity: 'operEntity',
|
|
234
|
-
data: {
|
|
235
|
-
id: 1,
|
|
236
|
-
entity: 1,
|
|
237
|
-
entityId: 1,
|
|
238
|
-
},
|
|
239
|
-
},
|
|
240
|
-
bornAt: 1,
|
|
241
|
-
$$createAt$$: 1,
|
|
242
|
-
},
|
|
243
|
-
filter: {
|
|
244
|
-
id: ids[0],
|
|
245
|
-
}
|
|
246
|
-
}, { dontCollect: true, forUpdate: true });
|
|
247
|
-
const { operatorId, targetEntity, operEntity$oper: operEntities, action, data } = oper;
|
|
248
|
-
const entityIds = operEntities.map(ele => ele.entityId);
|
|
249
|
-
const pushEntityNodes = pushAccessMap[targetEntity];
|
|
250
|
-
if (pushEntityNodes && pushEntityNodes.length > 0) {
|
|
251
|
-
// 每个pushEntityNode代表配置的一个remoteEntity
|
|
252
|
-
await Promise.all(pushEntityNodes.map(async (node) => {
|
|
253
|
-
const { projection, groupByUsers, getRemotePushInfo: getRemoteAccessInfo, endpoint, entity, actions, onSynchronized } = node;
|
|
254
|
-
if (!actions || actions.includes(action)) {
|
|
255
|
-
const pushed = [];
|
|
256
|
-
const rows = await context.select(targetEntity, {
|
|
257
|
-
data: {
|
|
258
|
-
id: 1,
|
|
259
|
-
...projection,
|
|
260
|
-
},
|
|
261
|
-
filter: {
|
|
262
|
-
id: {
|
|
263
|
-
$in: entityIds,
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
}, { dontCollect: true, includedDeleted: true });
|
|
267
|
-
// userId就是需要发送给远端的user,但是要将本次操作的user过滤掉(操作的原本产生者)
|
|
268
|
-
const userSendDict = groupByUsers(rows);
|
|
269
|
-
const pushToUserIdFn = async (userId) => {
|
|
270
|
-
const rowIds = userSendDict[userId];
|
|
271
|
-
// 推送到远端结点的oper
|
|
272
|
-
const oper2 = {
|
|
273
|
-
id: oper.id,
|
|
274
|
-
action: action,
|
|
275
|
-
data: (action === 'create' && data instanceof Array) ? data.filter(ele => rowIds.includes(ele.id)) : data,
|
|
276
|
-
filter: {
|
|
277
|
-
id: rowIds.length === 1 ? rowIds[0] : {
|
|
278
|
-
$in: rowIds,
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
bornAt: oper.bornAt,
|
|
282
|
-
targetEntity,
|
|
283
|
-
};
|
|
284
|
-
const { url } = await getRemoteAccessInfo(userId);
|
|
285
|
-
await this.pushOper(oper2 /** 这里不明白为什么TS过不去 */, userId, url, endpoint);
|
|
286
|
-
};
|
|
287
|
-
for (const userId in userSendDict) {
|
|
288
|
-
if (userId !== operatorId) {
|
|
289
|
-
pushed.push(pushToUserIdFn(userId));
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (pushed.length > 0) {
|
|
293
|
-
await Promise.all(pushed);
|
|
294
|
-
if (onSynchronized) {
|
|
295
|
-
await onSynchronized({
|
|
296
|
-
action: action,
|
|
297
|
-
data: data,
|
|
298
|
-
rowIds: entityIds,
|
|
299
|
-
}, context);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
}));
|
|
304
|
-
return entityIds.length * pushEntityNodes.length;
|
|
305
|
-
}
|
|
306
|
-
return 0;
|
|
351
|
+
const selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
|
|
352
|
+
this.synchronizeOpersToRemote(ids[0], context, selfEncryptInfo);
|
|
353
|
+
throw new types_1.OakException('consistency on oper will be managed by myself');
|
|
307
354
|
}
|
|
308
355
|
};
|
|
309
356
|
return createOperTrigger;
|
|
@@ -319,17 +366,40 @@ class Synchronizer {
|
|
|
319
366
|
getSyncTriggers() {
|
|
320
367
|
return [this.makeCreateOperTrigger()];
|
|
321
368
|
}
|
|
322
|
-
|
|
369
|
+
getSyncRoutine() {
|
|
370
|
+
return {
|
|
371
|
+
name: 'checkpoint routine for sync',
|
|
372
|
+
entity: 'oper',
|
|
373
|
+
filter: {
|
|
374
|
+
[types_1.TriggerDataAttribute]: {
|
|
375
|
+
$exists: true,
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
projection: {
|
|
379
|
+
id: 1,
|
|
380
|
+
[types_1.TriggerDataAttribute]: 1,
|
|
381
|
+
},
|
|
382
|
+
fn: async (context, data) => {
|
|
383
|
+
for (const ele of data) {
|
|
384
|
+
const { id, [types_1.TriggerDataAttribute]: triggerData } = ele;
|
|
385
|
+
const { cxtStr = '{}' } = triggerData;
|
|
386
|
+
await context.initialize(JSON.parse(cxtStr), true);
|
|
387
|
+
const selfEncryptInfo = await this.config.self.getSelfEncryptInfo(context);
|
|
388
|
+
this.synchronizeOpersToRemote(id, context, selfEncryptInfo);
|
|
389
|
+
}
|
|
390
|
+
return {};
|
|
391
|
+
}
|
|
392
|
+
};
|
|
323
393
|
}
|
|
324
394
|
getSelfEndpoint() {
|
|
325
395
|
return {
|
|
326
396
|
name: this.config.self.endpoint || 'sync',
|
|
327
397
|
method: 'post',
|
|
328
|
-
params: ['entity'],
|
|
398
|
+
params: ['entity', 'entityId'],
|
|
329
399
|
fn: async (context, params, headers, req, body) => {
|
|
330
400
|
// body中是传过来的oper数组信息
|
|
331
|
-
const { entity } = params;
|
|
332
|
-
const { [
|
|
401
|
+
const { entity, entityId } = params;
|
|
402
|
+
const { [OAK_SYNC_HEADER_ENTITY]: meEntity, [OAK_SYNC_HEADER_ENTITYID]: meEntityId } = headers;
|
|
333
403
|
console.log('接收到来自远端的sync数据', entity, JSON.stringify(body));
|
|
334
404
|
const successIds = [];
|
|
335
405
|
let failed;
|
|
@@ -337,22 +407,29 @@ class Synchronizer {
|
|
|
337
407
|
if (!this.remotePullInfoMap[entity]) {
|
|
338
408
|
this.remotePullInfoMap[entity] = {};
|
|
339
409
|
}
|
|
340
|
-
if (!this.remotePullInfoMap[entity][
|
|
341
|
-
const {
|
|
410
|
+
if (!this.remotePullInfoMap[entity][entityId]) {
|
|
411
|
+
const { getPullInfo, pullEntities } = this.config.remotes.find(ele => ele.entity === entity);
|
|
342
412
|
const pullEntityDict = {};
|
|
343
413
|
if (pullEntities) {
|
|
344
414
|
pullEntities.forEach((def) => pullEntityDict[def.entity] = def);
|
|
345
415
|
}
|
|
346
|
-
this.remotePullInfoMap[entity][
|
|
347
|
-
pullInfo: await
|
|
416
|
+
this.remotePullInfoMap[entity][entityId] = {
|
|
417
|
+
pullInfo: await getPullInfo(context, {
|
|
418
|
+
selfId: meEntityId,
|
|
419
|
+
remoteEntityId: entityId,
|
|
420
|
+
}),
|
|
348
421
|
pullEntityDict,
|
|
349
422
|
};
|
|
350
423
|
}
|
|
351
|
-
const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][
|
|
352
|
-
const { userId, algorithm, publicKey } = pullInfo;
|
|
353
|
-
// todo 解密
|
|
424
|
+
const { pullInfo, pullEntityDict } = this.remotePullInfoMap[entity][entityId];
|
|
425
|
+
const { userId, algorithm, publicKey, cxtInfo } = pullInfo;
|
|
354
426
|
(0, assert_1.default)(userId);
|
|
355
|
-
|
|
427
|
+
context.setCurrentUserId(userId);
|
|
428
|
+
if (cxtInfo) {
|
|
429
|
+
await context.initialize(cxtInfo);
|
|
430
|
+
}
|
|
431
|
+
// todo 解密
|
|
432
|
+
if (!this.pullMaxBornAtMap.hasOwnProperty(entityId)) {
|
|
356
433
|
const [maxHisOper] = await context.select('oper', {
|
|
357
434
|
data: {
|
|
358
435
|
id: 1,
|
|
@@ -372,10 +449,9 @@ class Synchronizer {
|
|
|
372
449
|
indexFrom: 0,
|
|
373
450
|
count: 1,
|
|
374
451
|
}, { dontCollect: true });
|
|
375
|
-
this.pullMaxBornAtMap[
|
|
452
|
+
this.pullMaxBornAtMap[entityId] = maxHisOper?.bornAt || 0;
|
|
376
453
|
}
|
|
377
|
-
let maxBornAt = this.pullMaxBornAtMap[
|
|
378
|
-
context.setCurrentUserId(userId);
|
|
454
|
+
let maxBornAt = this.pullMaxBornAtMap[entityId];
|
|
379
455
|
const opers = body;
|
|
380
456
|
const outdatedOpers = opers.filter(ele => ele.bornAt <= maxBornAt);
|
|
381
457
|
const freshOpers = opers.filter(ele => ele.bornAt > maxBornAt);
|
|
@@ -431,6 +507,7 @@ class Synchronizer {
|
|
|
431
507
|
maxBornAt = bornAt;
|
|
432
508
|
}
|
|
433
509
|
catch (err) {
|
|
510
|
+
console.error(err);
|
|
434
511
|
console.error('sync时出错', entity, JSON.stringify(freshOper));
|
|
435
512
|
failed = {
|
|
436
513
|
id,
|
|
@@ -441,7 +518,7 @@ class Synchronizer {
|
|
|
441
518
|
}
|
|
442
519
|
})()
|
|
443
520
|
]);
|
|
444
|
-
this.pullMaxBornAtMap[
|
|
521
|
+
this.pullMaxBornAtMap[entityId] = maxBornAt;
|
|
445
522
|
return {
|
|
446
523
|
successIds,
|
|
447
524
|
failed,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oak-backend-base",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.4",
|
|
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.2.5",
|
|
24
24
|
"oak-db": "^3.1.0",
|
|
25
|
-
"oak-domain": "^4.2.
|
|
26
|
-
"oak-frontend-base": "^4.2.
|
|
25
|
+
"oak-domain": "^4.2.8",
|
|
26
|
+
"oak-frontend-base": "^4.2.7",
|
|
27
27
|
"socket.io": "^4.7.2",
|
|
28
28
|
"socket.io-client": "^4.7.2",
|
|
29
29
|
"uuid": "^8.3.2"
|